2022-03-24 19:48:41 -03:00
use clap ::{ Parser , Subcommand } ;
2023-03-25 17:06:01 +01:00
use libips ::actions ::{ ActionError , File , Manifest } ;
2025-07-26 10:34:45 +02:00
use libips ::repository ::{ ReadableRepository , WritableRepository , FileBackend } ;
2021-04-19 09:35:05 -03:00
2025-07-21 23:20:19 +02:00
use anyhow ::{ Result , anyhow } ;
2021-04-24 23:19:25 -03:00
use std ::collections ::HashMap ;
2022-09-01 19:23:09 -03:00
use std ::fs ::{ read_dir , OpenOptions } ;
use std ::io ::Write ;
use std ::path ::{ Path , PathBuf } ;
use userland ::repology ::find_newest_version ;
2023-03-25 17:06:01 +01:00
use userland ::{ Component , Makefile } ;
2021-04-19 09:35:05 -03:00
2022-03-24 19:48:41 -03:00
#[ derive(Parser, Debug) ]
#[ clap(author, version, about, long_about = None) ]
#[ clap(propagate_version = true) ]
struct App {
#[ clap(subcommand) ]
2022-09-01 19:23:09 -03:00
command : Commands ,
2022-03-24 19:48:41 -03:00
}
2021-04-19 09:35:05 -03:00
2022-03-24 19:48:41 -03:00
#[ derive(Subcommand, Debug) ]
enum Commands {
2022-09-01 19:23:09 -03:00
DiffComponent {
2022-03-24 19:48:41 -03:00
component : String ,
2022-09-01 19:23:09 -03:00
#[ clap(short) ]
replacements : Option < Vec < String > > ,
/// Place the file actions missing in the manifests but present in sample-manifest into this file
#[ clap(short = 'm') ]
output_manifest : Option < PathBuf > ,
2022-03-24 19:48:41 -03:00
} ,
2022-09-01 19:23:09 -03:00
ShowComponent {
2022-03-24 19:48:41 -03:00
component : String ,
} ,
2025-07-21 23:20:19 +02:00
/// Publish a package to a repository
Publish {
/// Path to the manifest file
#[ clap(short = 'm', long) ]
manifest_path : PathBuf ,
/// Path to the prototype directory containing the files to publish
#[ clap(short = 'p', long) ]
prototype_dir : PathBuf ,
/// Path to the repository
#[ clap(short = 'r', long) ]
repo_path : PathBuf ,
/// Publisher name (defaults to "test" if not specified)
#[ clap(short = 'u', long) ]
publisher : Option < String > ,
} ,
2022-03-24 19:48:41 -03:00
}
fn main ( ) -> Result < ( ) > {
let cli = App ::parse ( ) ;
match & cli . command {
2022-09-01 19:23:09 -03:00
Commands ::ShowComponent { component } = > show_component_info ( component ) ,
Commands ::DiffComponent {
component ,
replacements ,
output_manifest ,
} = > diff_component ( component , replacements , output_manifest ) ,
2025-07-21 23:20:19 +02:00
Commands ::Publish {
manifest_path ,
prototype_dir ,
repo_path ,
publisher ,
} = > publish_package ( manifest_path , prototype_dir , repo_path , publisher ) ,
2021-04-25 18:40:31 -03:00
}
2021-04-24 23:19:25 -03:00
}
2023-03-25 17:06:01 +01:00
fn parse_tripplet_replacements ( replacements : & [ String ] ) -> HashMap < String , String > {
2022-09-01 19:23:09 -03:00
let mut map = HashMap ::new ( ) ;
for pair in replacements
2023-03-25 17:06:01 +01:00
. iter ( )
2022-09-01 19:23:09 -03:00
. map ( | str | {
2023-03-25 17:06:01 +01:00
str . split_once ( ':' )
2022-09-01 19:23:09 -03:00
. map ( | s | ( s . 0. to_owned ( ) , s . 1. to_owned ( ) ) )
. unwrap_or ( ( String ::new ( ) , String ::new ( ) ) )
} )
. collect ::< Vec < ( String , String ) > > ( )
{
map . insert ( pair . 0 , pair . 1 ) ;
}
map
}
fn diff_component (
component_path : impl AsRef < Path > ,
replacements : & Option < Vec < String > > ,
output_manifest : & Option < PathBuf > ,
) -> Result < ( ) > {
let replacements = if let Some ( replacements ) = replacements {
let map = parse_tripplet_replacements ( replacements ) ;
Some ( map )
} else {
None
} ;
2022-03-24 19:48:41 -03:00
let files = read_dir ( & component_path ) ? ;
2021-04-24 23:19:25 -03:00
let manifest_files : Vec < String > = files
. filter_map ( std ::result ::Result ::ok )
2022-09-01 19:23:09 -03:00
. filter ( | d | {
if let Some ( e ) = d . path ( ) . extension ( ) {
e = = " p5m "
} else {
false
}
} )
. map ( | e | e . path ( ) . into_os_string ( ) . into_string ( ) . unwrap ( ) )
. collect ( ) ;
2021-04-24 23:19:25 -03:00
2022-09-01 19:23:09 -03:00
let sample_manifest_file = & component_path
. as_ref ( )
. join ( " manifests/sample-manifest.p5m " ) ;
2021-04-24 23:19:25 -03:00
2023-03-25 17:06:01 +01:00
let manifests_res : Result < Vec < Manifest > , ActionError > =
manifest_files . iter ( ) . map ( Manifest ::parse_file ) . collect ( ) ;
2022-03-24 19:48:41 -03:00
2021-04-24 23:19:25 -03:00
let sample_manifest = Manifest ::parse_file ( sample_manifest_file ) ? ;
let manifests : Vec < Manifest > = manifests_res . unwrap ( ) ;
2022-09-01 19:23:09 -03:00
let missing_files =
find_files_missing_in_manifests ( & sample_manifest , manifests . clone ( ) , & replacements ) ? ;
2021-04-24 23:19:25 -03:00
2022-09-01 19:23:09 -03:00
for f in missing_files . clone ( ) {
2021-04-24 23:19:25 -03:00
println! ( " file {} is missing in the manifests " , f . path ) ;
}
2021-04-19 09:35:05 -03:00
2023-03-25 17:06:01 +01:00
let removed_files =
find_removed_files ( & sample_manifest , manifests , & component_path , & replacements ) ? ;
2021-04-19 09:35:05 -03:00
2021-04-24 23:19:25 -03:00
for f in removed_files {
2022-09-01 19:23:09 -03:00
println! (
" file path={} has been removed from the sample-manifest " ,
f . path
) ;
}
if let Some ( output_manifest ) = output_manifest {
let mut f = OpenOptions ::new ( )
. write ( true )
. truncate ( true )
. create ( true )
. open ( output_manifest ) ? ;
for action in missing_files {
writeln! ( & mut f , " file path={} " , action . path ) ? ;
}
2021-04-24 23:19:25 -03:00
}
2021-04-25 18:40:31 -03:00
Ok ( ( ) )
}
2022-03-24 19:48:41 -03:00
fn show_component_info < P : AsRef < Path > > ( component_path : P ) -> Result < ( ) > {
let makefile_path = component_path . as_ref ( ) . join ( " Makefile " ) ;
2021-04-25 18:40:31 -03:00
2023-03-25 17:06:01 +01:00
let initial_makefile = Makefile ::parse_single_file ( makefile_path ) ? ;
2022-03-28 20:27:15 -03:00
let makefile = initial_makefile . parse_all ( ) ? ;
2021-04-25 18:40:31 -03:00
let mut name = String ::new ( ) ;
2022-09-01 19:23:09 -03:00
let component = Component ::new_from_makefile ( & makefile ) ? ;
2022-03-28 15:07:05 -03:00
if let Some ( var ) = makefile . get ( " COMPONENT_NAME " ) {
2023-03-25 17:06:01 +01:00
println! ( " Name: {} " , var . replace ( '\n' , " \n \t " ) ) ;
2022-09-01 19:23:09 -03:00
if let Some ( component_name ) = makefile . get_first_value_of_variable_by_name ( " COMPONENT_NAME " )
{
2023-03-25 17:06:01 +01:00
name = component_name ;
2022-03-28 15:07:05 -03:00
}
2021-04-25 18:40:31 -03:00
}
2022-03-28 15:07:05 -03:00
if let Some ( var ) = makefile . get ( " COMPONENT_VERSION " ) {
2023-03-25 17:06:01 +01:00
println! ( " Version: {} " , var . replace ( '\n' , " \n \t " ) ) ;
2021-04-25 18:40:31 -03:00
let latest_version = find_newest_version ( & name ) ;
if latest_version . is_ok ( ) {
println! ( " Latest Version: {} " , latest_version ? ) ;
} else {
2022-09-01 19:23:09 -03:00
println! (
" Error: Could not get latest version info: {:?} " ,
latest_version . unwrap_err ( )
)
2021-04-25 18:40:31 -03:00
}
}
2022-03-28 15:07:05 -03:00
if let Some ( var ) = makefile . get ( " BUILD_BITS " ) {
2023-03-25 17:06:01 +01:00
println! ( " Build bits: {} " , var . replace ( '\n' , " \n \t " ) ) ;
2022-03-28 15:07:05 -03:00
}
if let Some ( var ) = makefile . get ( " COMPONENT_BUILD_ACTION " ) {
2023-03-25 17:06:01 +01:00
println! ( " Build action: {} " , var . replace ( '\n' , " \n \t " ) ) ;
2022-03-28 15:07:05 -03:00
}
if let Some ( var ) = makefile . get ( " COMPONENT_PROJECT_URL " ) {
2023-03-25 17:06:01 +01:00
println! ( " Project URl: {} " , var . replace ( '\n' , " \n \t " ) ) ;
2021-04-25 18:40:31 -03:00
}
2022-03-28 15:07:05 -03:00
if let Some ( var ) = makefile . get ( " COMPONENT_ARCHIVE_URL " ) {
2023-03-25 17:06:01 +01:00
println! ( " Source URl: {} " , var . replace ( '\n' , " \n \t " ) ) ;
2021-04-25 18:40:31 -03:00
}
2022-03-28 15:07:05 -03:00
if let Some ( var ) = makefile . get ( " COMPONENT_ARCHIVE_HASH " ) {
2023-03-25 17:06:01 +01:00
println! ( " Source Archive File Hash: {} " , var . replace ( '\n' , " \n \t " ) ) ;
2021-04-25 18:40:31 -03:00
}
2022-03-28 15:07:05 -03:00
if let Some ( var ) = makefile . get ( " REQUIRED_PACKAGES " ) {
2023-03-25 17:06:01 +01:00
println! ( " Dependencies: \n \t {} " , var . replace ( '\n' , " \n \t " ) ) ;
2021-04-25 18:40:31 -03:00
}
2022-03-28 15:07:05 -03:00
if let Some ( var ) = makefile . get ( " COMPONENT_INSTALL_ACTION " ) {
println! ( " Install Action: \n \t {} " , var ) ;
2021-04-25 18:40:31 -03:00
}
2022-09-01 19:23:09 -03:00
println! ( " Component: {:?} " , component ) ;
2021-04-24 23:19:25 -03:00
Ok ( ( ) )
}
// Show all files that have been removed in the sample-manifest
2022-09-01 19:23:09 -03:00
fn find_removed_files < P : AsRef < Path > > (
sample_manifest : & Manifest ,
manifests : Vec < Manifest > ,
component_path : P ,
replacements : & Option < HashMap < String , String > > ,
) -> Result < Vec < File > > {
2021-04-24 23:19:25 -03:00
let f_map = make_file_map ( sample_manifest . files . clone ( ) ) ;
2023-03-25 17:06:01 +01:00
let all_files : Vec < File > = manifests . iter ( ) . flat_map ( | m | m . files . clone ( ) ) . collect ( ) ;
2021-04-24 23:19:25 -03:00
let mut removed_files : Vec < File > = Vec ::new ( ) ;
for f in all_files {
match f . get_original_path ( ) {
Some ( path ) = > {
2023-03-25 17:06:01 +01:00
if ! f_map . contains_key ( replace_func ( path . clone ( ) , replacements ) . as_str ( ) )
& & ! component_path . as_ref ( ) . join ( path ) . exists ( )
{
removed_files . push ( f )
2021-04-24 23:19:25 -03:00
}
2022-09-01 19:23:09 -03:00
}
2021-04-24 23:19:25 -03:00
None = > {
2022-09-01 19:23:09 -03:00
if ! f_map . contains_key ( replace_func ( f . path . clone ( ) , replacements ) . as_str ( ) ) {
2021-04-24 23:19:25 -03:00
removed_files . push ( f )
}
}
}
}
Ok ( removed_files )
2021-04-19 09:35:05 -03:00
}
2021-04-24 23:19:25 -03:00
// Show all files missing in the manifests that are in sample_manifest
2022-09-01 19:23:09 -03:00
fn find_files_missing_in_manifests (
sample_manifest : & Manifest ,
manifests : Vec < Manifest > ,
replacements : & Option < HashMap < String , String > > ,
) -> Result < Vec < File > > {
2023-03-25 17:06:01 +01:00
let all_files : Vec < File > = manifests . iter ( ) . flat_map ( | m | m . files . clone ( ) ) . collect ( ) ;
2021-04-24 23:19:25 -03:00
let f_map = make_file_map ( all_files ) ;
2021-04-19 09:35:05 -03:00
2021-04-24 23:19:25 -03:00
let mut missing_files : Vec < File > = Vec ::new ( ) ;
2021-04-19 09:35:05 -03:00
2021-04-24 23:19:25 -03:00
for f in sample_manifest . files . clone ( ) {
match f . get_original_path ( ) {
Some ( path ) = > {
2022-09-01 19:23:09 -03:00
if ! f_map . contains_key ( replace_func ( path , replacements ) . as_str ( ) ) {
2021-04-24 23:19:25 -03:00
missing_files . push ( f )
}
2022-09-01 19:23:09 -03:00
}
2021-04-24 23:19:25 -03:00
None = > {
2022-09-01 19:23:09 -03:00
if ! f_map . contains_key ( replace_func ( f . path . clone ( ) , replacements ) . as_str ( ) ) {
2021-04-24 23:19:25 -03:00
missing_files . push ( f )
}
}
}
}
Ok ( missing_files )
}
2021-04-19 09:35:05 -03:00
2022-09-01 19:23:09 -03:00
fn replace_func ( orig : String , replacements : & Option < HashMap < String , String > > ) -> String {
if let Some ( replacements ) = replacements {
let mut replacement = orig . clone ( ) ;
2023-03-25 17:06:01 +01:00
for ( i , ( from , to ) ) in replacements . iter ( ) . enumerate ( ) {
2022-09-01 19:23:09 -03:00
let from : & str = & format! ( " $( {} ) " , from ) ;
if i = = 0 {
replacement = orig . replace ( from , to ) ;
} else {
replacement = replacement . replace ( from , to ) ;
}
2021-04-24 23:19:25 -03:00
}
2022-09-01 19:23:09 -03:00
replacement
} else {
orig
}
}
fn make_file_map ( files : Vec < File > ) -> HashMap < String , File > {
files
. iter ( )
. map ( | f | {
let orig_path_opt = f . get_original_path ( ) ;
2023-03-25 17:06:01 +01:00
if orig_path_opt . is_none ( ) {
2022-09-01 19:23:09 -03:00
return ( f . path . clone ( ) , f . clone ( ) ) ;
}
( orig_path_opt . unwrap ( ) , f . clone ( ) )
} )
. collect ( )
2021-03-21 14:12:03 -03:00
}
2025-07-21 23:20:19 +02:00
/// Publish a package to a repository
///
/// This function:
/// 1. Opens the repository at the specified path
/// 2. Parses the manifest file
/// 3. Uses the FileBackend's publish_files method to publish the files from the prototype directory
fn publish_package (
manifest_path : & PathBuf ,
prototype_dir : & PathBuf ,
repo_path : & PathBuf ,
publisher : & Option < String > ,
) -> Result < ( ) > {
// Check if the manifest file exists
if ! manifest_path . exists ( ) {
return Err ( anyhow! ( " Manifest file does not exist: {} " , manifest_path . display ( ) ) ) ;
}
// Check if the prototype directory exists
if ! prototype_dir . exists ( ) {
return Err ( anyhow! ( " Prototype directory does not exist: {} " , prototype_dir . display ( ) ) ) ;
}
// Parse the manifest file
println! ( " Parsing manifest file: {} " , manifest_path . display ( ) ) ;
let manifest = Manifest ::parse_file ( manifest_path ) ? ;
// Open the repository
println! ( " Opening repository at: {} " , repo_path . display ( ) ) ;
let repo = match FileBackend ::open ( repo_path ) {
Ok ( repo ) = > repo ,
Err ( _ ) = > {
println! ( " Repository does not exist, creating a new one... " ) ;
// Create a new repository with version 4
FileBackend ::create ( repo_path , libips ::repository ::RepositoryVersion ::V4 ) ?
}
} ;
// Determine which publisher to use
let publisher_name = if let Some ( pub_name ) = publisher {
// Use the explicitly specified publisher
if ! repo . config . publishers . contains ( pub_name ) {
return Err ( anyhow! ( " Publisher '{}' does not exist in the repository. Please add it first using pkg6repo add-publisher. " , pub_name ) ) ;
}
pub_name . clone ( )
} else {
// Use the default publisher
match & repo . config . default_publisher {
Some ( default_pub ) = > default_pub . clone ( ) ,
None = > return Err ( anyhow! ( " No default publisher set in the repository. Please specify a publisher using the --publisher option or set a default publisher. " ) )
}
} ;
// Begin a transaction
println! ( " Beginning transaction for publisher: {} " , publisher_name ) ;
let mut transaction = repo . begin_transaction ( ) ? ;
// Add files from the prototype directory to the transaction
println! ( " Adding files from prototype directory: {} " , prototype_dir . display ( ) ) ;
for file_action in manifest . files . iter ( ) {
// Construct the full path to the file in the prototype directory
let file_path = prototype_dir . join ( & file_action . path ) ;
// Check if the file exists
if ! file_path . exists ( ) {
println! ( " Warning: File does not exist in prototype directory: {} " , file_path . display ( ) ) ;
continue ;
}
// Add the file to the transaction
println! ( " Adding file: {} " , file_action . path ) ;
transaction . add_file ( file_action . clone ( ) , & file_path ) ? ;
}
// Update the manifest in the transaction
println! ( " Updating manifest in the transaction... " ) ;
transaction . update_manifest ( manifest ) ;
// Commit the transaction
println! ( " Committing transaction... " ) ;
transaction . commit ( ) ? ;
println! ( " Package published successfully! " ) ;
Ok ( ( ) )
}