diff --git a/Cargo.toml b/Cargo.toml index fc915693d..cc00b8bf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ default-members = ["moss"] resolver = "2" [workspace.package] -version = "0.25.6" +version = "0.25.7" edition = "2024" rust-version = "1.85" diff --git a/boulder/src/build.rs b/boulder/src/build.rs index 94c90cdfe..bcaaa4149 100644 --- a/boulder/src/build.rs +++ b/boulder/src/build.rs @@ -462,4 +462,6 @@ pub enum Error { RecreateArtefactsDir(#[source] io::Error), #[error("git upstream processing")] Git(#[from] git::GitError), + #[error("invalid repo in mv-to-repo flag")] + InvalidRepo, } diff --git a/boulder/src/cli.rs b/boulder/src/cli.rs index 7b1a68833..6c1785cac 100644 --- a/boulder/src/cli.rs +++ b/boulder/src/cli.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright © 2020-2025 Serpent OS Developers // // SPDX-License-Identifier: MPL-2.0 -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; use boulder::{Env, env}; use clap::{Args, CommandFactory, Parser}; @@ -12,6 +12,7 @@ use clap_complete::{ use clap_mangen::Man; use fs_err::{self as fs, File}; use thiserror::Error; +use tui::Styled; mod build; mod chroot; @@ -51,6 +52,21 @@ pub struct Global { pub generate_manpages: Option, #[arg(long, global = true, hide = true)] pub generate_completions: Option, + #[arg( + long, + require_equals = true, + global = true, + help = "Move newly built .stone package files to the given repo" + )] + pub mv_to_repo: Option, + #[arg( + long, + default_value_t = false, + requires = "mv-to-repo", + global = true, + help = "Auto re-index the repo after a successful build and move" + )] + pub re_index: bool, } #[derive(Debug, clap::Subcommand)] @@ -106,6 +122,35 @@ pub fn process() -> Result<(), Error> { return Ok(()); } + match subcommand { + Some(Subcommand::Build(_)) | Some(Subcommand::Recipe(_)) => { /* do nothing, the flags were passed in appropriately. */ + } + _ => match (global.mv_to_repo.clone(), global.re_index) { + (Some(_), false) => { + eprintln!( + "{}: The `--mv-to-repo` flag cannot be used with anything but the `build` or `recipe` subcommands", + "Error".red() + ); + std::process::exit(1); + } + (None, true) => { + eprintln!( + "{}: The `--re-index` cannot be used with anything but the `build` or `recipe` subcommands and requires `--mv-to-repo`", + "Error".red() + ); + std::process::exit(1); + } + (Some(_), true) => { + eprintln!( + "{}: The ``--mv-to-repo` and ``--re-index` flags can only be used with the `build` or `recipe` subcommands", + "Error".red() + ); + std::process::exit(1); + } + (None, false) => { /* do nothing, the flags weren't passed in. */ } + }, + } + let env = Env::new(global.cache_dir, global.config_dir, global.data_dir, global.moss_root)?; if global.verbose { @@ -120,10 +165,166 @@ pub fn process() -> Result<(), Error> { } match subcommand { - Some(Subcommand::Build(command)) => build::handle(command, env)?, + Some(Subcommand::Build(command)) => { + match build::handle(command, env) { + Ok(_) => { + if let Some(repo) = global.mv_to_repo { + // Check to see if the repo is in moss + let moss_cmd = std::process::Command::new("moss") + .args(["repo", "list"]) + .stdout(std::process::Stdio::piped()) + .output() + .expect("Couldn't get a list of moss repos"); + + // Convert the output to a String + let repos = String::from_utf8(moss_cmd.stdout).expect("Could get the repo list from moss"); + + let mv_repo = repos + .lines() + .filter_map(|line| { + if line.contains(&repo) { + let mut ret_map = HashMap::new(); + let uri = line + .split_whitespace() + .filter(|line| line.contains("//")) + .last() + .and_then(|uri| { + if uri.contains("file:///") { + Some(uri.to_string().replace("file://", "")) + } else { + Some(uri.to_string()) + } + }) + .expect("Couldn't get URI from repo string".red().to_string().as_str()); + + let _ = ret_map.insert(&repo, Some(uri.clone())); + + Some(ret_map) + } else { + None + } + }) + .last() + .unwrap_or_else(|| HashMap::new()); + + // Check to ensure that the repo has a URI; + // return Err if there isn't. + if mv_repo.get(&repo).is_none() { + eprintln!("{} {}", &repo, "is not a valid repo registered with moss"); + return Err(Error::Build(build::Error::Build(boulder::build::Error::InvalidRepo))); + } + + // Move the newly built .stone files + match mv_to_repo(&repo, &mv_repo) { + Ok(repo) => { + if global.re_index && repo.is_some() { + if let Err(err) = re_index_repo(&repo.expect("Repo was supposed to be Some")) { + eprintln!("Error: {err}"); + return Err(err); + } + } else if global.re_index && repo.is_none() { + eprintln!("Error: Cannot re-index, returned repo name was empty!"); + return Err(Error::Reindex( + "Cannot re-index, move operation returned an invalid repo name".to_string(), + )); + } + } + Err(err) => { + eprintln!("Error: {err}"); + return Err(err); + } + } + } + } + Err(e) => { + eprintln!("{e}"); + return Err(Error::Build(e)); + } + }; + } Some(Subcommand::Chroot(command)) => chroot::handle(command, env)?, Some(Subcommand::Profile(command)) => profile::handle(command, env)?, - Some(Subcommand::Recipe(command)) => recipe::handle(command, env)?, + // Recipe takes into account the global.build flag + Some(Subcommand::Recipe(command)) => { + // Give an error message and exit without running the command + // if the --mv-to-repo flag was give without the --build flag. + if global.mv_to_repo.is_some() && !command.build { + eprintln!("Error: Cannot use `--mv-to-repo` without the `--build` flag"); + std::process::exit(1); + } + if let Some(repo) = global.mv_to_repo { + // Check to see if the repo is in moss + let moss_cmd = std::process::Command::new("moss") + .args(["repo", "list"]) + .output() + .expect("Couldn't get a list of moss repos"); + + let repos = String::from_utf8(moss_cmd.stdout).expect("Could not get the repo list from moss"); + + let mv_repo = repos + .lines() + .filter_map(|line| { + if line.contains(&repo) { + let mut ret_map = HashMap::new(); + + let uri = line + .split_whitespace() + .filter(|line| line.contains("//")) + .last() + .and_then(|uri| { + if uri.contains("file:///") { + Some(uri.to_string().replace("file://", "")) + } else { + Some(uri.to_string()) + } + }) + .expect("Could not get URI from repo string"); + + let _ = ret_map.insert(&repo, Some(uri.clone())); + + Some(ret_map) + } else { + None + } + }) + .last() + .unwrap_or_else(|| HashMap::new()); + + if mv_repo.get(&repo).is_none() { + eprintln!("{} is not a valid repo registered with moss", &repo); + return Err(Error::Build(build::Error::Build(boulder::build::Error::InvalidRepo))); + } + + recipe::handle(command, env)?; + + match mv_to_repo(&repo, &mv_repo) { + Ok(repo) => { + // Ok to re-index as there is a value use + if global.re_index && repo.is_some() { + if let Err(err) = + re_index_repo(&repo.clone().expect("Error: Returned repo should've been Some")) + { + eprintln!("Error {err}"); + return Err(Error::Reindex( + "Cannot re-index, move operation returned an invalid repo name".to_string(), + )); + } + } else if global.re_index && repo.is_none() { + eprintln!("Error: Cannot re-index, returned repo name was empty!"); + return Err(Error::Reindex( + "Cannot re-index, move operation returned an invalid repo name".to_string(), + )); + } + } + Err(err) => { + eprintln!("Error: {err}"); + return Err(err); + } + } + } else { + recipe::handle(command, env)?; + } + } Some(Subcommand::Version(command)) => version::handle(command), None => (), } @@ -160,6 +361,97 @@ fn replace_aliases(args: std::env::Args) -> Vec { args } +fn mv_to_repo(repo_key: &String, repo_map: &HashMap<&String, Option>) -> Result, Error> { + let repo_path = repo_map.get(repo_key).unwrap_or_else(|| &None); + if let Some(repo_path) = repo_path { + let cwd = PathBuf::from("."); + let manifest_ext = "stone"; + + let repo_path = PathBuf::from(if repo_path.contains("file://") { + repo_path.replacen("file://", "", 1).replacen("stone.index", "", 1) + } else { + repo_path.to_string().replacen("stone.index", "", 1) + }); + + // Create repo directory if it doesn't exist + if !repo_path.exists() { + fs::create_dir_all(&repo_path).expect("Failed to create {repo_key} repo directories"); + } + + match fs::read_dir(&cwd) { + Ok(dir) => { + for (_, pkg_file) in dir.enumerate() { + let pkg_file = pkg_file.expect("Failed to get package file to move"); + let path = pkg_file.path(); + + if path.is_file() + && let Some(ext) = path.extension().and_then(|ext| ext.to_str()) + { + if ext == manifest_ext { + let file_name = path + .file_name() + .ok_or("Invalid package file name") + .expect("Failed to get package file name"); + let dest_path = PathBuf::from(&repo_path).join(file_name); + + println!( + "Moving {} to {}", + &path.to_string_lossy().to_string(), + &dest_path.to_string_lossy().to_string() + ); + match fs::rename(&path, &dest_path) { + Ok(_) => { + println!( + "Successfully moved {} to {}", + &path.to_string_lossy().to_string(), + &dest_path.to_string_lossy().to_string() + ); + } + Err(e) => { + eprintln!( + "Failed to move {} to {}: {e}", + &path.to_string_lossy().to_string(), + &dest_path.to_string_lossy().to_string(), + ); + } + } + } + } + } + + return Ok(Some(repo_path.to_string_lossy().to_string())); + } + Err(e) => { + eprintln!("Failed to read directory: {e}"); + return Err(Error::Io(e)); + } + } + } + + if let Some(repo_path) = repo_path { + Ok(Some(repo_path.clone())) + } else { + Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Error: {repo_key} doesn't have a valid path").as_str(), + ))) + } +} + +fn re_index_repo(repo: &str) -> Result<(), Error> { + use std::process::{Command as Cmd, Stdio}; + + let mut moss_cmd = Cmd::new("moss") + .args(["index", repo]) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn()?; + + let _index_status = moss_cmd.wait()?; + + Ok(()) +} + #[derive(Debug, Error)] pub enum Error { #[error("build")] @@ -174,4 +466,6 @@ pub enum Error { Recipe(#[from] recipe::Error), #[error("io error")] Io(#[from] std::io::Error), + #[error("reindex")] + Reindex(String), } diff --git a/boulder/src/cli/recipe.rs b/boulder/src/cli/recipe.rs index 587433555..b3d1b853a 100644 --- a/boulder/src/cli/recipe.rs +++ b/boulder/src/cli/recipe.rs @@ -29,6 +29,14 @@ use url::Url; #[derive(Debug, Parser)] #[command(about = "Utilities to create and manipulate stone recipe files")] pub struct Command { + #[arg( + long, + required = false, + default_value_t = false, + global = true, + help = "Build the recipe after successful completion of the subcommand" + )] + pub(crate) build: bool, #[command(subcommand)] subcommand: Subcommand, } @@ -105,7 +113,7 @@ fn parse_upstream(s: &str) -> Result { } pub fn handle(command: Command, env: Env) -> Result<(), Error> { - match command.subcommand { + let run_cmd = match command.subcommand { Subcommand::Bump { recipe, release } => bump(recipe, release), Subcommand::New { output, upstreams } => new(output, upstreams, env), Subcommand::Update { @@ -116,7 +124,34 @@ pub fn handle(command: Command, env: Env) -> Result<(), Error> { no_bump, } => update(recipe, overwrite, version, upstreams, no_bump), Subcommand::Macros { _macro } => macros(_macro, env), + }; + + if command.build + && let Ok(_) = run_cmd + { + use std::process::{Command as Cmd, Stdio}; + + let mut boulder_build = Cmd::new("boulder") + .args(["build", "-u", "stone.yaml"]) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .expect("Failed to run boulder build command!"); + + let status = boulder_build + .wait() + .expect(&format!("{}", "boulder build command failed to wait to complete".red())); + + if !status.success() { + let err_str = "Failed to build package".red().to_string(); + return Err(Error::BuildErr(err_str)); + } else { + let success_msg = "Successfully updated and built package!".green().to_string(); + println!("{success_msg}"); + } } + + run_cmd } fn bump(recipe: PathBuf, release: Option) -> Result<(), Error> { @@ -162,7 +197,7 @@ fn new(output: PathBuf, upstreams: Vec, env: Env) -> Result<(), Error> { fs::write(PathBuf::from(&output).join(RECIPE_FILE), draft.stone).map_err(Error::Write)?; fs::write(PathBuf::from(&output).join(MONITORING_FILE), draft.monitoring).map_err(Error::Write)?; - println!("Saved {RECIPE_FILE} & {MONITORING_FILE} to {output:?}"); + println!("{}", "Saved {RECIPE_FILE} & {MONITORING_FILE} to {output:?}".green()); Ok(()) } @@ -279,9 +314,9 @@ fn update( if overwrite { let recipe = recipe.expect("checked above"); fs::write(&recipe, updated.as_bytes()).map_err(Error::Write)?; - println!("{} updated", recipe.display()); + println!("{} {}", recipe.display().to_string().green(), "updated".green()); } else { - print!("{updated}"); + print!("{}", updated.green()); } Ok(()) @@ -433,4 +468,6 @@ pub enum Error { Utf8(#[from] std::string::FromUtf8Error), #[error("draft")] Draft(#[from] draft::Error), + #[error("Recipe auto-build error")] + BuildErr(String), }