diff --git a/Cargo.toml b/Cargo.toml index 7030c0e..567b29e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,14 +11,12 @@ repository = "https://github.com/cat-in-136/cargo-generate-rpm" version = "0.11.1" edition = "2018" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] glob = "0.3.0" -rpm = { version = "0.11.0", default-features = false } +rpm = { version = "0.12", default-features = false } toml = "0.7" cargo_toml = "0.15" -getopts = "0.2" +clap = { version = "4.3", features = ["derive"] } thiserror = "1" elf = "0.7" diff --git a/src/auto_req/builtin.rs b/src/auto_req/builtin.rs index c1ab54a..92781dd 100644 --- a/src/auto_req/builtin.rs +++ b/src/auto_req/builtin.rs @@ -77,17 +77,17 @@ fn find_requires_by_ldd( .map_err(|e| AutoReqError::ProcessError(OsString::from("ldd"), e))?; let unversioned_libraries = s - .split("\n") + .split('\n') .take_while(|&line| !line.trim().is_empty()) - .filter_map(|line| line.trim_start().splitn(2, " ").nth(0)); + .filter_map(|line| line.trim_start().split(' ').next()); let versioned_libraries = s - .split("\n") + .split('\n') .skip_while(|&line| !line.contains("Version information:")) .skip(1) .skip_while(|&line| !line.contains(path.to_str().unwrap())) .skip(1) .take_while(|&line| line.contains(" => ")) - .filter_map(|line| line.trim_start().splitn(2, " => ").nth(0)); + .filter_map(|line| line.trim_start().split(" => ").next()); let marker = marker.unwrap_or_default(); let mut requires = BTreeSet::new(); @@ -98,22 +98,18 @@ fn find_requires_by_ldd( { if name.contains(" (") { // Insert "unversioned" library name - requires.insert(format!( - "{}(){}", - name.splitn(2, " ").nth(0).unwrap(), - marker - )); - requires.insert(format!("{}{}", name.replace(" ", ""), marker)); + requires.insert(format!("{}(){}", name.split(' ').next().unwrap(), marker)); + requires.insert(format!("{}{}", name.replace(' ', ""), marker)); } else { - requires.insert(format!("{}(){}", name.replace(" ", ""), marker)); + requires.insert(format!("{}(){}", name.replace(' ', ""), marker)); } } Ok(requires) } fn find_requires_of_elf(path: &Path) -> Result>, AutoReqError> { - if let Ok(info) = ElfInfo::new(&path) { - let mut requires = find_requires_by_ldd(&path, info.marker())?; + if let Ok(info) = ElfInfo::new(path) { + let mut requires = find_requires_by_ldd(path, info.marker())?; if info.got_gnu_hash && !info.got_hash { requires.insert("rtld(GNU_HASH)".to_string()); } @@ -142,16 +138,16 @@ fn find_require_of_shebang(path: &Path) -> Result, AutoReqError> let mut line = String::new(); read.read_line(&mut line)?; line.trim() - .splitn(2, |c: char| !c.is_ascii() || c.is_whitespace()) - .nth(0) - .map(&String::from) + .split(|c: char| !c.is_ascii() || c.is_whitespace()) + .next() + .map(String::from) } else { None } }; Ok(match interpreter { - Some(i) if Path::new(&i).exists() => Some(i.to_string()), + Some(i) if Path::new(&i).exists() => Some(i), _ => None, }) } @@ -172,8 +168,8 @@ fn test_find_require_of_shebang() { fn is_executable(path: &Path) -> bool { use std::os::unix::fs::MetadataExt; std::fs::metadata(path) - .and_then(|metadata| Ok(metadata.mode())) - .and_then(|mode| Ok(mode & 0o111 != 0)) + .map(|metadata| metadata.mode()) + .map(|mode| mode & 0o111 != 0) .unwrap_or_default() } diff --git a/src/auto_req/mod.rs b/src/auto_req/mod.rs index abfb116..ca7bfb4 100644 --- a/src/auto_req/mod.rs +++ b/src/auto_req/mod.rs @@ -1,5 +1,4 @@ -use crate::error::AutoReqError; -use std::convert::TryFrom; +use crate::{cli, error::AutoReqError}; use std::path::{Path, PathBuf}; mod builtin; @@ -20,46 +19,21 @@ pub enum AutoReqMode { BuiltIn, } -impl TryFrom for AutoReqMode { - type Error = AutoReqError; - - fn try_from(value: String) -> Result { - match value.as_str() { - "auto" | "" => Ok(AutoReqMode::Auto), - "no" | "disabled" => Ok(AutoReqMode::Disabled), - "builtin" => Ok(AutoReqMode::BuiltIn), - "find-requires" => Ok(AutoReqMode::Script(PathBuf::from(RPM_FIND_REQUIRES))), - v if Path::new(v).exists() => Ok(AutoReqMode::Script(PathBuf::from(v))), - _ => Err(AutoReqError::WrongMode), +impl From<&Option> for AutoReqMode { + fn from(value: &Option) -> Self { + use cli::AutoReqMode as M; + use AutoReqMode::*; + + match value { + None => Auto, + Some(M::Disabled) => Disabled, + Some(M::Builtin) => BuiltIn, + Some(M::Script(path)) => Script(path.into()), + Some(M::FindRequires) => Script(PathBuf::from(RPM_FIND_REQUIRES)), } } } -#[test] -pub fn test_try_from_for_auto_req_mode() { - for (text, auto_req_mode) in &[ - ("auto", AutoReqMode::Auto), - ("", AutoReqMode::Auto), - ("no", AutoReqMode::Disabled), - ("disabled", AutoReqMode::Disabled), - ( - "find-requires", - AutoReqMode::Script(PathBuf::from(RPM_FIND_REQUIRES)), - ), - (file!(), AutoReqMode::Script(PathBuf::from(file!()))), - ] { - assert_eq!( - AutoReqMode::try_from(text.to_string()).unwrap(), - *auto_req_mode - ); - } - - assert!(matches!( - AutoReqMode::try_from("invalid-value".to_string()), - Err(AutoReqError::WrongMode) - )); -} - /// Find requires pub fn find_requires, P: AsRef>( files: T, diff --git a/src/auto_req/script.rs b/src/auto_req/script.rs index 3c88036..9138b31 100644 --- a/src/auto_req/script.rs +++ b/src/auto_req/script.rs @@ -33,7 +33,7 @@ pub(super) fn find_requires, S: AsRef>( .read_to_string(&mut requires) .map_err(|e| AutoReqError::ProcessError(script_path.as_ref().to_os_string(), e))?; - Ok(requires.trim().split("\n").map(&String::from).collect()) + Ok(requires.trim().split('\n').map(&String::from).collect()) } #[test] diff --git a/src/build_target.rs b/src/build_target.rs index bde7c4e..4ce9c98 100644 --- a/src/build_target.rs +++ b/src/build_target.rs @@ -1,15 +1,30 @@ use std::env::consts::ARCH; use std::path::{Path, PathBuf}; -#[derive(Debug, Default)] +use crate::cli::Cli; + +#[derive(Debug, Clone)] pub struct BuildTarget { - pub target_dir: Option, - pub target: Option, - pub profile: Option, - pub arch: Option, + target_dir: Option, + target: Option, + profile: String, + arch: Option, } impl BuildTarget { + pub fn new(args: &Cli) -> Self { + Self { + target_dir: args.target_dir.clone(), + target: args.target.clone(), + profile: args.profile.clone(), + arch: args.arch.clone(), + } + } + + pub fn profile(&self) -> &str { + self.profile.as_str() + } + pub fn build_target_path(&self) -> PathBuf { if let Some(target_dir) = &self.target_dir { PathBuf::from(&target_dir) @@ -36,8 +51,8 @@ impl BuildTarget { let arch = self .target .as_ref() - .and_then(|v| v.splitn(2, "-").nth(0)) - .unwrap_or_else(|| ARCH); + .and_then(|v| v.split('-').next()) + .unwrap_or(ARCH); match arch { "x86" => "i586", @@ -57,12 +72,13 @@ mod test { #[test] fn test_build_target_path() { - let target = BuildTarget::default(); + let args = crate::cli::Cli::default(); + let target = BuildTarget::new(&args); assert_eq!(target.build_target_path(), PathBuf::from("target")); let target = BuildTarget { target_dir: Some("/tmp/foobar/target".to_string()), - ..Default::default() + ..target }; assert_eq!( target.build_target_path(), @@ -72,15 +88,16 @@ mod test { #[test] fn test_target_path() { - let target = BuildTarget::default(); + let args = crate::cli::Cli::default(); + let default_target = BuildTarget::new(&args); assert_eq!( - target.target_path("release"), + default_target.target_path("release"), PathBuf::from("target/release") ); let target = BuildTarget { target: Some("x86_64-unknown-linux-gnu".to_string()), - ..Default::default() + ..default_target.clone() }; assert_eq!( target.target_path("release"), @@ -89,7 +106,7 @@ mod test { let target = BuildTarget { target_dir: Some("/tmp/foobar/target".to_string()), - ..Default::default() + ..default_target.clone() }; assert_eq!( target.target_path("debug"), @@ -99,7 +116,7 @@ mod test { let target = BuildTarget { target_dir: Some("/tmp/foobar/target".to_string()), target: Some("x86_64-unknown-linux-gnu".to_string()), - ..Default::default() + ..default_target }; assert_eq!( target.target_path("debug"), diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..cb203c2 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,154 @@ +use clap::{builder::PossibleValue, Parser, ValueEnum}; +use std::path::PathBuf; + +/// Wrapper used when the application is executed as Cargo plugin +#[derive(Debug, Parser)] +#[command(name = "cargo")] +#[command(bin_name = "cargo")] +pub enum CargoWrapper { + GenerateRpm(Cli), +} + +/// Arguments of the command line interface +#[derive(Debug, Parser)] +#[command(name = "cargo-generate-rpm")] +#[command(bin_name = "cargo-generate-rpm")] +#[command(author, version, about, long_about = None)] +pub struct Cli { + /// Target arch of generated package. + #[arg(short, long)] + pub arch: Option, + + /// Output file or directory. + #[arg(short, long)] + pub output: Option, + + /// Name of a crate in the workspace for which + /// RPM package will be generated. + #[arg(short, long)] + pub package: Option, + + /// Automatic dependency processing mode. + #[arg(long)] + pub auto_req: Option, + + /// Sub-directory name for all generated artifacts. May be + /// specified with CARGO_BUILD_TARGET environment + /// variable. + #[arg(long)] + pub target: Option, + + /// Directory for all generated artifacts. May be + /// specified with CARGO_BUILD_TARGET_DIR or + /// CARGO_TARGET_DIR environment variables. + #[arg(long)] + pub target_dir: Option, + + /// Build profile for packaging. + #[arg(long, default_value = "release")] + pub profile: String, + + /// Compression type of package payload. + #[arg(long, default_value = "zstd")] + pub payload_compress: Compression, + + /// Timestamp in seconds since the UNIX Epoch for clamping + /// modification time of included files and package build time. + /// + /// This value can also be provided using the SOURCE_DATE_EPOCH + /// enviroment variable. + #[arg(long)] + pub source_date: Option, + + /// Overwrite metadata with TOML file. If "#dotted.key" + /// suffixed, load "dotted.key" table instead of the root + /// table. + #[arg(long, value_delimiter = ',')] + pub metadata_overwrite: Vec, + + /// Overwrite metadata with TOML text. + #[arg(short, long, value_delimiter = ',')] + pub set_metadata: Vec, + + /// Shortcut to --metadata-overwrite=path/to/Cargo.toml#package.metadata.generate-rpm.variants.VARIANT + #[arg(long, value_delimiter = ',')] + pub variant: Vec, +} + +impl Default for Cli { + fn default() -> Self { + Cli::parse_from([""]) + } +} + +#[derive(ValueEnum, Clone, Copy, Debug, Default)] +pub enum Compression { + None, + Gzip, + #[default] + Zstd, + Xz, +} + +impl From for rpm::CompressionWithLevel { + fn from(val: Compression) -> Self { + let ct = match val { + Compression::None => rpm::CompressionType::None, + Compression::Gzip => rpm::CompressionType::Gzip, + Compression::Zstd => rpm::CompressionType::Zstd, + Compression::Xz => rpm::CompressionType::Xz, + }; + ct.into() + } +} + +#[derive(Clone, Debug)] +pub enum AutoReqMode { + Disabled, + Builtin, + FindRequires, + Script(String), +} + +static AUTO_REQ_VARIANTS: &[AutoReqMode] = &[ + AutoReqMode::Disabled, + AutoReqMode::Builtin, + AutoReqMode::FindRequires, + AutoReqMode::Script(String::new()), +]; + +impl ValueEnum for AutoReqMode { + fn value_variants<'a>() -> &'a [Self] { + AUTO_REQ_VARIANTS + } + + fn to_possible_value(&self) -> Option { + use AutoReqMode::*; + + let val = match self { + Disabled => { + PossibleValue::new("disabled").help("Disable automatic discovery of dependencies") + } + Builtin => { + PossibleValue::new("builtin").help("Use the builtin procedure based on ldd.") + } + FindRequires => PossibleValue::new("find-requires") + .help("Use the external program specified in RPM_FIND_REQUIRES."), + _ => PossibleValue::new("/path/to/find-requires") + .help("Use the specified external program."), + }; + Some(val) + } + + // Provided method + fn from_str(input: &str, ignore_case: bool) -> Result { + let lowercase = String::from(input).to_lowercase(); + let val = if ignore_case { &lowercase } else { input }; + Ok(match val { + "disabled" => Self::Disabled, + "builtin" => Self::Builtin, + "find-requires" => Self::FindRequires, + _ => Self::Script(input.into()), + }) + } +} diff --git a/src/config/file_info.rs b/src/config/file_info.rs index 620da88..a62f4e8 100644 --- a/src/config/file_info.rs +++ b/src/config/file_info.rs @@ -1,5 +1,4 @@ use glob::glob; -use rpm::RPMFileOptions; use toml::value::Table; use crate::build_target::BuildTarget; @@ -53,7 +52,7 @@ impl FileInfo<'_, '_, '_, '_> { } else { None }; - let mode = Self::get_mode(&table, source, idx)?; + let mode = Self::get_mode(table, source, idx)?; let config = if let Some(is_config) = table.get("config") { is_config .as_bool() @@ -108,18 +107,17 @@ impl FileInfo<'_, '_, '_, '_> { parent: P, idx: usize, ) -> Result, ConfigError> { - let profile = match build_target.profile.as_deref() { - Some(v) if v == "dev" => "debug", - Some(v) => v, - None => "release", + let dir_name = match build_target.profile() { + "dev" => "debug", + p => p, }; let source = self .source .strip_prefix("target/release/") - .or_else(|| self.source.strip_prefix(&format!("target/{profile}/"))) + .or_else(|| self.source.strip_prefix(&format!("target/{dir_name}/"))) .and_then(|rel_path| { build_target - .target_path(profile) + .target_path(dir_name) .join(rel_path) .to_str() .map(|v| v.to_string()) @@ -127,13 +125,13 @@ impl FileInfo<'_, '_, '_, '_> { .unwrap_or(self.source.to_string()); let expanded = expand_glob(source.as_str(), self.dest, idx)?; - if expanded.len() > 0 { + if !expanded.is_empty() { return Ok(expanded); } if let Some(src) = parent.as_ref().join(&source).to_str() { let expanded = expand_glob(src, self.dest, idx)?; - if expanded.len() > 0 { + if !expanded.is_empty() { return Ok(expanded); } } @@ -141,8 +139,8 @@ impl FileInfo<'_, '_, '_, '_> { Err(ConfigError::AssetFileNotFound(PathBuf::from(source))) } - fn generate_rpm_file_options(&self, dest: T) -> RPMFileOptions { - let mut rpm_file_option = RPMFileOptions::new(dest.to_string()); + fn generate_rpm_file_options(&self, dest: T) -> rpm::FileOptions { + let mut rpm_file_option = rpm::FileOptions::new(dest.to_string()); if let Some(user) = self.user { rpm_file_option = rpm_file_option.user(user); } @@ -166,7 +164,7 @@ impl FileInfo<'_, '_, '_, '_> { build_target: &BuildTarget, parent: P, idx: usize, - ) -> Result, ConfigError> { + ) -> Result, ConfigError> { self.generate_expanded_path(build_target, parent, idx) .map(|p| { p.iter() @@ -201,8 +199,8 @@ fn expand_glob( ) -> Result, ConfigError> { let mut vec = Vec::new(); if source.contains('*') { - let base = get_base_from_glob(&source); - for path in glob(&source).map_err(|e| ConfigError::AssetGlobInvalid(idx, e.msg))? { + let base = get_base_from_glob(source); + for path in glob(source).map_err(|e| ConfigError::AssetGlobInvalid(idx, e.msg))? { let file = path.map_err(|_| ConfigError::AssetReadFailed(idx))?; if file.is_dir() { continue; @@ -285,8 +283,8 @@ mod test { files, vec![ FileInfo { - source: "target/release/cargo-generate-rpm".into(), - dest: "/usr/bin/cargo-generate-rpm".into(), + source: "target/release/cargo-generate-rpm", + dest: "/usr/bin/cargo-generate-rpm", user: None, group: None, mode: Some(0o0100755), @@ -294,8 +292,8 @@ mod test { doc: false, }, FileInfo { - source: "LICENSE".into(), - dest: "/usr/share/doc/cargo-generate-rpm/LICENSE".into(), + source: "LICENSE", + dest: "/usr/share/doc/cargo-generate-rpm/LICENSE", user: None, group: None, mode: Some(0o0100644), @@ -303,8 +301,8 @@ mod test { doc: true, }, FileInfo { - source: "README.md".into(), - dest: "/usr/share/doc/cargo-generate-rpm/README.md".into(), + source: "README.md", + dest: "/usr/share/doc/cargo-generate-rpm/README.md", user: None, group: None, mode: Some(0o0100644), @@ -318,10 +316,11 @@ mod test { #[test] fn test_generate_rpm_file_path() { let tempdir = tempfile::tempdir().unwrap(); - let target = BuildTarget::default(); + let args = crate::cli::Cli::default(); + let target = BuildTarget::new(&args); let file_info = FileInfo { - source: "README.md".into(), - dest: "/usr/share/doc/cargo-generate-rpm/README.md".into(), + source: "README.md", + dest: "/usr/share/doc/cargo-generate-rpm/README.md", user: None, group: None, mode: None, @@ -340,8 +339,8 @@ mod test { ); let file_info = FileInfo { - source: "not-exist-file".into(), - dest: "/usr/share/doc/cargo-generate-rpm/not-exist-file".into(), + source: "not-exist-file", + dest: "/usr/share/doc/cargo-generate-rpm/not-exist-file", user: None, group: None, mode: None, @@ -356,8 +355,8 @@ mod test { std::fs::create_dir_all(tempdir.path().join("target/release")).unwrap(); File::create(tempdir.path().join("target/release/foobar")).unwrap(); let file_info = FileInfo { - source: "target/release/foobar".into(), - dest: "/usr/bin/foobar".into(), + source: "target/release/foobar", + dest: "/usr/bin/foobar", user: None, group: None, mode: None, @@ -384,7 +383,7 @@ mod test { )] ); - let target = BuildTarget { + let args = crate::cli::Cli { target_dir: Some( tempdir .path() @@ -394,9 +393,9 @@ mod test { .unwrap() .to_string(), ), - target: None, ..Default::default() }; + let target = BuildTarget::new(&args); let expanded = file_info .generate_expanded_path(&target, &tempdir, 0) .unwrap(); @@ -425,15 +424,15 @@ mod test { ) .unwrap(); let file_info = FileInfo { - source: "target/release/my-bin".into(), - dest: "/usr/bin/my-bin".into(), + source: "target/release/my-bin", + dest: "/usr/bin/my-bin", user: None, group: None, mode: None, config: false, doc: false, }; - let target = BuildTarget { + let args = crate::cli::Cli { target_dir: Some( tempdir .path() @@ -444,9 +443,10 @@ mod test { .to_string(), ), target: Some("target-triple".to_string()), - profile: Some("my-profile".to_string()), + profile: "my-profile".to_string(), ..Default::default() }; + let target = BuildTarget::new(&args); let expanded = file_info .generate_expanded_path(&target, &tempdir, 0) .unwrap(); diff --git a/src/config/metadata.rs b/src/config/metadata.rs index 2e66f0d..8703f07 100644 --- a/src/config/metadata.rs +++ b/src/config/metadata.rs @@ -164,7 +164,7 @@ impl<'a> MetadataConfig<'a> { pub fn new(metadata: &'a Table, branch_path: Option) -> Self { Self { metadata, - branch_path: branch_path.map(|v| v.to_string()), + branch_path, } } @@ -298,7 +298,7 @@ impl<'a> CompoundMetadataConfig<'a> { F: Fn(&MetadataConfig<'a>) -> Result, ConfigError>, { for item in self.config.iter().rev() { - match func(&item) { + match func(item) { v @ (Ok(Some(_)) | Err(_)) => return v, Ok(None) => continue, } @@ -377,11 +377,11 @@ mod test { assert_eq!(metadata_config.get_str("not-exist").unwrap(), None); assert!(matches!( metadata_config.get_str("int"), - Err(ConfigError::WrongType(v, "string")) if v == "int".to_string() + Err(ConfigError::WrongType(v, "string")) if v == "int" )); assert!(matches!( metadata_config.get_string_or_i64("array"), - Err(ConfigError::WrongType(v, "string or integer")) if v == "array".to_string() + Err(ConfigError::WrongType(v, "string or integer")) if v == "array" )); let metadata_config = MetadataConfig { @@ -390,11 +390,11 @@ mod test { }; assert!(matches!( metadata_config.get_str("int"), - Err(ConfigError::WrongType(v, "string")) if v == "branch.int".to_string() + Err(ConfigError::WrongType(v, "string")) if v == "branch.int" )); assert!(matches!( metadata_config.get_string_or_i64("array"), - Err(ConfigError::WrongType(v, "string or integer")) if v == "branch.array".to_string() + Err(ConfigError::WrongType(v, "string or integer")) if v == "branch.array" )); } @@ -413,7 +413,7 @@ mod test { let metadata_config = metadata .iter() .map(|v| MetadataConfig { - metadata: &v, + metadata: v, branch_path: None, }) .collect::>(); diff --git a/src/config/mod.rs b/src/config/mod.rs index 8b12534..19c2e60 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,53 +1,20 @@ use std::path::{Path, PathBuf}; -use std::str::FromStr; use cargo_toml::Error as CargoTomlError; use cargo_toml::Manifest; -use rpm::{CompressionType, Dependency, RPMBuilder}; +use rpm::Dependency; use toml::value::Table; use crate::auto_req::{find_requires, AutoReqMode}; use crate::build_target::BuildTarget; -use crate::error::{ConfigError, Error, PayloadCompressError}; +use crate::cli::Cli; +use crate::error::{ConfigError, Error}; use file_info::FileInfo; use metadata::{CompoundMetadataConfig, ExtraMetaData, MetadataConfig, TomlValueHelper}; mod file_info; mod metadata; -#[derive(Debug, Clone, Default)] -pub enum PayloadCompressType { - None, - Gzip, - #[default] - Zstd, - Xz, -} - -impl FromStr for PayloadCompressType { - type Err = PayloadCompressError; - fn from_str(raw: &str) -> Result { - match raw { - "none" => Ok(PayloadCompressType::None), - "gzip" => Ok(PayloadCompressType::Gzip), - "zstd" => Ok(PayloadCompressType::Zstd), - "xz" => Ok(PayloadCompressType::Xz), - _ => Err(PayloadCompressError::UnsupportedType(raw.to_string())), - } - } -} - -impl From for CompressionType { - fn from(value: PayloadCompressType) -> Self { - match value { - PayloadCompressType::None => Self::None, - PayloadCompressType::Gzip => Self::Gzip, - PayloadCompressType::Zstd => Self::Zstd, - PayloadCompressType::Xz => Self::Xz, - } - } -} - #[derive(Debug, Clone)] pub enum ExtraMetadataSource { File(PathBuf, Option), @@ -55,23 +22,14 @@ pub enum ExtraMetadataSource { } #[derive(Debug)] -pub struct RpmBuilderConfig<'a> { +pub struct BuilderConfig<'a> { build_target: &'a BuildTarget, - auto_req_mode: AutoReqMode, - payload_compress: PayloadCompressType, + args: &'a Cli, } -impl<'a> RpmBuilderConfig<'a> { - pub fn new( - build_target: &'a BuildTarget, - auto_req_mode: AutoReqMode, - payload_compress: PayloadCompressType, - ) -> RpmBuilderConfig<'a> { - RpmBuilderConfig { - build_target, - auto_req_mode, - payload_compress, - } +impl<'a> BuilderConfig<'a> { + pub fn new(build_target: &'a BuildTarget, args: &'a Cli) -> BuilderConfig<'a> { + BuilderConfig { build_target, args } } } @@ -122,7 +80,7 @@ impl Config { let extra_metadata = extra_metadata .iter() - .map(|v| ExtraMetaData::new(v)) + .map(ExtraMetaData::new) .collect::, _>>()?; Ok(Config { @@ -143,7 +101,7 @@ impl Config { .as_str() .ok_or(ConfigError::WrongDependencyVersion(key.clone()))? .trim(); - let ver_vec = ver.trim().split_whitespace().collect::>(); + let ver_vec = ver.split_whitespace().collect::>(); let dependency = match ver_vec.as_slice() { [] | ["*"] => Ok(Dependency::any(key)), ["<", ver] => Ok(Dependency::less(key.as_str(), ver.trim())), @@ -158,10 +116,7 @@ impl Config { Ok(dependencies) } - pub fn create_rpm_builder( - &self, - rpm_builder_config: RpmBuilderConfig, - ) -> Result { + pub fn create_rpm_builder(&self, cfg: BuilderConfig) -> Result { let mut metadata_config = Vec::new(); metadata_config.push(MetadataConfig::new_from_manifest(&self.manifest)?); for v in &self.extra_metadata { @@ -174,9 +129,7 @@ impl Config { .package .as_ref() .ok_or(ConfigError::Missing("package".to_string()))?; - let name = metadata - .get_str("name")? - .unwrap_or_else(|| pkg.name.as_str()); + let name = metadata.get_str("name")?.unwrap_or(pkg.name.as_str()); let version = match metadata.get_str("version")? { Some(v) => v, None => pkg.version.get()?, @@ -186,7 +139,7 @@ impl Config { (None, None) => Err(ConfigError::Missing("package.license".to_string()))?, (None, Some(v)) => v.get()?, }; - let arch = rpm_builder_config.build_target.binary_arch(); + let arch = cfg.build_target.binary_arch(); let desc = match ( metadata.get_str("summary")?, metadata.get_str("description")?, @@ -203,12 +156,22 @@ impl Config { let files = FileInfo::new(assets)?; let parent = self.manifest_path.parent().unwrap(); - let mut builder = RPMBuilder::new(name, version, license, arch.as_str(), desc) - .compression(CompressionType::from(rpm_builder_config.payload_compress)); + let mut builder = rpm::PackageBuilder::new(name, version, license, arch.as_str(), desc) + .compression(cfg.args.payload_compress); + builder = if let Some(t) = cfg.args.source_date { + builder.source_date(t) + } else if let Ok(t) = std::env::var("SOURCE_DATE_EPOCH") { + let t = t + .parse::() + .map_err(|err| Error::EnvError("SOURCE_DATE_EPOCH", err.to_string()))?; + builder.source_date(t) + } else { + builder + }; + let mut expanded_file_paths = vec![]; for (idx, file) in files.iter().enumerate() { - let entries = - file.generate_rpm_file_entry(rpm_builder_config.build_target, parent, idx)?; + let entries = file.generate_rpm_file_entry(cfg.build_target, parent, idx)?; for (file_source, options) in entries { expanded_file_paths.push(file_source.clone()); builder = builder.with_file(file_source, options)?; @@ -261,13 +224,13 @@ impl Config { builder = builder.requires(dependency); } } - let auto_req = if rpm_builder_config.auto_req_mode == AutoReqMode::Auto - && matches!(metadata.get_str("auto-req")?, Some("no") | Some("disabled")) - { - AutoReqMode::Disabled - } else { - rpm_builder_config.auto_req_mode + + let meta_aut_req = metadata.get_str("auto-req")?; + let auto_req = match (&cfg.args.auto_req, meta_aut_req) { + (None, Some("no" | "disabled")) => AutoReqMode::Disabled, + (r, _) => r.into(), }; + for requires in find_requires(expanded_file_paths, auto_req)? { builder = builder.requires(Dependency::any(requires)); } @@ -320,7 +283,7 @@ mod test { std::fs::create_dir(&workspace_dir).unwrap(); std::fs::write( - &workspace_dir.join("Cargo.toml"), + workspace_dir.join("Cargo.toml"), r#" [workspace] members = ["bar"] @@ -335,7 +298,7 @@ documentation = "https://example.com/bar" .unwrap(); std::fs::create_dir(&project_dir).unwrap(); std::fs::write( - &project_dir.join("Cargo.toml"), + project_dir.join("Cargo.toml"), r#" [package] name = "bar" @@ -421,14 +384,15 @@ documentation.workspace = true #[test] fn test_config_create_rpm_builder() { let config = Config::new(Path::new("."), None, &[]).unwrap(); - let builder = config.create_rpm_builder(RpmBuilderConfig::new( - &BuildTarget::default(), - AutoReqMode::Disabled, - PayloadCompressType::default(), - )); + let args = crate::cli::Cli { + ..Default::default() + }; + let target = BuildTarget::new(&args); + let cfg = BuilderConfig::new(&target, &args); + let builder = config.create_rpm_builder(cfg); assert!(if Path::new("target/release/cargo-generate-rpm").exists() { - matches!(builder, Ok(_)) + builder.is_ok() } else { matches!(builder, Err(Error::Config(ConfigError::AssetFileNotFound(path))) if path.to_str() == Some("target/release/cargo-generate-rpm")) }); diff --git a/src/error.rs b/src/error.rs index 3379efc..28528f9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,4 @@ use cargo_toml::Error as CargoTomlError; -use rpm::RPMError; use std::error::Error as StdError; use std::ffi::OsString; use std::fmt::{Debug, Display, Formatter}; @@ -57,26 +56,20 @@ impl Display for FileAnnotatedError { #[derive(thiserror::Error, Debug)] pub enum AutoReqError { - #[error("Wrong auto-req mode")] - WrongMode, #[error("Failed to execute `{}`: {1}", .0.clone().into_string().unwrap_or_default())] ProcessError(OsString, #[source] IoError), #[error(transparent)] Io(#[from] IoError), } -#[derive(thiserror::Error, Debug)] -pub enum PayloadCompressError { - #[error("Unsupported payload compress type: {0}")] - UnsupportedType(String), -} - #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Cargo.toml: {0}")] CargoToml(#[from] CargoTomlError), #[error(transparent)] Config(#[from] ConfigError), + #[error("Invalid value of enviroment variable {0}: {1}")] + EnvError(&'static str, String), #[error(transparent)] ParseTomlFile(#[from] FileAnnotatedError), #[error(transparent)] @@ -84,9 +77,7 @@ pub enum Error { #[error(transparent)] AutoReq(#[from] AutoReqError), #[error(transparent)] - Rpm(#[from] RPMError), - #[error(transparent)] - PayloadCompress(#[from] PayloadCompressError), + Rpm(#[from] rpm::Error), #[error("{1}: {0}")] FileIo(PathBuf, #[source] IoError), #[error(transparent)] diff --git a/src/main.rs b/src/main.rs index f4f663a..418223d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,247 +1,103 @@ -extern crate core; - -use crate::auto_req::AutoReqMode; -use crate::build_target::BuildTarget; -use crate::config::{Config, ExtraMetadataSource, PayloadCompressType, RpmBuilderConfig}; -use crate::error::Error; -use getopts::Options; -use std::convert::TryFrom; -use std::env; -use std::fs::{create_dir_all, File}; -use std::path::{Path, PathBuf}; +use crate::{build_target::BuildTarget, config::BuilderConfig}; +use clap::Parser; +use cli::{CargoWrapper, Cli}; +use std::{ + fs, + path::{Path, PathBuf}, +}; mod auto_req; mod build_target; +mod cli; mod config; mod error; -#[derive(Debug)] -struct CliSetting { - auto_req_mode: AutoReqMode, - payload_compress: PayloadCompressType, - extra_metadata: Vec, +use config::{Config, ExtraMetadataSource}; +use error::Error; + +fn collect_metadata(args: &Cli) -> Vec { + args.metadata_overwrite + .iter() + .map(|v| { + let (file, branch) = match v.split_once('#') { + None => (PathBuf::from(v), None), + Some((file, branch)) => (PathBuf::from(file), Some(branch.to_string())), + }; + ExtraMetadataSource::File(file, branch) + }) + .chain( + args.set_metadata + .iter() + .map(|v| ExtraMetadataSource::Text(v.to_string())), + ) + .chain(args.variant.iter().map(|v| { + let file = match &args.package { + Some(package) => Config::create_cargo_toml_path(package), + None => Config::create_cargo_toml_path(""), + }; + let branch = String::from("package.metadata.generate-rpm.variants.") + v; + ExtraMetadataSource::File(file, Some(branch)) + })) + .collect::>() } -fn process( - build_target: &BuildTarget, - target_path: Option, - package: Option, - setting: CliSetting, -) -> Result<(), Error> { - let config = if let Some(p) = package { - Config::new( - Path::new(&p), - Some(Path::new("")), - setting.extra_metadata.as_slice(), - )? +fn run() -> Result<(), Error> { + let mut args = std::env::args(); + let args = if let Some("generate-rpm") = args.nth(1).as_deref() { + let CargoWrapper::GenerateRpm(args) = CargoWrapper::parse(); + args } else { - Config::new(Path::new(""), None, setting.extra_metadata.as_slice())? + Cli::parse() }; + let build_target = BuildTarget::new(&args); + let extra_metadata = collect_metadata(&args); + + let config = if let Some(p) = &args.package { + Config::new(Path::new(p), Some(Path::new("")), &extra_metadata)? + } else { + Config::new(Path::new(""), None, &extra_metadata)? + }; let rpm_pkg = config - .create_rpm_builder(RpmBuilderConfig::new( - build_target, - setting.auto_req_mode, - setting.payload_compress, - ))? + .create_rpm_builder(BuilderConfig::new(&build_target, &args))? .build()?; - let default_file_name = build_target.target_path("generate-rpm").join(format!( - "{}-{}{}{}.rpm", - rpm_pkg.metadata.get_name()?, - rpm_pkg.metadata.get_version()?, - rpm_pkg - .metadata - .get_release() - .map(|v| format!("-{}", v)) - .unwrap_or_default(), - rpm_pkg - .metadata - .get_arch() - .map(|v| format!(".{}", v)) - .unwrap_or_default(), - )); - - let target_file_name = match target_path { - Some(path) => { - if path.is_dir() { - path.join(default_file_name.file_name().unwrap()) - } else { - path - } - } - None => default_file_name, + let pkg_name = rpm_pkg.metadata.get_name()?; + let pkg_version = rpm_pkg.metadata.get_version()?; + let pkg_release = rpm_pkg + .metadata + .get_release() + .map(|v| format!("-{}", v)) + .unwrap_or_default(); + let pkg_arch = rpm_pkg + .metadata + .get_arch() + .map(|v| format!(".{}", v)) + .unwrap_or_default(); + let file_name = format!("{pkg_name}-{pkg_version}{pkg_release}{pkg_arch}.rpm"); + + let target_file_name = match args.target.map(PathBuf::from) { + Some(path) if path.is_dir() => path.join(file_name), + Some(path) => path, + None => build_target.target_path("generate-rpm").join(file_name), }; if let Some(parent_dir) = target_file_name.parent() { if !parent_dir.exists() { - create_dir_all(parent_dir) + fs::create_dir_all(parent_dir) .map_err(|err| Error::FileIo(parent_dir.to_path_buf(), err))?; } } - let mut f = File::create(&target_file_name) + let mut f = fs::File::create(&target_file_name) .map_err(|err| Error::FileIo(target_file_name.to_path_buf(), err))?; rpm_pkg.write(&mut f)?; Ok(()) } -fn parse_arg() -> Result<(BuildTarget, Option, Option, CliSetting), Error> { - let program = env::args().nth(0).unwrap(); - let mut build_target = BuildTarget::default(); - - let mut opts = Options::new(); - opts.optopt("a", "arch", "set target arch", "ARCH"); - opts.optopt("o", "output", "set output file or directory", "OUTPUT"); - opts.optopt( - "p", - "package", - "set a package name of the workspace", - "NAME", - ); - opts.optopt( - "", - "auto-req", - "set automatic dependency processing mode, \ - auto(Default), no, builtin, /path/to/find-requires", - "MODE", - ); - opts.optopt( - "", - "target", - "Sub-directory name for all generated artifacts. \ - May be specified with CARGO_BUILD_TARGET environment variable.", - "TARGET-TRIPLE", - ); - opts.optopt( - "", - "target-dir", - "Directory for all generated artifacts. \ - May be specified with CARGO_BUILD_TARGET_DIR or CARGO_TARGET_DIR environment variables.", - "DIRECTORY", - ); - opts.optopt( - "", - "profile", - "Select which build profile to package. Defaults to \"release\".", - "PROFILE", - ); - opts.optopt( - "", - "payload-compress", - "Compression type of package payloads. \ - none, gzip, zstd(Default) or xz.", - "TYPE", - ); - opts.optmulti( - "", - "metadata-overwrite", - "Overwrite metadata with TOML file. \ - if \"#dotted.key\" suffixed, load \"dotted.key\" table instead of the root table.", - "TOML_FILE", - ); - opts.optmulti( - "s", - "set-metadata", - "Overwrite metadata with TOML text.", - "TOML", - ); - opts.optopt( - "", - "variant", - "Shortcut to --metadata-overwrite=path/to/Cargo.toml#package.metadata.generate-rpm.variants.VARIANT", - "VARIANT", - ); - - opts.optflag("h", "help", "print this help menu"); - opts.optflag("V", "version", "print version information"); - - let opt_matches = opts.parse(env::args().skip(1)).unwrap_or_else(|err| { - eprintln!("{}: {}", program, err); - std::process::exit(1); - }); - if opt_matches.opt_present("h") { - println!("{}", opts.usage(&*format!("Usage: {} [options]", program))); - std::process::exit(0); - } - if opt_matches.opt_present("V") { - println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); - std::process::exit(0); - } - - if let Some(target_arch) = opt_matches.opt_str("a") { - build_target.arch = Some(target_arch); - } - let target_path = opt_matches.opt_str("o").map(PathBuf::from); - let package = opt_matches.opt_str("p"); - let auto_req_mode = AutoReqMode::try_from( - opt_matches - .opt_str("auto-req") - .unwrap_or("auto".to_string()), - )?; - if let Some(target) = opt_matches.opt_str("target") { - build_target.target = Some(target); - } - if let Some(target_dir) = opt_matches.opt_str("target-dir") { - build_target.target_dir = Some(target_dir); - } - if let Some(profile) = opt_matches.opt_str("profile") { - build_target.profile = Some(profile); - } - let payload_compress = opt_matches - .opt_str("payload-compress") - .map(|v| v.parse::()) - .unwrap_or(Ok(PayloadCompressType::default()))?; - let metadata_overwrite = opt_matches.opt_strs_pos("metadata-overwrite"); - let set_metadata = opt_matches.opt_strs_pos("set-metadata"); - let variant = opt_matches.opt_strs_pos("variant"); - - let mut extra_metadata = metadata_overwrite - .iter() - .map(|(i, v)| { - let (file, branch) = match v.split_once("#") { - None => (PathBuf::from(v), None), - Some((file, branch)) => (PathBuf::from(file), Some(branch.to_string())), - }; - (*i, ExtraMetadataSource::File(file, branch)) - }) - .chain( - set_metadata - .iter() - .map(|(i, v)| (*i, ExtraMetadataSource::Text(v.to_string()))), - ) - .chain(variant.iter().map(|(i, v)| { - let file = Config::create_cargo_toml_path(package.as_ref().unwrap_or(&"".to_string())); - let branch = String::from("package.metadata.generate-rpm.variants.") + v; - (*i, ExtraMetadataSource::File(file, Some(branch))) - })) - .collect::>(); - extra_metadata.sort_by_key(|(i, _)| *i); - let extra_metadata = extra_metadata.iter().map(|(_, v)| v).cloned().collect(); - Ok(( - build_target, - target_path, - package, - CliSetting { - auto_req_mode, - payload_compress, - extra_metadata, - }, - )) -} - fn main() { - (|| -> Result<(), Error> { - let (build_target, target_file, package, setting) = parse_arg()?; - process(&build_target, target_file, package, setting)?; - Ok(()) - })() - .unwrap_or_else(|err| { - let program = env::args().nth(0).unwrap(); - eprintln!("{}: {}", program, err); - if cfg!(debug_assertions) { - panic!("{:?}", err); - } + if let Err(err) = run() { + eprintln!("{err}"); std::process::exit(1); - }); + } }