diff --git a/Cargo.lock b/Cargo.lock index e9b49cf108..30f698bcf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -202,6 +202,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" +dependencies = [ + "memchr", + "regex-automata 0.4.6", + "serde", +] + [[package]] name = "build-env" version = "0.3.1" @@ -2374,6 +2385,7 @@ dependencies = [ "serde_json", "serde_yaml", "shlex", + "similar-asserts", "system-deps 7.0.3", "systemd", "tempfile", @@ -2604,6 +2616,26 @@ dependencies = [ "libc", ] +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" +dependencies = [ + "bstr", + "unicode-segmentation", +] + +[[package]] +name = "similar-asserts" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe85670573cd6f0fa97940f26e7e6601213c3b0555246c24234131f88c5709e" +dependencies = [ + "console", + "similar", +] + [[package]] name = "siphasher" version = "0.3.10" @@ -3107,6 +3139,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.1.10" diff --git a/Cargo.toml b/Cargo.toml index e097188c1d..b84900db51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,6 +94,9 @@ xmlrpc = "0.15.1" termcolor = "1.4.1" shlex = "1.3.0" +[dev-dependencies] +similar-asserts = "1.6.0" + [build-dependencies] anyhow = "1.0" system-deps = "7.0" diff --git a/rpmostree-cxxrs.cxx b/rpmostree-cxxrs.cxx index 5b76e8b953..a31e039f1f 100644 --- a/rpmostree-cxxrs.cxx +++ b/rpmostree-cxxrs.cxx @@ -1780,6 +1780,7 @@ struct Treefile final : public ::rust::Opaque ::rust::String get_gpg_key () const noexcept; ::rust::String get_automatic_version_suffix () const noexcept; bool get_container () const noexcept; + void assert_no_repovars () const; bool get_machineid_compat () const noexcept; ::rust::Vec< ::rust::String> get_etc_group_members () const noexcept; bool get_boot_location_is_modules () const noexcept; @@ -2626,6 +2627,9 @@ extern "C" bool rpmostreecxx$cxxbridge1$Treefile$get_container (::rpmostreecxx::Treefile const &self) noexcept; + ::rust::repr::PtrLen rpmostreecxx$cxxbridge1$Treefile$assert_no_repovars ( + ::rpmostreecxx::Treefile const &self) noexcept; + bool rpmostreecxx$cxxbridge1$Treefile$get_machineid_compat ( ::rpmostreecxx::Treefile const &self) noexcept; @@ -5188,6 +5192,16 @@ Treefile::get_container () const noexcept return rpmostreecxx$cxxbridge1$Treefile$get_container (*this); } +void +Treefile::assert_no_repovars () const +{ + ::rust::repr::PtrLen error$ = rpmostreecxx$cxxbridge1$Treefile$assert_no_repovars (*this); + if (error$.ptr) + { + throw ::rust::impl< ::rust::Error>::error (error$); + } +} + bool Treefile::get_machineid_compat () const noexcept { diff --git a/rpmostree-cxxrs.h b/rpmostree-cxxrs.h index 91e5464883..2ef86b4e3f 100644 --- a/rpmostree-cxxrs.h +++ b/rpmostree-cxxrs.h @@ -1557,6 +1557,7 @@ struct Treefile final : public ::rust::Opaque ::rust::String get_gpg_key () const noexcept; ::rust::String get_automatic_version_suffix () const noexcept; bool get_container () const noexcept; + void assert_no_repovars () const; bool get_machineid_compat () const noexcept; ::rust::Vec< ::rust::String> get_etc_group_members () const noexcept; bool get_boot_location_is_modules () const noexcept; diff --git a/rust/src/cli_experimental.rs b/rust/src/cli_experimental.rs new file mode 100644 index 0000000000..725aca2bac --- /dev/null +++ b/rust/src/cli_experimental.rs @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use anyhow::Result; +use camino::Utf8PathBuf; +use clap::{Parser, ValueEnum}; +use std::fmt::Display; + +#[derive(Debug, Parser)] +#[clap(rename_all = "kebab-case")] +/// Main options struct +struct Experimental { + #[clap(subcommand)] + cmd: Cmd, +} + +#[derive(Debug, clap::Subcommand)] +#[clap(rename_all = "kebab-case")] +/// Subcommands +enum Cmd { + /// Verbs for (container) build time operations. + Build { + #[clap(subcommand)] + cmd: BuildCmd, + }, +} + +/// Choice of build backend. +#[derive(Debug, Clone, Default, clap::ValueEnum)] +enum Mechanism { + /// Use rpm-ostree to construct the root. + #[default] + RpmOstree, + /// Use dnf to construct the root. + Dnf, +} + +impl Display for Mechanism { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.to_possible_value().unwrap().get_name().fmt(f) + } +} + +#[derive(Debug, clap::Subcommand)] +#[clap(rename_all = "kebab-case")] +/// Subcommands +enum BuildCmd { + /// Initialize a root filesystem tree from a set of packages, + /// including setting up mounts for the API filesystems. + /// + /// All configuration for rpm/dnf will come from the source root. + InitRootFromManifest { + /// Path to source root used for base rpm/dnf configuration. + #[clap(long, required = true)] + source_root: Utf8PathBuf, + + /// Path to rpm-ostree treefile. + #[clap(long, required = true)] + manifest: Utf8PathBuf, + + /// Path to the target root, which should not exist. However, its parent + /// directory must exist. + target: Utf8PathBuf, + }, +} + +impl BuildCmd { + fn run(self) -> Result<()> { + match self { + BuildCmd::InitRootFromManifest { + source_root, + manifest, + target, + } => { + crate::compose::build_rootfs_from_manifest(&source_root, &manifest, &target) + } + } + } +} + +/// Primary entrypoint to running our wrapped `yum`/`dnf` handling. +pub fn main(argv: &[&str]) -> Result { + let opt = Experimental::parse_from(argv.into_iter().skip(1)); + match opt.cmd { + Cmd::Build { cmd } => cmd.run()?, + } + Ok(0) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse() -> Result<()> { + let opt = Experimental::try_parse_from([ + "experimental", + "build", + "init-root", + "--source-root=/blah", + "/rootfs", + ]) + .unwrap(); + match opt.cmd { + Cmd::Build { + cmd: BuildCmd::InitRootFromManifest { target, .. }, + } => { + assert_eq!(target, "/rootfs"); + } + } + Ok(()) + } +} diff --git a/rust/src/cmdutils.rs b/rust/src/cmdutils.rs new file mode 100644 index 0000000000..96e4444519 --- /dev/null +++ b/rust/src/cmdutils.rs @@ -0,0 +1,207 @@ +//! Helpers intended for [`std::process::Command`] and related structures. +//! This is copied from bootc, please edit there and re-sync! + +use std::{ + io::{Read, Seek}, + os::unix::process::CommandExt, + process::Command, +}; + +use anyhow::{Context, Result}; + +#[allow(dead_code)] +/// Helpers intended for [`std::process::Command`]. +pub trait CommandRunExt { + /// Log (at debug level) the full child commandline. + fn log_debug(&mut self) -> &mut Self; + + /// Execute the child process. + fn run(&mut self) -> Result<()>; + + /// Ensure the child does not outlive the parent. + fn lifecycle_bind(&mut self) -> &mut Self; + + /// Execute the child process and capture its output. This uses `run` internally + /// and will return an error if the child process exits abnormally. + fn run_get_output(&mut self) -> Result>; + + /// Execute the child process, parsing its stdout as JSON. This uses `run` internally + /// and will return an error if the child process exits abnormally. + fn run_and_parse_json(&mut self) -> Result; +} + +/// Helpers intended for [`std::process::ExitStatus`]. +pub trait ExitStatusExt { + /// If the exit status signals it was not successful, return an error. + /// Note that we intentionally *don't* include the command string + /// in the output; we leave it to the caller to add that if they want, + /// as it may be verbose. + fn check_status(&mut self, stderr: std::fs::File) -> Result<()>; +} + +/// Parse the last chunk (e.g. 1024 bytes) from the provided file, +/// ensure it's UTF-8, and return that value. This function is infallible; +/// if the file cannot be read for some reason, a copy of a static string +/// is returned. +fn last_utf8_content_from_file(mut f: std::fs::File) -> String { + // u16 since we truncate to just the trailing bytes here + // to avoid pathological error messages + const MAX_STDERR_BYTES: u16 = 1024; + let size = f + .metadata() + .map_err(|e| { + tracing::warn!("failed to fstat: {e}"); + }) + .map(|m| m.len().try_into().unwrap_or(u16::MAX)) + .unwrap_or(0); + let size = size.min(MAX_STDERR_BYTES); + let seek_offset = -(size as i32); + let mut stderr_buf = Vec::with_capacity(size.into()); + // We should never fail to seek()+read() really, but let's be conservative + let r = match f + .seek(std::io::SeekFrom::End(seek_offset.into())) + .and_then(|_| f.read_to_end(&mut stderr_buf)) + { + Ok(_) => String::from_utf8_lossy(&stderr_buf), + Err(e) => { + tracing::warn!("failed seek+read: {e}"); + "".into() + } + }; + (&*r).to_owned() +} + +impl ExitStatusExt for std::process::ExitStatus { + fn check_status(&mut self, stderr: std::fs::File) -> Result<()> { + let stderr_buf = last_utf8_content_from_file(stderr); + if self.success() { + return Ok(()); + } + anyhow::bail!(format!("Subprocess failed: {self:?}\n{stderr_buf}")) + } +} + +impl CommandRunExt for Command { + /// Synchronously execute the child, and return an error if the child exited unsuccessfully. + fn run(&mut self) -> Result<()> { + let stderr = tempfile::tempfile()?; + self.stderr(stderr.try_clone()?); + tracing::trace!("exec: {self:?}"); + self.status()?.check_status(stderr) + } + + #[allow(unsafe_code)] + fn lifecycle_bind(&mut self) -> &mut Self { + // SAFETY: This API is safe to call in a forked child. + unsafe { + self.pre_exec(|| { + rustix::process::set_parent_process_death_signal(Some( + rustix::process::Signal::Term, + )) + .map_err(Into::into) + }) + } + } + + /// Output a debug-level log message with this command. + fn log_debug(&mut self) -> &mut Self { + // We unconditionally log at trace level, so avoid double logging + if !tracing::enabled!(tracing::Level::TRACE) { + tracing::debug!("exec: {self:?}"); + } + self + } + + fn run_get_output(&mut self) -> Result> { + let mut stdout = tempfile::tempfile()?; + self.stdout(stdout.try_clone()?); + self.run()?; + stdout.seek(std::io::SeekFrom::Start(0)).context("seek")?; + Ok(Box::new(std::io::BufReader::new(stdout))) + } + + /// Synchronously execute the child, and parse its stdout as JSON. + fn run_and_parse_json(&mut self) -> Result { + let output = self.run_get_output()?; + serde_json::from_reader(output).map_err(Into::into) + } +} + +/// Helpers intended for [`tokio::process::Command`]. +#[allow(async_fn_in_trait)] +#[allow(dead_code)] +pub trait AsyncCommandRunExt { + /// Asynchronously execute the child, and return an error if the child exited unsuccessfully. + async fn run(&mut self) -> Result<()>; +} + +impl AsyncCommandRunExt for tokio::process::Command { + async fn run(&mut self) -> Result<()> { + let stderr = tempfile::tempfile()?; + self.stderr(stderr.try_clone()?); + self.status().await?.check_status(stderr) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn command_run_ext() { + // The basics + Command::new("true").run().unwrap(); + assert!(Command::new("false").run().is_err()); + + // Verify we capture stderr + let e = Command::new("/bin/sh") + .args(["-c", "echo expected-this-oops-message 1>&2; exit 1"]) + .run() + .err() + .unwrap(); + similar_asserts::assert_eq!( + e.to_string(), + "Subprocess failed: ExitStatus(unix_wait_status(256))\nexpected-this-oops-message\n" + ); + + // Ignoring invalid UTF-8 + let e = Command::new("/bin/sh") + .args([ + "-c", + r"echo -e 'expected\xf5\x80\x80\x80\x80-foo\xc0bar\xc0\xc0' 1>&2; exit 1", + ]) + .run() + .err() + .unwrap(); + similar_asserts::assert_eq!( + e.to_string(), + "Subprocess failed: ExitStatus(unix_wait_status(256))\nexpected�����-foo�bar��\n" + ); + } + + #[test] + fn command_run_ext_json() { + #[derive(serde::Deserialize)] + struct Foo { + a: String, + b: u32, + } + let v: Foo = Command::new("echo") + .arg(r##"{"a": "somevalue", "b": 42}"##) + .run_and_parse_json() + .unwrap(); + assert_eq!(v.a, "somevalue"); + assert_eq!(v.b, 42); + } + + #[tokio::test] + async fn async_command_run_ext() { + use tokio::process::Command as AsyncCommand; + let mut success = AsyncCommand::new("true"); + let mut fail = AsyncCommand::new("false"); + // Run these in parallel just because we can + let (success, fail) = tokio::join!(success.run(), fail.run(),); + success.unwrap(); + assert!(fail.is_err()); + } +} diff --git a/rust/src/compose.rs b/rust/src/compose.rs index ee68e68dc8..a8627f11f8 100644 --- a/rust/src/compose.rs +++ b/rust/src/compose.rs @@ -6,8 +6,9 @@ use std::fs::File; use std::io::{BufWriter, Write}; use std::process::Command; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; +use cap_std::fs::Dir; use clap::Parser; use oci_spec::image::ImageManifest; use ostree::gio; @@ -16,6 +17,7 @@ use ostree_ext::containers_image_proxy; use ostree_ext::keyfileext::{map_keyfile_optional, KeyFileExt}; use ostree_ext::{oci_spec, ostree}; +use crate::cmdutils::CommandRunExt; use crate::cxxrsutil::{CxxResult, FFIGObjectWrapper}; #[derive(clap::ValueEnum, Clone, Debug)] @@ -427,3 +429,47 @@ pub(crate) fn configure_build_repo_from_target( Ok(()) } + +pub(crate) fn build_rootfs_from_manifest( + source_root: &Utf8Path, + manifest: &Utf8Path, + target: &Utf8Path, +) -> Result<()> { + if target.try_exists()? { + anyhow::bail!("Refusing to operate on extant {target}") + } + let target_parent = target + .parent() + .ok_or_else(|| anyhow::anyhow!("No parent for {target}"))?; + if !target_parent.try_exists()? { + anyhow::bail!("Expected parent directory of target to exist: {target_parent}"); + } + let tmpdir = tempfile::tempdir_in(target_parent)?; + let tmp_inst: Utf8PathBuf = tmpdir.path().join("inst").try_into()?; + std::fs::create_dir(&tmp_inst)?; + let repo_path: Utf8PathBuf = tmpdir.path().join("repo").try_into()?; + ostree_ext::ostree::Repo::create_at( + libc::AT_FDCWD, + repo_path.as_str(), + ostree_ext::ostree::RepoMode::BareUser, + None, + ostree_ext::gio::Cancellable::NONE, + )?; + Command::new("/proc/self/exe") + .args(["compose", "install"]) + .arg(format!("--source-root={source_root}")) + .arg(format!("--repo={repo_path}")) + .arg(manifest) + .arg(&tmp_inst) + .run()?; + let tmp_rootfs = &&tmp_inst.join("rootfs"); + { + let rootfs = &Dir::open_ambient_dir(tmp_rootfs, cap_std::ambient_authority()) + .context("Opening target root")?; + // In this new path we always use the new Fedora rpmdb location. + // TODO: Handle C9S too + crate::composepost::rpmdb_sysimage_canonical(rootfs)?; + } + std::fs::rename(&tmp_inst, target).with_context(|| format!("Renaming to {target}"))?; + Ok(()) +} diff --git a/rust/src/composepost.rs b/rust/src/composepost.rs index 7d9199f22a..3884bc82b1 100644 --- a/rust/src/composepost.rs +++ b/rust/src/composepost.rs @@ -51,6 +51,9 @@ const RPMOSTREE_BASE_RPMDB: &str = "usr/lib/sysimage/rpm-ostree-base-db"; pub(crate) const RPMOSTREE_RPMDB_LOCATION: &str = "usr/share/rpm"; const RPMOSTREE_SYSIMAGE_RPMDB: &str = "usr/lib/sysimage/rpm"; pub(crate) const TRADITIONAL_RPMDB_LOCATION: &str = "var/lib/rpm"; +/// Name of the sqlite rpmdb file. +#[cfg(test)] +const RPMDB_NAME: &str = "rpmdb.sqlite"; const SD_LOCAL_FS_TARGET_REQUIRES: &str = "usr/lib/systemd/system/local-fs.target.requires"; @@ -659,6 +662,70 @@ fn compose_postprocess_rpmdb(rootfs_dfd: &Dir) -> Result<()> { Ok(()) } +/// Ensure that /usr/lib/sysimage/rpm is the canonical rpmdb location, +/// and make backcompat symlinks from our legacy locations to it. +/// This isn't yet used in the main rpm-ostree compose paths, but only by +/// the new experimental builder path. +#[context("Canonicalizing rpmdb")] +pub(crate) fn rpmdb_sysimage_canonical(rootfs_dfd: &Dir) -> Result<()> { + // This should only be a symlink, if it exists. + rootfs_dfd.remove_file_optional(TRADITIONAL_RPMDB_LOCATION)?; + let new_meta = rootfs_dfd.symlink_metadata_optional(RPMOSTREE_SYSIMAGE_RPMDB)?; + let legacy_meta = rootfs_dfd.symlink_metadata_optional(RPMOSTREE_RPMDB_LOCATION)?; + // Always ensure the parent directory of both paths exists + if new_meta.is_none() { + rootfs_dfd.create_dir_all(Utf8Path::new(RPMOSTREE_SYSIMAGE_RPMDB).parent().unwrap())?; + } + if legacy_meta.is_none() { + rootfs_dfd.create_dir_all(Utf8Path::new(RPMOSTREE_RPMDB_LOCATION).parent().unwrap())?; + } + let uplink_from_old_to_new = format!("../../{RPMOSTREE_SYSIMAGE_RPMDB}"); + if let Some(legacy_meta) = legacy_meta { + if legacy_meta.is_dir() { + match new_meta { + // OK, we found the expected state from an initial rpm-ostree install. + // Move the legacy to the sysimage location, then make a symlink (inverting the previous state). + Some(o) if o.is_symlink() => { + rootfs_dfd.remove_file(RPMOSTREE_SYSIMAGE_RPMDB)?; + rootfs_dfd.rename( + RPMOSTREE_RPMDB_LOCATION, + rootfs_dfd, + RPMOSTREE_SYSIMAGE_RPMDB, + )?; + } + None => { + rootfs_dfd.rename( + RPMOSTREE_RPMDB_LOCATION, + rootfs_dfd, + RPMOSTREE_SYSIMAGE_RPMDB, + )?; + } + Some(o) => anyhow::bail!( + "Unexpected {RPMOSTREE_SYSIMAGE_RPMDB} state (legacy is dir, new type is {:?})", + o.file_type() + ), + } + rootfs_dfd + .symlink_contents(&uplink_from_old_to_new, RPMOSTREE_RPMDB_LOCATION) + .context("Writing symlink")?; + } else if !legacy_meta.is_symlink() { + anyhow::bail!("Unexpected state for {RPMOSTREE_RPMDB_LOCATION}"); + } + } else if let Some(new_meta) = new_meta { + anyhow::ensure!(new_meta.is_dir()); + // We only found the sysimage location, so just make the symlink from the legacy one. + if legacy_meta.is_none() { + rootfs_dfd + .symlink_contents(&uplink_from_old_to_new, RPMOSTREE_RPMDB_LOCATION) + .context("Writing symlink")?; + } + } else { + anyhow::bail!("Failed to find rpm") + } + + Ok(()) +} + /// Enables ostree-state-overlay@.service for /usr/lib/opt and /usr/local. These /// symlinks are also used later in the compose process (and client-side composes) /// as a way to check that state overlays are turned on. @@ -1783,6 +1850,58 @@ OSTREE_VERSION='33.4' assert_eq!(&sysimage_link, Path::new("../../share/rpm")); } + #[test] + fn rpmdb_new_canonical_fromnew() -> Result<()> { + fn case_only_legacy(rootfs: &Dir) -> Result<()> { + let rpmdb_path = &Utf8Path::new(RPMOSTREE_RPMDB_LOCATION).join(RPMDB_NAME); + rootfs.create_dir_all(RPMOSTREE_RPMDB_LOCATION)?; + rootfs.write(rpmdb_path, "dummy rpmdb")?; + Ok(()) + } + fn case_symlinked_new_to_old(rootfs: &Dir) -> Result<()> { + case_only_legacy(rootfs)?; + rootfs.create_dir_all(Utf8Path::new(RPMOSTREE_SYSIMAGE_RPMDB).parent().unwrap())?; + rootfs.symlink_contents( + format!("../../{RPMOSTREE_RPMDB_LOCATION}"), + RPMOSTREE_SYSIMAGE_RPMDB, + )?; + Ok(()) + } + fn case_only_new(rootfs: &Dir) -> Result<()> { + let rpmdb_path = &Utf8Path::new(RPMOSTREE_SYSIMAGE_RPMDB).join(RPMDB_NAME); + rootfs.create_dir_all(RPMOSTREE_SYSIMAGE_RPMDB)?; + rootfs.write(rpmdb_path, "dummy rpmdb")?; + Ok(()) + } + fn verify(rootfs: &Dir) -> Result<()> { + let rpmdb_path = &Utf8Path::new(RPMOSTREE_RPMDB_LOCATION).join(RPMDB_NAME); + // And verify it + assert!(rootfs + .symlink_metadata(RPMOSTREE_RPMDB_LOCATION) + .unwrap() + .is_symlink()); + assert!(rootfs + .symlink_metadata(RPMOSTREE_SYSIMAGE_RPMDB) + .unwrap() + .is_dir()); + assert!(rootfs.symlink_metadata(rpmdb_path).unwrap().is_file()); + Ok(()) + } + for case in [case_only_legacy, case_symlinked_new_to_old, case_only_new] { + // Create a tempdir + let rootfs = &cap_tempfile::tempdir(cap_std::ambient_authority()).unwrap(); + // Initialize the root + case(&rootfs)?; + // Canonicalize + rpmdb_sysimage_canonical(rootfs).unwrap(); + verify(&rootfs).unwrap(); + // And we should be idempotent + rpmdb_sysimage_canonical(rootfs).unwrap(); + verify(&rootfs).unwrap(); + } + Ok(()) + } + #[test] fn test_postprocess_rpm_macro() { static MACRO_PATH: &str = "usr/lib/rpm/macros.d/macros.rpm-ostree"; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index fe00440e59..1cbd60e59d 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -16,6 +16,7 @@ #![allow(clippy::ptr_arg)] // pub(crate) utilities +mod cmdutils; mod cxxrsutil; mod ffiutil; pub(crate) mod ffiwrappers; @@ -624,6 +625,7 @@ pub mod ffi { fn get_gpg_key(&self) -> String; fn get_automatic_version_suffix(&self) -> String; fn get_container(&self) -> bool; + fn assert_no_repovars(&self) -> Result<()>; fn get_machineid_compat(&self) -> bool; fn get_etc_group_members(&self) -> Vec; fn get_boot_location_is_modules(&self) -> bool; @@ -971,6 +973,7 @@ pub(crate) use composepost::*; mod core; use crate::core::*; mod capstdext; +pub mod cli_experimental; mod daemon; pub(crate) use daemon::*; mod deployment_utils; diff --git a/rust/src/main.rs b/rust/src/main.rs index 0bde1a12ee..a3b76d4e13 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -41,6 +41,9 @@ async fn inner_async_main(args: Vec) -> Result { rpmostree_rust::container::container_encapsulate(args_orig).map(|_| 0) .map_err(anyhow::Error::msg) }, + "experimental" => { + rpmostree_rust::cli_experimental::main(args) + } // C++ main _ => Ok(rpmostree_rust::ffi::rpmostree_main(args)?), } diff --git a/rust/src/treefile.rs b/rust/src/treefile.rs index 3334e38961..74e1d1d7c2 100644 --- a/rust/src/treefile.rs +++ b/rust/src/treefile.rs @@ -1345,6 +1345,16 @@ impl Treefile { self.parsed.base.container.unwrap_or(false) } + // --source-root and repovars conflict + pub(crate) fn assert_no_repovars(&self) -> Result<()> { + if let Some(v) = self.parsed.repovars.as_ref() { + if !v.is_empty() { + anyhow::bail!("Cannot use repovars with source root") + } + } + Ok(()) + } + pub(crate) fn get_machineid_compat(&self) -> bool { self.parsed.base.machineid_compat.unwrap_or(true) } diff --git a/src/app/rpmostree-compose-builtin-tree.cxx b/src/app/rpmostree-compose-builtin-tree.cxx index d261a222be..39225a0d53 100644 --- a/src/app/rpmostree-compose-builtin-tree.cxx +++ b/src/app/rpmostree-compose-builtin-tree.cxx @@ -324,8 +324,16 @@ install_packages (RpmOstreeTreeComposeContext *self, gboolean *out_unmodified, rpmlogSetFile (NULL); } - if (!set_repos_dir (dnfctx, **self->treefile_rs, self->workdir_dfd, cancellable, error)) - return FALSE; + if (opt_source_root) + { + CXX_TRY((*self->treefile_rs)->assert_no_repovars(), error); + g_debug ("source root set, validated no repovars"); + } + else + { + if (!set_repos_dir (dnfctx, **self->treefile_rs, self->workdir_dfd, cancellable, error)) + return FALSE; + } /* By default, retain packages in addition to metadata with --cachedir, unless * we're doing unified core, in which case the pkgcache repo is the cache. diff --git a/src/libpriv/rpmostree-core.cxx b/src/libpriv/rpmostree-core.cxx index a8b84e056b..373ee8d5b4 100644 --- a/src/libpriv/rpmostree-core.cxx +++ b/src/libpriv/rpmostree-core.cxx @@ -668,6 +668,11 @@ rpmostree_context_setup (RpmOstreeContext *self, const char *install_root, const dnf_context_set_install_root (self->dnfctx, install_root); dnf_context_set_source_root (self->dnfctx, source_root); + if (source_root) + { + g_autofree char *reposdir = g_build_filename (source_root, "etc/yum.repos.d", NULL); + dnf_context_set_repo_dir (self->dnfctx, reposdir); + } /* Hackaround libdnf logic, ensuring that `/etc/dnf/vars` gets sourced * from the host environment instead of the install_root: