mod error; use clap::{Parser, Subcommand}; use libips::actions::{ActionError, File, Manifest}; use libips::repository::{FileBackend, ReadableRepository, WritableRepository}; use error::{Pkg6DevError, Result}; use std::collections::HashMap; use std::fs::{OpenOptions, read_dir}; use std::io::Write; use std::path::{Path, PathBuf}; use tracing::{debug, info, warn}; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::{EnvFilter, fmt}; use userland::repology::find_newest_version; use userland::{Component, Makefile}; #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] #[clap(propagate_version = true)] struct App { #[clap(subcommand)] command: Commands, } #[derive(Subcommand, Debug)] enum Commands { DiffComponent { component: String, #[clap(short)] replacements: Option>, /// Place the file actions missing in the manifests but present in sample-manifest into this file #[clap(short = 'm')] output_manifest: Option, }, ShowComponent { component: String, }, /// 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, }, } fn main() -> Result<()> { // Initialize the tracing subscriber with the default log level as debug and no decorations // Parse the environment filter first, handling any errors with our custom error type let env_filter = EnvFilter::builder() .with_default_directive(LevelFilter::WARN.into()) .from_env() .map_err(|e| { Pkg6DevError::LoggingEnvError(format!("Failed to parse environment filter: {}", e)) })?; fmt::Subscriber::builder() .with_max_level(tracing::Level::DEBUG) .with_env_filter(env_filter) .without_time() .with_target(false) .with_ansi(false) .with_writer(std::io::stderr) .init(); let cli = App::parse(); match &cli.command { Commands::ShowComponent { component } => show_component_info(component), Commands::DiffComponent { component, replacements, output_manifest, } => diff_component(component, replacements, output_manifest), Commands::Publish { manifest_path, prototype_dir, repo_path, publisher, } => publish_package(manifest_path, prototype_dir, repo_path, publisher), } } fn parse_triplet_replacements(replacements: &[String]) -> HashMap { let mut map = HashMap::new(); for pair in replacements .iter() .map(|str| { str.split_once(':') .map(|s| (s.0.to_owned(), s.1.to_owned())) .unwrap_or((String::new(), String::new())) }) .collect::>() { map.insert(pair.0, pair.1); } map } fn diff_component( component_path: impl AsRef, replacements: &Option>, output_manifest: &Option, ) -> Result<()> { // Validate component path let component_path_ref = component_path.as_ref(); if !component_path_ref.exists() || !component_path_ref.is_dir() { return Err(Pkg6DevError::ComponentPathError { path: component_path_ref.to_path_buf(), }); } // Process replacements let replacements = if let Some(replacements) = replacements { // Validate replacement format for replacement in replacements { if !replacement.contains(':') { return Err(Pkg6DevError::ReplacementFormatError { value: replacement.clone(), }); } } let map = parse_triplet_replacements(replacements); Some(map) } else { None }; // Read directory contents let files = read_dir(&component_path).map_err(|e| Pkg6DevError::IoError(e))?; // Filter for manifest files let manifest_files: Vec = files .filter_map(std::result::Result::ok) .filter(|d| { if let Some(e) = d.path().extension() { e == "p5m" } else { false } }) .map(|e| e.path().into_os_string().into_string().unwrap()) .collect(); // Check for the sample manifest let sample_manifest_file = &component_path_ref.join("manifests/sample-manifest.p5m"); if !sample_manifest_file.exists() { return Err(Pkg6DevError::ManifestNotFoundError { path: sample_manifest_file.to_path_buf(), }); } // Parse manifests // Use std::result::Result here to avoid confusion with our custom Result type let manifests_res: std::result::Result, ActionError> = manifest_files.iter().map(Manifest::parse_file).collect(); // Parse sample manifest let sample_manifest = Manifest::parse_file(sample_manifest_file)?; // Unwrap manifests result let manifests: Vec = manifests_res?; // Find missing files let missing_files = find_files_missing_in_manifests(&sample_manifest, manifests.clone(), &replacements)?; // Print missing files for f in missing_files.clone() { debug!("file {} is missing in the manifests", f.path); } // Find removed files let removed_files = find_removed_files(&sample_manifest, manifests, &component_path, &replacements)?; // Print removed files for f in removed_files { debug!( "file path={} has been removed from the sample-manifest", f.path ); } // Write output manifest if requested if let Some(output_manifest) = output_manifest { let mut f = OpenOptions::new() .write(true) .truncate(true) .create(true) .open(output_manifest) .map_err(|e| Pkg6DevError::IoError(e))?; for action in missing_files { writeln!(&mut f, "file path={}", action.path).map_err(|e| Pkg6DevError::IoError(e))?; } } Ok(()) } fn show_component_info>(component_path: P) -> Result<()> { // Validate component path let component_path_ref = component_path.as_ref(); if !component_path_ref.exists() || !component_path_ref.is_dir() { return Err(Pkg6DevError::ComponentPathError { path: component_path_ref.to_path_buf(), }); } // Get a Makefile path let makefile_path = component_path_ref.join("Makefile"); if !makefile_path.exists() { return Err(Pkg6DevError::MakefileParseError { message: format!("Makefile not found at {}", makefile_path.display()), }); } // Parse Makefile // We'll wrap the anyhow errors with our more specific error types let initial_makefile = Makefile::parse_single_file(&makefile_path).map_err(|e| { Pkg6DevError::MakefileParseError { message: format!("Failed to parse Makefile: {}", e), } })?; let makefile = initial_makefile .parse_all() .map_err(|e| Pkg6DevError::MakefileParseError { message: format!("Failed to parse all Makefiles: {}", e), })?; let mut name = String::new(); // Get component information let component = Component::new_from_makefile(&makefile).map_err(|e| Pkg6DevError::ComponentInfoError { message: format!("Failed to get component information: {}", e), })?; // Display component information if let Some(var) = makefile.get("COMPONENT_NAME") { info!("Name: {}", var.replace('\n', "\n\t")); if let Some(component_name) = makefile.get_first_value_of_variable_by_name("COMPONENT_NAME") { name = component_name; } } if let Some(var) = makefile.get("COMPONENT_VERSION") { info!("Version: {}", var.replace('\n', "\n\t")); let latest_version = find_newest_version(&name); if latest_version.is_ok() { info!( "Latest Version: {}", latest_version.map_err(|e| Pkg6DevError::ComponentInfoError { message: format!("Failed to get latest version: {}", e), })? ); } else { warn!( "Could not get latest version info: {}", latest_version.unwrap_err() ) } } if let Some(var) = makefile.get("BUILD_BITS") { info!("Build bits: {}", var.replace('\n', "\n\t")); } if let Some(var) = makefile.get("COMPONENT_BUILD_ACTION") { info!("Build action: {}", var.replace('\n', "\n\t")); } if let Some(var) = makefile.get("COMPONENT_PROJECT_URL") { info!("Project URl: {}", var.replace('\n', "\n\t")); } if let Some(var) = makefile.get("COMPONENT_ARCHIVE_URL") { info!("Source URl: {}", var.replace('\n', "\n\t")); } if let Some(var) = makefile.get("COMPONENT_ARCHIVE_HASH") { info!("Source Archive File Hash: {}", var.replace('\n', "\n\t")); } if let Some(var) = makefile.get("REQUIRED_PACKAGES") { info!("Dependencies:\n\t{}", var.replace('\n', "\n\t")); } if let Some(var) = makefile.get("COMPONENT_INSTALL_ACTION") { info!("Install Action:\n\t{}", var); } info!("Component: {:?}", component); Ok(()) } // Show all files that have been removed in the sample-manifest fn find_removed_files>( sample_manifest: &Manifest, manifests: Vec, component_path: P, replacements: &Option>, ) -> Result> { let f_map = make_file_map(sample_manifest.files.clone()); let all_files: Vec = manifests.iter().flat_map(|m| m.files.clone()).collect(); let mut removed_files: Vec = Vec::new(); for f in all_files { match f.get_original_path() { Some(path) => { if !f_map.contains_key(replace_func(path.clone(), replacements).as_str()) && !component_path.as_ref().join(path).exists() { removed_files.push(f) } } None => { if !f_map.contains_key(replace_func(f.path.clone(), replacements).as_str()) { removed_files.push(f) } } } } Ok(removed_files) } // Show all files missing in the manifests that are in sample_manifest fn find_files_missing_in_manifests( sample_manifest: &Manifest, manifests: Vec, replacements: &Option>, ) -> Result> { let all_files: Vec = manifests.iter().flat_map(|m| m.files.clone()).collect(); let f_map = make_file_map(all_files); let mut missing_files: Vec = Vec::new(); for f in sample_manifest.files.clone() { match f.get_original_path() { Some(path) => { if !f_map.contains_key(replace_func(path, replacements).as_str()) { missing_files.push(f) } } None => { if !f_map.contains_key(replace_func(f.path.clone(), replacements).as_str()) { missing_files.push(f) } } } } Ok(missing_files) } fn replace_func(orig: String, replacements: &Option>) -> String { if let Some(replacements) = replacements { let mut replacement = orig.clone(); for (i, (from, to)) in replacements.iter().enumerate() { let from: &str = &format!("$({})", from); if i == 0 { replacement = orig.replace(from, to); } else { replacement = replacement.replace(from, to); } } replacement } else { orig } } fn make_file_map(files: Vec) -> HashMap { files .iter() .map(|f| { let orig_path_opt = f.get_original_path(); if orig_path_opt.is_none() { return (f.path.clone(), f.clone()); } (orig_path_opt.unwrap(), f.clone()) }) .collect() } /// Publish a package to a repository /// /// This function: /// 1. Opens the repository at the specified path /// 2. Parses 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, ) -> Result<()> { // Check if the manifest file exists if !manifest_path.exists() { return Err(Pkg6DevError::ManifestFileNotFoundError { path: manifest_path.clone(), }); } // Check if the prototype directory exists if !prototype_dir.exists() { return Err(Pkg6DevError::PrototypeDirNotFoundError { path: prototype_dir.clone(), }); } // Parse the manifest file info!("Parsing manifest file: {}", manifest_path.display()); let manifest = Manifest::parse_file(manifest_path)?; // Open the repository info!("Opening repository at: {}", repo_path.display()); let repo = match FileBackend::open(repo_path) { Ok(repo) => repo, Err(_) => { info!("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(Pkg6DevError::PublisherNotFoundError { publisher: pub_name.clone(), }); } pub_name.clone() } else { // Use the default publisher match &repo.config.default_publisher { Some(default_pub) => default_pub.clone(), None => return Err(Pkg6DevError::NoDefaultPublisherError), } }; // Begin a transaction info!("Beginning transaction for publisher: {}", publisher_name); let mut transaction = repo.begin_transaction()?; // Set the publisher for the transaction transaction.set_publisher(&publisher_name); // Add files from the prototype directory to the transaction info!( "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() { return Err(Pkg6DevError::FileNotFoundInPrototypeError { path: file_path.clone(), }); } // Add the file to the transaction debug!("Adding file: {}", file_action.path); transaction.add_file(file_action.clone(), &file_path)?; } // Update the manifest in the transaction info!("Updating manifest in the transaction..."); transaction.update_manifest(manifest); // Commit the transaction info!("Committing transaction..."); transaction.commit()?; // Regenerate catalog and search index info!("Regenerating catalog and search index..."); repo.rebuild(Some(&publisher_name), false, false)?; info!("Package published successfully!"); Ok(()) }