From 2a01452c40e24f0eafb87c05188dc75def927c55 Mon Sep 17 00:00:00 2001 From: Daniel Alley Date: Mon, 11 Dec 2023 10:31:58 -0500 Subject: [PATCH 01/11] Bump release version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1172a48e..459ce5b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rpm" -version = "0.13.0" +version = "0.13.1" authors = [ "René Richter ", "Bernhard Schuster ", From 9efa497d975102ee385550c7bd93744b522e09b4 Mon Sep 17 00:00:00 2001 From: Daniel Alley Date: Mon, 25 Dec 2023 14:37:13 -0500 Subject: [PATCH 02/11] Add MacOS to CI --- .github/workflows/ci.yml | 6 +++++- tests/compat.rs | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b8023c3..8bf1f272 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,6 @@ jobs: test-feature-matrix: name: Test Suite (feature-matrix) - runs-on: ubuntu-latest strategy: matrix: rust: @@ -63,6 +62,11 @@ jobs: flags: - "--all-features" - "--no-default-features" + os: + - "ubuntu-latest" + - "macos-latest" + # - "windows-latest" TODO + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@master diff --git a/tests/compat.rs b/tests/compat.rs index 46185005..58a0c249 100644 --- a/tests/compat.rs +++ b/tests/compat.rs @@ -8,6 +8,7 @@ mod common; use signature::{self, Verifying}; +#[cfg(target_os = "linux")] mod pgp { use super::*; use signature::pgp::{Signer, Verifier}; From 89fc05fac073943e90e7a45d759abc0afb5888fe Mon Sep 17 00:00:00 2001 From: Daniel Alley Date: Sat, 15 Jul 2023 10:27:50 -0400 Subject: [PATCH 03/11] Improve documentation --- CHANGELOG.md | 4 ++ src/constants.rs | 24 +------- src/rpm/builder.rs | 118 +++++++++++++++++++++++++-------------- src/rpm/headers/types.rs | 102 ++++++++++++++++++++++++--------- src/rpm/package.rs | 5 +- 5 files changed, 160 insertions(+), 93 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b91b8a98..404b3d60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +- Improved documentation + ## 0.13.1 ### Added diff --git a/src/constants.rs b/src/constants.rs index b7b8afe4..0e9ad02e 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -516,23 +516,13 @@ bitflags! { bitflags! { /// Flags to configure scriptlet execution, - /// #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] pub struct ScriptletFlags: u32 { - /// Macro expansion, - /// - /// Corresponds to RPMSCRIPT_FLAG_EXPAND - /// + /// Macro expansion const EXPAND = 1; - /// Header queryformat expansion, - /// - /// Corresponds to RPMSCRIPT_FLAG_QFORMAT - /// + /// Header queryformat expansion const QFORMAT = 1 << 1; - /// Critical for success/failure, - /// - /// Corresponds to RPMSCRIPT_FLAG_CRITICAL - /// + /// Critical for success/failure const CRITICAL = 1 << 2; } } @@ -593,7 +583,6 @@ pub enum DigestAlgorithm { } /// Index tag values for the %prein scriptlet, -/// pub(crate) const PREIN_TAGS: ScriptletIndexTags = ( IndexTag::RPMTAG_PREIN, IndexTag::RPMTAG_PREINFLAGS, @@ -601,7 +590,6 @@ pub(crate) const PREIN_TAGS: ScriptletIndexTags = ( ); /// Index tag values for the %postin scriptlet, -/// pub(crate) const POSTIN_TAGS: ScriptletIndexTags = ( IndexTag::RPMTAG_POSTIN, IndexTag::RPMTAG_POSTINFLAGS, @@ -609,7 +597,6 @@ pub(crate) const POSTIN_TAGS: ScriptletIndexTags = ( ); /// Index tag values for the %preun scriptlet, -/// pub(crate) const PREUN_TAGS: ScriptletIndexTags = ( IndexTag::RPMTAG_PREUN, IndexTag::RPMTAG_PREUNFLAGS, @@ -617,7 +604,6 @@ pub(crate) const PREUN_TAGS: ScriptletIndexTags = ( ); /// Index tag values for the %postun scriptlet, -/// pub(crate) const POSTUN_TAGS: ScriptletIndexTags = ( IndexTag::RPMTAG_POSTUN, IndexTag::RPMTAG_POSTUNFLAGS, @@ -625,7 +611,6 @@ pub(crate) const POSTUN_TAGS: ScriptletIndexTags = ( ); /// Index tag values for the %pretrans scriptlet, -/// pub(crate) const PRETRANS_TAGS: ScriptletIndexTags = ( IndexTag::RPMTAG_PRETRANS, IndexTag::RPMTAG_PRETRANSFLAGS, @@ -633,7 +618,6 @@ pub(crate) const PRETRANS_TAGS: ScriptletIndexTags = ( ); /// Index tag values for the %posttrans scriptlet, -/// pub(crate) const POSTTRANS_TAGS: ScriptletIndexTags = ( IndexTag::RPMTAG_POSTTRANS, IndexTag::RPMTAG_POSTTRANSFLAGS, @@ -641,7 +625,6 @@ pub(crate) const POSTTRANS_TAGS: ScriptletIndexTags = ( ); /// Index tag values for the %preuntrans scriptlet, -/// pub(crate) const PREUNTRANS_TAGS: ScriptletIndexTags = ( IndexTag::RPMTAG_PREUNTRANS, IndexTag::RPMTAG_PREUNTRANSFLAGS, @@ -649,7 +632,6 @@ pub(crate) const PREUNTRANS_TAGS: ScriptletIndexTags = ( ); /// Index tag values for the %postuntrans scriptlet, -/// pub(crate) const POSTUNTRANS_TAGS: ScriptletIndexTags = ( IndexTag::RPMTAG_POSTUNTRANS, IndexTag::RPMTAG_POSTUNTRANSFLAGS, diff --git a/src/rpm/builder.rs b/src/rpm/builder.rs index 1646b921..864abe98 100644 --- a/src/rpm/builder.rs +++ b/src/rpm/builder.rs @@ -121,28 +121,64 @@ impl PackageBuilder { } } - pub fn vendor(mut self, content: impl Into) -> Self { - self.vendor = Some(content.into()); + /// Set the package epoch. + /// + /// The main scenario in which this is used is if the version numbering scheme of the packaged + /// software changes in such a way that "new" versions are seen as older than packages under + /// the old versioning scheme. Packages with bigger epochs are always treated as newer than + /// packages with smaller epochs, so it can be used as an override, forcing the new packages to + /// be seen as newer. + /// + /// However, because of this, the epoch of a package must never decrease, and shouldn't be set + /// unless required. + pub fn epoch(mut self, epoch: u32) -> Self { + self.epoch = epoch; self } + + /// Set the package release. Exactly what this value represents depends on the + /// distribution's packaging protocols, but often the components are: + /// + /// * an integer representing the number of builds that have been performed with this version + /// * a short-form representation of the distribution name and version + /// * whether this is a pre-release version + /// * source control information + /// + /// The distribution's packaging protocols should be followed when constructing this value. + /// + /// Examples: + /// + /// * 1.el8 + /// * 3.fc38 + /// * 5.el9_2.alma + /// * 0.20230715gitabcdef + pub fn release(mut self, release: impl Into) -> Self { + self.release = release.into(); + self + } + + /// Set the URL for the package. Most often this is the website of the upstream project being + /// packaged pub fn url(mut self, content: impl Into) -> Self { self.url = Some(content.into()); self } + /// Set the version control URL of the upstream project pub fn vcs(mut self, content: impl Into) -> Self { self.vcs = Some(content.into()); self } - pub fn epoch(mut self, epoch: u32) -> Self { - self.epoch = epoch; + /// Define a detailed, multiline, description of what the packaged software does + pub fn description(mut self, desc: impl Into) -> Self { + self.desc = Some(desc.into()); self } - /// Define a detailed, multiline, description of what the packaged software does. - pub fn description(mut self, desc: impl Into) -> Self { - self.desc = Some(desc.into()); + /// Set the package vendor - the name of the organization that is producing the package. + pub fn vendor(mut self, content: impl Into) -> Self { + self.vendor = Some(content.into()); self } @@ -184,7 +220,8 @@ impl PackageBuilder { self } - /// Define a value that can be used for associating several package builds as being part of one operation + /// Define a value that can be used for associating several package builds as being part of + /// one operation /// /// You can use any value, but the standard format is "${build_host} ${build_time}" pub fn cookie(mut self, cookie: impl AsRef) -> Self { @@ -223,8 +260,8 @@ impl PackageBuilder { /// For Xz compression, the expected range is 0 to 9, with a default value of 9. /// For Zstd compression, the expected range is 1 to 22, with a default value of 19. /// - /// If this method is not called, the payload will be Gzip compressed by default. This may change - /// in future versions of the library. + /// If this method is not called, the payload will be Gzip compressed by default. This may + /// change in future versions of the library. pub fn compression(mut self, comp: impl Into) -> Self { self.compression = comp.into(); self @@ -372,91 +409,84 @@ impl PackageBuilder { Ok(()) } - /// Sets the scriptlet content for `%prein` during package install and upgrade, - /// - /// **Note** `%prein` script is executed right before the package is installed + /// Set a script to be executed just before the package is installed or upgraded. /// + /// See: %pre from specfile syntax #[inline] pub fn pre_install_script(mut self, content: impl Into) -> Self { self.pre_inst_script = Some(content.into()); self } - /// Sets the scriptlet content for `%postin` during package install and upgrade, - /// - /// **Note** `%postin` script is executed right after the package got installed + /// Set a script to be executed just after the package is installed or upgraded. /// + /// See: %post from specfile syntax #[inline] pub fn post_install_script(mut self, content: impl Into) -> Self { self.post_inst_script = Some(content.into()); self } - /// Sets the scriptlet content for `%preun` during package uninstall and upgrade, - /// - /// **Note** `%preun` script is executed right before the package gets removed + /// Set a script to be executed just before package removal during uninstallation or upgrade. /// + /// See: %preun from specfile syntax #[inline] pub fn pre_uninstall_script(mut self, content: impl Into) -> Self { self.pre_uninst_script = Some(content.into()); self } - /// Sets the scriptlet content for `%postun` during package uninstall and upgrade, - /// - /// **Note** `%postun` script is executed right after the package was removed + /// Set a script to be executed just after package removal during uninstallation or upgrade. /// + /// See: %postun from specfile syntax #[inline] pub fn post_uninstall_script(mut self, content: impl Into) -> Self { self.post_uninst_script = Some(content.into()); self } - /// Sets the scriptlet content for `%pretrans` during package install and upgrade, - /// - /// **Note** `%pretrans` scripts are executed for to be installed packages before any packages are installed/removed + /// Set a script to be executed before a transaction in which the package is installed or + /// upgraded. This happens before any packages have been installed / upgraded / removed. /// + /// See: %pretrans from specfile syntax #[inline] pub fn pre_trans_script(mut self, content: impl Into) -> Self { self.pre_trans_script = Some(content.into()); self } - /// Sets the scriptlet content for `%posttrans` during package install and upgrade, - /// - /// **Note** `%posttrans` scripts are all executed at the end of the transaction that installed their packages + /// Set a script to be executed after a transaction in which the package has been installed + /// or upgraded. This happens after all packages in the transaction have been installed / + /// upgraded / removed. /// + /// See: %posttrans from specfile syntax #[inline] pub fn post_trans_script(mut self, content: impl Into) -> Self { self.post_trans_script = Some(content.into()); self } - /// Sets the scriptlet content for `%preuntrans` during package uninstall and upgrade, - /// - /// **Note** `%preuntrans` scripts are executed for to be removed packages before any packages are installed/removed + /// Set a script to be executed before a transaction in which the package is being removed or + /// upgraded. This happens before any packages have been installed / upgraded / removed. /// + /// See: %preuntrans from specfile syntax #[inline] pub fn pre_untrans_script(mut self, content: impl Into) -> Self { self.pre_untrans_script = Some(content.into()); self } - /// Sets the scriptlet content for `%postuntrans` during package uninstall and upgrade, - /// - /// **Note** `%postuntrans` scripts are all executed at the end of the transaction that removes their packages + /// Set a script to be executed after a transaction in which the package is being removed or + /// upgraded. This happens after all packages in the transaction have been installed / + /// upgraded / removed. /// + /// See: %posttrans from specfile syntax #[inline] pub fn post_untrans_script(mut self, content: impl Into) -> Self { self.post_untrans_script = Some(content.into()); self } - pub fn release(mut self, release: impl Into) -> Self { - self.release = release.into(); - self - } - /// Add a "provides" dependency /// /// These are aliases or capabilities provided by this package which other packages can reference. @@ -531,9 +561,7 @@ impl PackageBuilder { self } - /// build without a signature - /// - /// ignores a present key, if any + /// Build the package pub fn build(self) -> Result { let (lead, header_idx_tag, content) = self.prepare_data()?; @@ -568,7 +596,7 @@ impl PackageBuilder { Ok(pkg) } - /// use an external signer to sing and build + /// Build the package and sign it with the provided signer /// /// See `signature::Signing` for more details. #[cfg(feature = "signature-meta")] @@ -666,6 +694,10 @@ impl PackageBuilder { let mut combined_file_sizes: u64 = 0; + // @todo: sort entries by path? + // @todo: normalize path? + // @todo: remove duplicates? + // if we remove duplicates, remember anything already pre-computed for (cpio_path, entry) in self.files.iter() { combined_file_sizes += entry.size; file_sizes.push(entry.size); diff --git a/src/rpm/headers/types.rs b/src/rpm/headers/types.rs index d25dba17..45902660 100644 --- a/src/rpm/headers/types.rs +++ b/src/rpm/headers/types.rs @@ -184,10 +184,6 @@ impl From for u16 { } } -/// Description of file modes. -/// -/// A subset - #[derive(Debug)] pub struct FileOptions { pub(crate) destination: String, @@ -201,6 +197,10 @@ pub struct FileOptions { } impl FileOptions { + /// Create a new FileOptions for a file which will be placed at the provided path. + /// + /// By default, files will be owned by the "root" user and group, and inherit their permissions + /// from the on-disk file. #[allow(clippy::new_ret_no_self)] pub fn new(dest: impl Into) -> FileOptionsBuilder { FileOptionsBuilder { @@ -224,27 +224,46 @@ pub struct FileOptionsBuilder { } impl FileOptionsBuilder { + /// Indicates that the file should be owned by the specified username. + /// + /// Specifying a non-root user here will direct RPM to create the user via sysusers.d at + /// installation time. + /// + /// See: %attr from specfile syntax pub fn user(mut self, user: impl Into) -> Self { self.inner.user = user.into(); self } + /// Indicates that the file should be part of the specified group. + /// + /// Specifying a non-root group here will direct RPM to create the group via sysusers.d at + /// installation time. + /// + /// See: %attr from specfile syntax pub fn group(mut self, group: impl Into) -> Self { self.inner.group = group.into(); self } + /// Indicates that a file is a symlink pointing to the location provided pub fn symlink(mut self, symlink: impl Into) -> Self { self.inner.symlink = symlink.into(); self } + /// Set the FileMode - type of file (or directory, or symlink) and permissions. + /// + /// See: %attr from specfile syntax pub fn mode(mut self, mode: impl Into) -> Self { self.inner.mode = mode.into(); self.inner.inherit_permissions = false; self } + /// Indicates that a file should have the provided POSIX file capabilities. + /// + /// See: %caps from specfile syntax pub fn caps(mut self, caps: impl Into) -> Result { // verify capabilities self.inner.caps = match FileCaps::from_str(&caps.into()) { @@ -258,31 +277,60 @@ impl FileOptionsBuilder { Ok(self) } + /// Indicates that a file is documentation. + /// + /// See: %doc from specfile syntax pub fn is_doc(mut self) -> Self { self.inner.flag.insert(FileFlags::DOC); self } + /// Indicates that a file is a configuration file. When a package is updated, files marked as + /// configuration files will be checked for modifications compared to their default state, + /// and if any are present then the old configuration file will be saved with a .rpmsave extension. + /// + /// User intervention may be required to reconcile the changes between the new and old configs. + /// + /// See: %config from specfile syntax pub fn is_config(mut self) -> Self { self.inner.flag.insert(FileFlags::CONFIG); self } + /// Indicates that a (configuration) file should not be replaced if it has been modified. + /// When a package is updated, configuration files will be checked for modifications compared + /// to their default state, and if any are present then the new configuration file will + /// be installed with a .rpmnew extension. + /// + /// User intervention may be required to reconcile the changes between the new and old configs. + /// + /// See: %config(noreplace) from specfile syntax pub fn is_no_replace(mut self) -> Self { self.inner.flag.insert(FileFlags::NOREPLACE); self } + /// Indicates that a file ought not to actually be included in the package, but that it should + /// still be considered owned by a package (e.g. a log file). Its attributes are still tracked. + /// + /// See: %ghost from specfile syntax pub fn is_ghost(mut self) -> Self { self.inner.flag.insert(FileFlags::GHOST); self } + /// Indicates that a file is a software license. License files are always included - they are + /// never filtered out during installation. + /// + /// See: %license from specfile syntax pub fn is_license(mut self) -> Self { self.inner.flag.insert(FileFlags::LICENSE); self } + /// Deprecated (use `is_doc()`` instead). Marks a file as a README. + /// + /// See: %readme from specfile syntax pub fn is_readme(mut self) -> Self { self.inner.flag.insert(FileFlags::README); self @@ -304,10 +352,22 @@ pub struct Dependency { } impl Dependency { + /// Create a dependency on any version of some package or file (or string in general). + pub fn any(dep_name: impl Into) -> Self { + Self::new(dep_name.into(), DependencyFlags::ANY, "".to_string()) + } + + /// Create a dependency on an exact version of some package. + pub fn eq(dep_name: impl Into, version: impl Into) -> Self { + Self::new(dep_name.into(), DependencyFlags::EQUAL, version.into()) + } + + /// Create a dependency on a version of some package less than the provided one. pub fn less(dep_name: impl Into, version: impl Into) -> Self { Self::new(dep_name.into(), DependencyFlags::LESS, version.into()) } + /// Create a dependency on a version of some package less than or equal to the provided one. pub fn less_eq(dep_name: impl Into, version: impl Into) -> Self { Self::new( dep_name.into(), @@ -316,14 +376,12 @@ impl Dependency { ) } - pub fn eq(dep_name: impl Into, version: impl Into) -> Self { - Self::new(dep_name.into(), DependencyFlags::EQUAL, version.into()) - } - + /// Create a dependency on a version of some package greater than the provided one. pub fn greater(dep_name: impl Into, version: impl Into) -> Self { Self::new(dep_name.into(), DependencyFlags::GREATER, version.into()) } + /// Create a dependency on a version of some package greater than or equal to the provided one. pub fn greater_eq(dep_name: impl Into, version: impl Into) -> Self { Self::new( dep_name.into(), @@ -332,10 +390,7 @@ impl Dependency { ) } - pub fn any(dep_name: impl Into) -> Self { - Self::new(dep_name.into(), DependencyFlags::ANY, "".to_string()) - } - + /// Create a dependency on an rpm feature, required to install this package pub fn rpmlib(dep_name: impl Into, version: impl Into) -> Self { Self::new( dep_name.into(), @@ -387,22 +442,18 @@ impl std::io::Write for Sha256Writer { /// pub(crate) type ScriptletIndexTags = (IndexTag, IndexTag, IndexTag); -/// Description of a scriptlet as present in a RPM header record, +/// Description of a scriptlet as present in a RPM header record pub struct Scriptlet { - /// Content of the scriptlet, - /// + /// Content of the scriptlet pub script: String, - /// Optional scriptlet flags, - /// + /// Optional scriptlet flags pub flags: Option, - /// Optional scriptlet interpreter/arguments, - /// + /// Optional scriptlet interpreter/arguments pub program: Option>, } impl Scriptlet { - /// Returns a new scriplet, - /// + /// Returns a new scriplet #[inline] pub fn new(script: impl Into) -> Scriptlet { Scriptlet { @@ -412,26 +463,23 @@ impl Scriptlet { } } - /// Sets the scriptlet flags, + /// Sets the scriptlet flags /// /// **Note** These flags can be used to configure macro expansions etc. - /// #[inline] pub fn flags(mut self, flags: ScriptletFlags) -> Self { self.flags = Some(flags); self } - /// Sets the scriptlet interpreter/arguments, - /// + /// Sets the scriptlet interpreter/arguments #[inline] pub fn prog(mut self, mut prog: Vec>) -> Self { self.program = Some(prog.drain(..).map(|p| p.into()).collect_vec()); self } - /// Consumes the receiver and applies all index entries for the scriptlet based on builder state, - /// + /// Consumes the receiver and applies all index entries for the scriptlet based on builder state pub(crate) fn apply( self, records: &mut Vec>, diff --git a/src/rpm/package.rs b/src/rpm/package.rs index 9902d89e..8f335449 100644 --- a/src/rpm/package.rs +++ b/src/rpm/package.rs @@ -468,6 +468,7 @@ impl PackageMetadata { .get_entry_data_as_i18n_string(IndexTag::RPMTAG_GROUP) } + /// Get the packager, the name of the person that produced this package #[inline] pub fn get_packager(&self) -> Result<&str, Error> { self.header @@ -504,13 +505,13 @@ impl PackageMetadata { .get_entry_data_as_string(IndexTag::RPMTAG_SOURCERPM) } - /// Get the %prein scriptlet for this package + /// Get the %pre scriptlet for this package #[inline] pub fn get_pre_install_script(&self) -> Result { self.get_scriptlet(PREIN_TAGS) } - /// Get the %postin scriptlet for this package + /// Get the %post scriptlet for this package #[inline] pub fn get_post_install_script(&self) -> Result { self.get_scriptlet(POSTIN_TAGS) From 94544c0dd533bb513d8190b1d2666f647746927b Mon Sep 17 00:00:00 2001 From: Daniel Alley Date: Sun, 7 Jan 2024 14:57:45 -0500 Subject: [PATCH 04/11] Added script and config dependencies --- CHANGELOG.md | 5 +++++ src/constants.rs | 2 +- src/rpm/headers/types.rs | 43 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 404b3d60..00527b5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- `Dependency::script_pre()`, `Dependency::script_post()`, `Dependency::script_preun()`, `Dependency::script_postun()` +- `Dependency::config()` + ### Changed - Improved documentation diff --git a/src/constants.rs b/src/constants.rs index 0e9ad02e..35b9b5dc 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -509,7 +509,7 @@ bitflags! { const TRIGGERPREIN = 1 << 25; // %triggerprein dependency const KEYRING = 1 << 26; // bit 27 unused - const CONFIG = 1 << 28; + const CONFIG = 1 << 28; // config() dependency const META = 1 << 29; // meta dependency } } diff --git a/src/rpm/headers/types.rs b/src/rpm/headers/types.rs index 45902660..32fd6af8 100644 --- a/src/rpm/headers/types.rs +++ b/src/rpm/headers/types.rs @@ -399,6 +399,49 @@ impl Dependency { ) } + /// Add a config dependency + pub fn config(dep_name: &str, version: impl Into) -> Self { + Self::new( + format!("config({})", dep_name), + DependencyFlags::CONFIG | DependencyFlags::EQUAL, + version.into(), + ) + } + + // TODO: Is it ever the case that version matters here? it's at least not the common case + + /// Create a dependency on a package or file required for a pre-install script. + pub fn script_pre(dep_name: impl Into) -> Self { + Self::new(dep_name.into(), DependencyFlags::SCRIPT_PRE, "".to_string()) + } + + /// Create a dependency on a package or file required for a post-install script. + pub fn script_post(dep_name: impl Into) -> Self { + Self::new( + dep_name.into(), + DependencyFlags::SCRIPT_POST, + "".to_string(), + ) + } + + /// Create a dependency on a package or file required for a pre-un-install script. + pub fn script_preun(dep_name: impl Into) -> Self { + Self::new( + dep_name.into(), + DependencyFlags::SCRIPT_PREUN, + "".to_string(), + ) + } + + /// Create a dependency on a package or file required for a post-un-install script. + pub fn script_postun(dep_name: impl Into) -> Self { + Self::new( + dep_name.into(), + DependencyFlags::SCRIPT_POSTUN, + "".to_string(), + ) + } + fn new(dep_name: String, flags: DependencyFlags, version: String) -> Self { Dependency { name: dep_name, From f5492db75e4ee0287328fc155cac9f0918ff507d Mon Sep 17 00:00:00 2001 From: Daniel Alley Date: Sun, 7 Jan 2024 15:06:16 -0500 Subject: [PATCH 05/11] Make Dependency::rpmlib() simpler to use --- CHANGELOG.md | 5 +++++ src/rpm/builder.rs | 24 ++++++++---------------- src/rpm/headers/types.rs | 2 +- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00527b5f..2dba0c3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Breaking Changes + +- `Dependency::rpmlib()` now inserts the `rpmlib()` portion automatically, only the feature name itself should + be provided in the string passed as the name argument. + ### Added - `Dependency::script_pre()`, `Dependency::script_post()`, `Dependency::script_preun()`, `Dependency::script_postun()` diff --git a/src/rpm/builder.rs b/src/rpm/builder.rs index 864abe98..bb67184e 100644 --- a/src/rpm/builder.rs +++ b/src/rpm/builder.rs @@ -752,26 +752,18 @@ impl PackageBuilder { self.version.clone(), )); - self.requires.push(Dependency::rpmlib( - "rpmlib(CompressedFileNames)".to_string(), - "3.0.4-1".to_string(), - )); + self.requires + .push(Dependency::rpmlib("CompressedFileNames", "3.0.4-1")); - self.requires.push(Dependency::rpmlib( - "rpmlib(FileDigests)".to_string(), - "4.6.0-1".to_string(), - )); + self.requires + .push(Dependency::rpmlib("FileDigests", "4.6.0-1")); - self.requires.push(Dependency::rpmlib( - "rpmlib(PayloadFilesHavePrefix)".to_string(), - "4.0-1".to_string(), - )); + self.requires + .push(Dependency::rpmlib("PayloadFilesHavePrefix", "4.0-1")); if self.compression.compression_type() == CompressionType::Zstd { - self.requires.push(Dependency::rpmlib( - "rpmlib(PayloadIsZstd)".to_string(), - "5.4.18-1".to_string(), - )); + self.requires + .push(Dependency::rpmlib("PayloadIsZstd", "5.4.18-1")); } let mut provide_names = Vec::new(); diff --git a/src/rpm/headers/types.rs b/src/rpm/headers/types.rs index 32fd6af8..a68c88a1 100644 --- a/src/rpm/headers/types.rs +++ b/src/rpm/headers/types.rs @@ -393,7 +393,7 @@ impl Dependency { /// Create a dependency on an rpm feature, required to install this package pub fn rpmlib(dep_name: impl Into, version: impl Into) -> Self { Self::new( - dep_name.into(), + format!("rpmlib({})", dep_name.into()), DependencyFlags::RPMLIB | DependencyFlags::EQUAL, version.into(), ) From bb99266e06aa261dcfa8200502344ceecac3de8f Mon Sep 17 00:00:00 2001 From: Daniel Alley Date: Sun, 7 Jan 2024 14:37:22 -0500 Subject: [PATCH 06/11] Add PackageBuilder::verify_script A scriptlet that runs at verification time --- CHANGELOG.md | 1 + src/rpm/builder.rs | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dba0c3a..7d9433e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Dependency::script_pre()`, `Dependency::script_post()`, `Dependency::script_preun()`, `Dependency::script_postun()` - `Dependency::config()` +- `PackageBuilder::verify_script()` ### Changed diff --git a/src/rpm/builder.rs b/src/rpm/builder.rs index bb67184e..f4984d1d 100644 --- a/src/rpm/builder.rs +++ b/src/rpm/builder.rs @@ -72,6 +72,7 @@ pub struct PackageBuilder { post_trans_script: Option, pre_untrans_script: Option, post_untrans_script: Option, + verify_script: Option, /// The author name with email followed by a dash with the version /// `Max Mustermann - 0.1-1` @@ -487,6 +488,16 @@ impl PackageBuilder { self } + /// Set a script to be executed during package verification, post-installation or using + /// `rpm --verify` + /// + /// See: `%verifyscript` from specfile syntax + #[inline] + pub fn verify_script(mut self, content: impl Into) -> Self { + self.verify_script = Some(content.into()); + self + } + /// Add a "provides" dependency /// /// These are aliases or capabilities provided by this package which other packages can reference. From c5a68b900552fcd49a8d71b260897d1dfae2f17c Mon Sep 17 00:00:00 2001 From: Daniel Alley Date: Sun, 7 Jan 2024 14:38:57 -0500 Subject: [PATCH 07/11] Add PackageBuilder::group() and PackageBuilder::packager() --- CHANGELOG.md | 1 + src/rpm/builder.rs | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d9433e9..ba6299cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Dependency::script_pre()`, `Dependency::script_post()`, `Dependency::script_preun()`, `Dependency::script_postun()` - `Dependency::config()` - `PackageBuilder::verify_script()` +- `PackageBuilder::group()` and `PackageBuilder::packager()` ### Changed diff --git a/src/rpm/builder.rs b/src/rpm/builder.rs index f4984d1d..429d1c42 100644 --- a/src/rpm/builder.rs +++ b/src/rpm/builder.rs @@ -82,6 +82,8 @@ pub struct PackageBuilder { compression: CompressionWithLevel, vendor: Option, + packager: Option, + group: Option, url: Option, vcs: Option, cookie: Option, @@ -183,6 +185,19 @@ impl PackageBuilder { self } + /// Set the packager, the name of the person producing the package. This is often not present, + /// or set to the same value as the vendor + pub fn packager(mut self, content: impl Into) -> Self { + self.packager = Some(content.into()); + self + } + + /// Set the package group (this is deprecated in most packaging guidelines) + pub fn group(mut self, content: impl Into) -> Self { + self.group = Some(content.into()); + self + } + /// Define the name of the build host. /// /// Commonly used in conjunction with the `gethostname` crate. From aae834ea1e3b88f0649acaf40c231c95dd3ab147 Mon Sep 17 00:00:00 2001 From: Daniel Alley Date: Fri, 22 Dec 2023 11:16:53 -0500 Subject: [PATCH 08/11] Add rpmlib() feature flag for packages with caps --- CHANGELOG.md | 4 ++++ src/rpm/builder.rs | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba6299cb..df1a8aba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved documentation +### Fixed + +- Using file capabilities now adds the appropriate rpmlib() dependency + ## 0.13.1 ### Added diff --git a/src/rpm/builder.rs b/src/rpm/builder.rs index 429d1c42..1bdd013f 100644 --- a/src/rpm/builder.rs +++ b/src/rpm/builder.rs @@ -719,6 +719,7 @@ impl PackageBuilder { let mut base_names = Vec::with_capacity(files_len); let mut combined_file_sizes: u64 = 0; + let mut uses_file_capabilities = false; // @todo: sort entries by path? // @todo: normalize path? @@ -726,6 +727,9 @@ impl PackageBuilder { // if we remove duplicates, remember anything already pre-computed for (cpio_path, entry) in self.files.iter() { combined_file_sizes += entry.size; + if entry.caps.is_some() { + uses_file_capabilities = true; + } file_sizes.push(entry.size); file_modes.push(entry.mode.into()); file_caps.push(entry.caps.to_owned()); @@ -792,6 +796,11 @@ impl PackageBuilder { .push(Dependency::rpmlib("PayloadIsZstd", "5.4.18-1")); } + if uses_file_capabilities { + self.requires + .push(Dependency::rpmlib("FileCaps", "4.6.1-1".to_owned())); + } + let mut provide_names = Vec::new(); let mut provide_flags = Vec::new(); let mut provide_versions = Vec::new(); From f5b749526022fe7ac29e373baaca5d1a8ad14e52 Mon Sep 17 00:00:00 2001 From: Daniel Alley Date: Fri, 22 Dec 2023 11:03:13 -0500 Subject: [PATCH 09/11] Add support for rpm 4.19 auto-creation of users and groups closes #206 --- CHANGELOG.md | 3 ++- src/rpm/builder.rs | 22 +++++++++++++++++++++- src/rpm/headers/types.rs | 24 ++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df1a8aba..4c6eacf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,9 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `Dependency::script_pre()`, `Dependency::script_post()`, `Dependency::script_preun()`, `Dependency::script_postun()` -- `Dependency::config()` +- `Dependency::config()`, `Dependency::user()`, `Dependency::group()` - `PackageBuilder::verify_script()` - `PackageBuilder::group()` and `PackageBuilder::packager()` +- Added support for the automatic user/group creation feature in rpm 4.19 ### Changed diff --git a/src/rpm/builder.rs b/src/rpm/builder.rs index 1bdd013f..1b684b11 100644 --- a/src/rpm/builder.rs +++ b/src/rpm/builder.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::convert::TryInto; use std::fs; @@ -717,6 +717,8 @@ impl PackageBuilder { let mut file_verify_flags = Vec::with_capacity(files_len); let mut dir_indixes = Vec::with_capacity(files_len); let mut base_names = Vec::with_capacity(files_len); + let mut users_to_create = HashSet::new(); + let mut groups_to_create = HashSet::new(); let mut combined_file_sizes: u64 = 0; let mut uses_file_capabilities = false; @@ -730,6 +732,12 @@ impl PackageBuilder { if entry.caps.is_some() { uses_file_capabilities = true; } + if &entry.user != "root" { + users_to_create.insert(entry.user.clone()); + } + if &entry.group != "root" { + groups_to_create.insert(entry.group.clone()); + } file_sizes.push(entry.size); file_modes.push(entry.mode.into()); file_caps.push(entry.caps.to_owned()); @@ -800,6 +808,16 @@ impl PackageBuilder { self.requires .push(Dependency::rpmlib("FileCaps", "4.6.1-1".to_owned())); } + // TODO: as per https://rpm-software-management.github.io/rpm/manual/users_and_groups.html, + // at some point in the future this might make sense as hard requirements, but since it's a new feature, + // they have to be weak requirements to avoid breaking things. + for user in &users_to_create { + self.recommends.push(Dependency::user(user)); + } + + for group in &groups_to_create { + self.recommends.push(Dependency::group(group)); + } let mut provide_names = Vec::new(); let mut provide_flags = Vec::new(); @@ -885,6 +903,8 @@ impl PackageBuilder { let small_package = combined_file_sizes <= u32::MAX.into(); let mut actual_records = vec![ + // Existence of this tag is how rpm decides whether or not a package is a source rpm or binary rpm + // If the SOURCERPM tag is set, then the package is seen as a binary rpm. IndexEntry::new( IndexTag::RPMTAG_SOURCERPM, offset, diff --git a/src/rpm/headers/types.rs b/src/rpm/headers/types.rs index a68c88a1..6120fa7f 100644 --- a/src/rpm/headers/types.rs +++ b/src/rpm/headers/types.rs @@ -408,6 +408,30 @@ impl Dependency { ) } + /// Create a new user dependency + /// + /// If such a dependency is required, versions of RPM 4.19 and newer will automatically create + /// the required users and groups using systemd sys.users.d. + pub fn user(username: &str) -> Self { + Self::new( + format!("user({})", username), + DependencyFlags::SCRIPT_PRE | DependencyFlags::SCRIPT_POSTUN, + "".to_owned(), + ) + } + + /// Create a new group dependency + /// + /// If such a dependency is required, versions of RPM 4.19 and newer will automatically create + /// the required users and groups using systemd sys.users.d. + pub fn group(groupname: &str) -> Self { + Self::new( + format!("group({})", groupname), + DependencyFlags::SCRIPT_PRE | DependencyFlags::SCRIPT_POSTUN, + "".to_owned(), + ) + } + // TODO: Is it ever the case that version matters here? it's at least not the common case /// Create a dependency on a package or file required for a pre-install script. From ad8ea0e00f97b31e75bb11096b6ee0a0102a14d6 Mon Sep 17 00:00:00 2001 From: Daniel Alley Date: Sun, 7 Jan 2024 15:34:27 -0500 Subject: [PATCH 10/11] Replace FileOptions::is_noreplace() with ::is_config_noreplace() --- CHANGELOG.md | 2 ++ src/rpm/headers/types.rs | 12 +++++++----- src/tests.rs | 4 +--- tests/compat.rs | 3 +-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c6eacf8..142a108d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Dependency::rpmlib()` now inserts the `rpmlib()` portion automatically, only the feature name itself should be provided in the string passed as the name argument. +- `FileOptions::is_no_replace()` is now `FileOptions::is_config_noreplace()` to reflect the fact that the noreplace + flag is only applicable to config files, and have more similar usage relative to `%config(noreplace)` ### Added diff --git a/src/rpm/headers/types.rs b/src/rpm/headers/types.rs index 6120fa7f..d29888e9 100644 --- a/src/rpm/headers/types.rs +++ b/src/rpm/headers/types.rs @@ -297,16 +297,18 @@ impl FileOptionsBuilder { self } - /// Indicates that a (configuration) file should not be replaced if it has been modified. - /// When a package is updated, configuration files will be checked for modifications compared - /// to their default state, and if any are present then the new configuration file will + /// Indicates that a file is a configuration file and that it should not be replaced if it has been + /// modified. When a package is updated, configuration files will be checked for modifications + /// compared to their default state, and if any are present then the new configuration file will /// be installed with a .rpmnew extension. /// /// User intervention may be required to reconcile the changes between the new and old configs. /// /// See: %config(noreplace) from specfile syntax - pub fn is_no_replace(mut self) -> Self { - self.inner.flag.insert(FileFlags::NOREPLACE); + pub fn is_config_noreplace(mut self) -> Self { + self.inner + .flag + .insert(FileFlags::CONFIG | FileFlags::NOREPLACE); self } diff --git a/src/tests.rs b/src/tests.rs index 3dcf3ef7..f1b70aa3 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -30,9 +30,7 @@ However, it does nothing.", .compression(rpm::CompressionType::Gzip) .with_file( "Cargo.toml", - FileOptions::new("/etc/awesome/config.toml") - .is_config() - .is_no_replace(), + FileOptions::new("/etc/awesome/config.toml").is_config_noreplace(), )? // file mode is inherited from source file .with_file("Cargo.toml", FileOptions::new("/usr/bin/awesome"))? diff --git a/tests/compat.rs b/tests/compat.rs index 58a0c249..b1a2b7be 100644 --- a/tests/compat.rs +++ b/tests/compat.rs @@ -51,8 +51,7 @@ mod pgp { cargo_file.to_str().unwrap(), FileOptions::new("/etc/foobar/hugo/bazz.toml") .mode(0o100_777) - .is_config() - .is_no_replace(), + .is_config_noreplace(), )? .with_file( cargo_file.to_str().unwrap(), From ea8f38561cddd7fdadea29b861cf3b1afe3a150b Mon Sep 17 00:00:00 2001 From: Daniel Alley Date: Mon, 8 Jan 2024 23:41:27 -0500 Subject: [PATCH 11/11] Address PR feedback --- src/rpm/headers/types.rs | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/rpm/headers/types.rs b/src/rpm/headers/types.rs index d29888e9..8390b8dc 100644 --- a/src/rpm/headers/types.rs +++ b/src/rpm/headers/types.rs @@ -229,7 +229,7 @@ impl FileOptionsBuilder { /// Specifying a non-root user here will direct RPM to create the user via sysusers.d at /// installation time. /// - /// See: %attr from specfile syntax + /// See: `%attr` from specfile syntax pub fn user(mut self, user: impl Into) -> Self { self.inner.user = user.into(); self @@ -240,7 +240,7 @@ impl FileOptionsBuilder { /// Specifying a non-root group here will direct RPM to create the group via sysusers.d at /// installation time. /// - /// See: %attr from specfile syntax + /// See: `%attr` from specfile syntax pub fn group(mut self, group: impl Into) -> Self { self.inner.group = group.into(); self @@ -254,7 +254,7 @@ impl FileOptionsBuilder { /// Set the FileMode - type of file (or directory, or symlink) and permissions. /// - /// See: %attr from specfile syntax + /// See: `%attr` from specfile syntax pub fn mode(mut self, mode: impl Into) -> Self { self.inner.mode = mode.into(); self.inner.inherit_permissions = false; @@ -263,7 +263,7 @@ impl FileOptionsBuilder { /// Indicates that a file should have the provided POSIX file capabilities. /// - /// See: %caps from specfile syntax + /// See: `%caps` from specfile syntax pub fn caps(mut self, caps: impl Into) -> Result { // verify capabilities self.inner.caps = match FileCaps::from_str(&caps.into()) { @@ -279,7 +279,7 @@ impl FileOptionsBuilder { /// Indicates that a file is documentation. /// - /// See: %doc from specfile syntax + /// See: `%doc` from specfile syntax pub fn is_doc(mut self) -> Self { self.inner.flag.insert(FileFlags::DOC); self @@ -287,11 +287,12 @@ impl FileOptionsBuilder { /// Indicates that a file is a configuration file. When a package is updated, files marked as /// configuration files will be checked for modifications compared to their default state, - /// and if any are present then the old configuration file will be saved with a .rpmsave extension. + /// and if any are present then the old configuration file will be saved with a `.rpmsave` + /// extension. /// /// User intervention may be required to reconcile the changes between the new and old configs. /// - /// See: %config from specfile syntax + /// See: `%config` from specfile syntax pub fn is_config(mut self) -> Self { self.inner.flag.insert(FileFlags::CONFIG); self @@ -300,11 +301,11 @@ impl FileOptionsBuilder { /// Indicates that a file is a configuration file and that it should not be replaced if it has been /// modified. When a package is updated, configuration files will be checked for modifications /// compared to their default state, and if any are present then the new configuration file will - /// be installed with a .rpmnew extension. + /// be installed with a `.rpmnew` extension. /// /// User intervention may be required to reconcile the changes between the new and old configs. /// - /// See: %config(noreplace) from specfile syntax + /// See: `%config(noreplace)` from specfile syntax pub fn is_config_noreplace(mut self) -> Self { self.inner .flag @@ -315,7 +316,7 @@ impl FileOptionsBuilder { /// Indicates that a file ought not to actually be included in the package, but that it should /// still be considered owned by a package (e.g. a log file). Its attributes are still tracked. /// - /// See: %ghost from specfile syntax + /// See: `%ghost` from specfile syntax pub fn is_ghost(mut self) -> Self { self.inner.flag.insert(FileFlags::GHOST); self @@ -324,15 +325,15 @@ impl FileOptionsBuilder { /// Indicates that a file is a software license. License files are always included - they are /// never filtered out during installation. /// - /// See: %license from specfile syntax + /// See: `%license` from specfile syntax pub fn is_license(mut self) -> Self { self.inner.flag.insert(FileFlags::LICENSE); self } - /// Deprecated (use `is_doc()`` instead). Marks a file as a README. + /// Deprecated (use `is_doc()` instead). Marks a file as a README. /// - /// See: %readme from specfile syntax + /// See: `%readme` from specfile syntax pub fn is_readme(mut self) -> Self { self.inner.flag.insert(FileFlags::README); self @@ -434,7 +435,7 @@ impl Dependency { ) } - // TODO: Is it ever the case that version matters here? it's at least not the common case + // @todo: Is it ever the case that version matters here? it's at least not the common case /// Create a dependency on a package or file required for a pre-install script. pub fn script_pre(dep_name: impl Into) -> Self {