2025-11-26 00:22:09 +01:00
use std ::env ;
2025-11-27 19:34:46 +01:00
use std ::io ::Write ;
2025-11-26 00:22:09 +01:00
extern crate clap ;
2025-11-30 12:51:52 +01:00
use clap ::{ Command , arg , command };
2025-12-25 17:10:44 +00:00
use pkh ::context ::ContextConfig ;
2025-11-26 00:22:09 +01:00
extern crate flate2 ;
2025-12-10 18:40:32 +01:00
use pkh ::changelog ::generate_entry ;
2025-11-26 21:30:34 +01:00
2025-11-27 19:34:46 +01:00
use indicatif_log_bridge ::LogWrapper ;
2025-11-30 12:51:52 +01:00
use log ::{ error , info };
2025-11-27 19:34:46 +01:00
2025-11-27 23:43:48 +01:00
mod ui ;
2026-07-17 10:12:46 +02:00
/// Obtain the current working directory, exiting with a helpful message on failure.
fn current_dir_or_exit () -> std ::path ::PathBuf {
match std ::env ::current_dir () {
Ok ( p ) => p ,
Err ( e ) => {
error! ( "Could not determine the current working directory: {}" , e );
std ::process ::exit ( 1 );
}
}
}
2025-11-26 00:22:09 +01:00
fn main () {
let rt = tokio ::runtime ::Runtime ::new (). unwrap ();
2025-11-27 19:34:46 +01:00
let logger =
env_logger ::Builder ::from_env ( env_logger ::Env ::default (). default_filter_or ( "info" ))
. format_timestamp ( None )
2025-11-30 12:51:52 +01:00
. format ( | buf , record | writeln! ( buf , "{}" , record . args ()))
2025-11-27 19:34:46 +01:00
. build ();
let multi = indicatif ::MultiProgress ::new ();
2025-11-30 12:51:52 +01:00
LogWrapper ::new ( multi . clone (), logger ). try_init (). unwrap ();
let matches = command! ()
2025-11-26 00:22:09 +01:00
. subcommand_required ( true )
. disable_version_flag ( true )
. subcommand (
2025-12-01 17:57:57 +01:00
Command ::new ( "pull" )
. about ( "Pull a source package from the archive or git" )
2025-11-26 00:22:09 +01:00
. arg (
2025-11-30 12:51:52 +01:00
arg! ( - s -- series < series > "Target package distribution series" ). required ( false ),
2025-11-26 00:22:09 +01:00
)
2025-11-28 23:47:55 +01:00
. arg (
arg! ( - d -- dist < dist > "Target package distribution (debian, ubuntu)" )
2025-11-30 12:51:52 +01:00
. required ( false ),
2025-11-28 23:47:55 +01:00
)
2025-11-30 12:51:52 +01:00
. arg ( arg! ( - v -- version < version > "Target package version" ). required ( false ))
2026-01-12 22:42:09 +01:00
. arg ( arg! ( -- archive "Only use the archive to download package source, not git" ). required ( false ))
2025-11-30 12:51:52 +01:00
. arg ( arg! ( -- ppa < ppa > "Download the package from a specific PPA" ). required ( false ))
. arg ( arg! ( < package > "Target package" )),
2025-11-26 00:22:09 +01:00
)
. subcommand (
2025-11-26 21:30:34 +01:00
Command ::new ( "chlog" )
2025-11-26 00:22:09 +01:00
. about ( "Auto-generate changelog entry, editing it, committing it afterwards" )
. arg ( arg! ( - s -- series < series > "Target distribution series" ). required ( false ))
. arg ( arg! ( -- backport "This changelog is for a backport entry" ). required ( false ))
2025-11-30 12:51:52 +01:00
. arg ( arg! ( - v -- version < version > "Target version" ). required ( false )),
2025-11-26 00:22:09 +01:00
)
2026-01-11 12:16:27 +01:00
. subcommand ( Command ::new ( "build" ). about ( "Build the source package (into a .dsc)" ))
2025-12-11 19:27:06 +01:00
. subcommand (
Command ::new ( "deb" )
2026-01-11 12:16:27 +01:00
. about ( "Build the source package into binary package (.deb)" )
2025-12-11 19:27:06 +01:00
. arg ( arg! ( - s -- series < series > "Target distribution series" ). required ( false ))
2025-12-25 17:10:44 +00:00
. arg ( arg! ( - a -- arch < arch > "Target architecture" ). required ( false ))
2026-02-19 15:36:08 +01:00
. arg ( arg! ( -- ppa < ppa > "Build the package adding a specific PPA for dependencies (can be specified multiple times)" )
. long_help ( "Build the package adding a specific PPA for dependencies. Can be specified multiple times." ). required ( false ). action ( clap ::ArgAction ::Append ))
2026-02-18 23:19:57 +01:00
. arg ( arg! ( -- inject < package > "Inject a package into the build environment (can be specified multiple times)" )
. long_help ( "Inject a package into the build environment before build-dep. Can be a .deb file path, a package name from the archive, or a package from a previously added PPA. Can be specified multiple times." ). required ( false ). action ( clap ::ArgAction ::Append ))
2025-12-25 17:10:44 +00:00
. arg ( arg! ( -- cross "Cross-compile for target architecture (instead of qemu-binfmt)" )
. long_help ( "Cross-compile for target architecture (instead of using qemu-binfmt) \n Note that most packages cannot be cross-compiled" ). required ( false ))
2026-07-24 14:32:47 +02:00
. arg ( arg! ( -- mode < mode > "Change build mode [local]" ). required ( false )
. long_help ( "Change build mode [local] \n Default will chose depending on other parameters, don't provide if unsure" )),
2025-12-15 20:48:44 +01:00
)
. subcommand (
Command ::new ( "context" )
. about ( "Manage contexts" )
. subcommand_required ( true )
. subcommand (
Command ::new ( "create" )
. about ( "Create a new context" )
. arg ( arg! ( < name > "Context name" ))
. arg ( arg! ( -- type < type > "Context type: ssh (only type supported for now)" ))
. arg ( arg! ( -- endpoint < endpoint > "Context endpoint (for example: ssh://user@host:port)" ))
)
. subcommand (
Command ::new ( "rm" )
. about ( "Remove a context" )
. arg ( arg! ( < name > "Context name" ))
)
2025-12-25 17:10:44 +00:00
. subcommand (
Command ::new ( "ls" )
. about ( "List contexts" )
)
2025-12-15 20:48:44 +01:00
. subcommand ( Command ::new ( "show" ). about ( "Show current context" ))
. subcommand (
Command ::new ( "use" )
. about ( "Set current context" )
. arg ( arg! ( < name > "Context name" ))
)
2025-12-11 19:27:06 +01:00
)
2025-11-26 00:22:09 +01:00
. get_matches ();
2025-11-30 12:51:52 +01:00
2025-11-26 00:22:09 +01:00
match matches . subcommand () {
2025-12-01 17:57:57 +01:00
Some (( "pull" , sub_matches )) => {
2025-11-26 00:22:09 +01:00
let package = sub_matches . get_one ::< String > ( "package" ). expect ( "required" );
2025-11-28 23:47:55 +01:00
let series = sub_matches . get_one ::< String > ( "series" ). map ( | s | s . as_str ());
let dist = sub_matches . get_one ::< String > ( "dist" ). map ( | s | s . as_str ());
2026-01-11 12:12:19 +01:00
let version = sub_matches . get_one ::< String > ( "version" ). map ( | s | s . as_str ());
2026-01-29 17:11:01 +01:00
let ppa = sub_matches . get_one ::< String > ( "ppa" ). map ( | s | s . as_str ());
2026-01-12 22:42:09 +01:00
let archive = sub_matches . get_one ::< bool > ( "archive" ). unwrap_or ( & false );
2025-11-30 12:51:52 +01:00
2025-12-01 00:05:39 +01:00
let ( pb , progress_callback ) = ui ::create_progress_bar ( & multi );
2025-11-27 19:34:46 +01:00
2026-01-29 17:11:01 +01:00
// Convert PPA to base URL if provided
let base_url = ppa . and_then ( | ppa_str | {
// PPA format: user/ppa_name
let parts : Vec <& str > = ppa_str . split ( '/' ). collect ();
if parts . len () == 2 {
Some ( pkh ::package_info ::ppa_to_base_url ( parts [ 0 ], parts [ 1 ]))
} else {
None
}
});
2026-01-01 18:37:40 +01:00
// Since pull is async, we need to block on it
2026-01-11 12:12:19 +01:00
if let Err ( e ) = rt . block_on ( async {
let package_info = pkh ::package_info ::lookup (
package ,
version ,
series ,
"" ,
dist ,
2026-01-29 17:11:01 +01:00
base_url . as_deref (),
2026-01-11 12:12:19 +01:00
Some ( & progress_callback ),
)
. await ? ;
2026-01-12 22:42:09 +01:00
pkh ::pull ::pull ( & package_info , None , Some ( & progress_callback ), * archive ). await
2026-01-11 12:12:19 +01:00
}) {
2025-11-27 19:34:46 +01:00
pb . finish_and_clear ();
error! ( "{}" , e );
2025-11-26 00:22:09 +01:00
std ::process ::exit ( 1 );
}
2025-11-27 19:34:46 +01:00
pb . finish_and_clear ();
multi . remove ( & pb );
info! ( "Done." );
2025-11-30 12:51:52 +01:00
}
2025-11-26 21:30:34 +01:00
Some (( "chlog" , sub_matches )) => {
2026-07-17 10:12:46 +02:00
let cwd = current_dir_or_exit ();
2025-11-26 21:30:34 +01:00
let version = sub_matches . get_one ::< String > ( "version" ). map ( | s | s . as_str ());
2026-08-11 14:42:23 +02:00
let cli_series = sub_matches . get_one ::< String > ( "series" ). map ( | s | s . as_str ());
2025-11-26 21:30:34 +01:00
2026-08-11 14:42:23 +02:00
// Determine target series: CLI flag > interactive selector > current changelog series
let target_series = if let Some ( s ) = cli_series {
Some ( s . to_string ())
} else {
// Parse current changelog to determine the default series
let changelog_path = cwd . join ( "debian/changelog" );
match pkh ::changelog ::parse_changelog_header ( & changelog_path ) {
Ok (( _pkg , _ver , current_series )) => {
// Try to get the list of available series for this distribution
match rt . block_on ( async {
let dist =
pkh ::distro_info ::get_dist_from_series ( & current_series ). await ? ;
pkh ::distro_info ::get_ordered_series_name ( & dist ). await
}) {
Ok ( series_list ) => {
match ui ::select_series ( & series_list , & current_series ) {
Ok ( selected ) => Some ( selected ),
Err ( e ) => {
error! (
"Series selection failed: {}. Using current series '{}' instead." ,
e , current_series
);
Some ( current_series )
}
}
}
Err ( _ ) => {
// Could not fetch series list, use current series as default
Some ( current_series )
}
}
}
Err ( _ ) => None ,
}
};
if let Err ( e ) = generate_entry (
"debian/changelog" ,
Some ( & cwd ),
version ,
target_series . as_deref (),
) {
2025-11-27 19:34:46 +01:00
error! ( "{}" , e );
2025-11-26 21:30:34 +01:00
std ::process ::exit ( 1 );
}
2026-07-17 10:12:46 +02:00
let editor = match std ::env ::var ( "EDITOR" ) {
Ok ( e ) => e ,
Err ( _ ) => {
error! (
"No editor configured. Set the EDITOR environment variable \
(e.g. `EDITOR=nano` or `export EDITOR=vim`) and retry."
);
std ::process ::exit ( 1 );
}
};
let _status = std ::process ::Command ::new ( & editor )
2025-11-26 21:30:34 +01:00
. current_dir ( & cwd )
2025-12-01 00:05:39 +01:00
. args ([ "debian/changelog" ])
2026-07-17 10:12:46 +02:00
. status ()
. map_err ( | e | {
error! (
"Could not launch editor '{}': {}. \
Make sure it is installed and available on PATH." ,
editor , e
);
std ::process ::exit ( 1 );
});
2025-11-30 12:51:52 +01:00
}
2025-12-01 20:56:50 +01:00
Some (( "build" , _sub_matches )) => {
2026-07-17 10:12:46 +02:00
let cwd = current_dir_or_exit ();
2025-12-01 20:56:50 +01:00
if let Err ( e ) = pkh ::build ::build_source_package ( Some ( & cwd )) {
error! ( "{}" , e );
std ::process ::exit ( 1 );
}
}
2025-12-11 19:27:06 +01:00
Some (( "deb" , sub_matches )) => {
2026-07-17 10:12:46 +02:00
let cwd = current_dir_or_exit ();
2025-12-11 19:27:06 +01:00
let series = sub_matches . get_one ::< String > ( "series" ). map ( | s | s . as_str ());
let arch = sub_matches . get_one ::< String > ( "arch" ). map ( | s | s . as_str ());
2025-12-25 17:10:44 +00:00
let cross = sub_matches . get_one ::< bool > ( "cross" ). unwrap_or ( & false );
2026-02-19 15:36:08 +01:00
let ppa : Vec <& str > = sub_matches
. get_many ::< String > ( "ppa" )
. map ( | v | v . map ( | s | s . as_str ()). collect ())
. unwrap_or_default ();
let ppa = if ppa . is_empty () {
None
} else {
Some ( ppa . as_slice ())
};
2026-02-18 23:19:57 +01:00
let inject_packages : Vec <& str > = sub_matches
. get_many ::< String > ( "inject" )
. map ( | v | v . map ( | s | s . as_str ()). collect ())
. unwrap_or_default ();
let inject_packages = if inject_packages . is_empty () {
None
} else {
Some ( inject_packages . as_slice ())
};
2025-12-25 17:10:44 +00:00
let mode : Option <& str > = sub_matches . get_one ::< String > ( "mode" ). map ( | s | s . as_str ());
let mode : Option < pkh ::deb ::BuildMode > = match mode {
Some ( "local" ) => Some ( pkh ::deb ::BuildMode ::Local ),
_ => None ,
};
2025-12-11 19:27:06 +01:00
2026-01-26 10:14:04 +01:00
if let Err ( e ) = rt . block_on ( async {
2026-02-18 23:19:57 +01:00
pkh ::deb ::build_binary_package (
arch ,
series ,
Some ( cwd . as_path ()),
* cross ,
mode ,
ppa ,
inject_packages ,
2026-03-19 00:24:48 +01:00
None ,
2026-02-18 23:19:57 +01:00
)
. await
2026-01-26 10:14:04 +01:00
}) {
2025-12-10 18:40:32 +01:00
error! ( "{}" , e );
std ::process ::exit ( 1 );
}
}
2025-12-15 20:48:44 +01:00
Some (( "context" , sub_matches )) => {
2025-12-25 17:10:44 +00:00
let mgr = pkh ::context ::manager ();
2025-12-15 20:48:44 +01:00
match sub_matches . subcommand () {
Some (( "create" , args )) => {
let name = args . get_one ::< String > ( "name" ). unwrap ();
let type_str = args
. get_one ::< String > ( "type" )
. map ( | s | s . as_str ())
. unwrap_or ( "local" );
let context = match type_str {
2025-12-25 17:10:44 +00:00
"local" => ContextConfig ::Local ,
2025-12-15 20:48:44 +01:00
"ssh" => {
2026-07-22 14:36:28 +02:00
let endpoint =
args . get_one ::< String > ( "endpoint" ). unwrap_or_else ( || {
2026-07-17 10:12:46 +02:00
error! (
"An --endpoint is required to create an ssh context. \
Expected format: [ssh://][user@]host[:port]"
);
std ::process ::exit ( 1 );
});
2025-12-15 20:48:44 +01:00
// Parse host, user, port from endpoint
// Formats: [ssh://][user@]host[:port]
let endpoint_re = regex ::Regex ::new ( r "^(?:ssh://)?(?:(?P<user>[^@]+)@)?(?P<host>[^:/]+)(?::(?P<port>\d+))?$" ). unwrap ();
let endpoint_cap = endpoint_re . captures ( endpoint ). unwrap_or_else ( || {
error! ( "Invalid endpoint format: '{}'. Expected [ssh://][user@]host[:port]" , endpoint );
std ::process ::exit ( 1 );
});
let host = endpoint_cap . name ( "host" ). unwrap (). as_str (). to_string ();
let user = endpoint_cap . name ( "user" ). map ( | m | m . as_str (). to_string ());
let port = endpoint_cap . name ( "port" ). map ( | m | {
m . as_str (). parse ::< u16 > (). unwrap_or_else ( | _ | {
error! ( "Invalid port number" );
std ::process ::exit ( 1 );
})
});
2025-12-25 17:10:44 +00:00
ContextConfig ::Ssh { host , user , port }
2025-12-15 20:48:44 +01:00
}
_ => {
error! ( "Unknown context type: {}" , type_str );
std ::process ::exit ( 1 );
}
};
if let Err ( e ) = mgr . add_context ( name , context ) {
error! ( "Failed to create context: {}" , e );
std ::process ::exit ( 1 );
}
info! ( "Context '{}' created." , name );
}
Some (( "rm" , args )) => {
let name = args . get_one ::< String > ( "name" ). unwrap ();
if let Err ( e ) = mgr . remove_context ( name ) {
error! ( "Failed to remove context: {}" , e );
std ::process ::exit ( 1 );
}
info! ( "Context '{}' removed." , name );
}
Some (( "ls" , _ )) => {
let contexts = mgr . list_contexts ();
let current = mgr . current_name ();
for ctx in contexts {
2025-12-25 17:10:44 +00:00
if ctx == current {
2025-12-15 20:48:44 +01:00
println! ( "* {} " , ctx );
} else {
println! ( " {} " , ctx );
}
}
}
2025-12-25 17:10:44 +00:00
Some (( "show" , _ )) => {}
2025-12-15 20:48:44 +01:00
Some (( "use" , args )) => {
let name = args . get_one ::< String > ( "name" ). unwrap ();
if let Err ( e ) = mgr . set_current ( name ) {
error! ( "Failed to set context: {}" , e );
std ::process ::exit ( 1 );
}
info! ( "Switched to context '{}'." , name );
}
_ => unreachable! (),
}
}
2025-11-26 00:22:09 +01:00
_ => unreachable! ( "Exhausted list of subcommands and subcommand_required prevents `None`" ),
}
2025-11-30 12:51:52 +01:00
}