diff --git a/ci/test-container.sh b/ci/test-container.sh index 4aa7e93dfc..01b4cb971a 100755 --- a/ci/test-container.sh +++ b/ci/test-container.sh @@ -144,4 +144,18 @@ if ! grep -qe "error: No such file or directory" err.txt; then fatal "did not find expected error when skipping CLI wraps." fi +# test treefile-apply +if rpm -q ltrace vim-enhanced; then + fatal "ltrace and/or vim-enhanced exist" +fi +vim_vr=$(rpm -q vim-minimal --qf '%{version}-%{release}') +cat > /tmp/treefile.yaml << EOF +packages: + - ltrace + # a split base/layered version-locked package + - vim-enhanced +EOF +rpm-ostree experimental compose treefile-apply /tmp/treefile.yaml +rpm -q ltrace vim-enhanced-"$vim_vr" + echo ok diff --git a/rust/src/cli_experimental.rs b/rust/src/cli_experimental.rs index 10758b1a0f..23865aca92 100644 --- a/rust/src/cli_experimental.rs +++ b/rust/src/cli_experimental.rs @@ -39,6 +39,10 @@ enum ComposeCmd { #[clap(flatten)] opts: crate::compose::CommitToContainerRootfsOpts, }, + TreefileApply { + #[clap(flatten)] + opts: crate::treefile::TreefileApplyOpts, + }, } impl ComposeCmd { @@ -47,6 +51,7 @@ impl ComposeCmd { ComposeCmd::BuildChunkedOCI { opts } => opts.run(), ComposeCmd::Rootfs { opts } => opts.run(), ComposeCmd::CommitToContainerRootfs { opts } => opts.run(), + ComposeCmd::TreefileApply { opts } => opts.run(), } } } diff --git a/rust/src/composepost.rs b/rust/src/composepost.rs index c1057c8c1b..95ec23de24 100644 --- a/rust/src/composepost.rs +++ b/rust/src/composepost.rs @@ -33,7 +33,7 @@ use std::os::unix::io::AsRawFd; use std::os::unix::prelude::IntoRawFd; use std::path::{Path, PathBuf}; use std::pin::Pin; -use std::process::Stdio; +use std::process::{Command, Stdio}; /// Directories that are moved out and symlinked from their `/var/lib/` /// location to `/usr/lib/`. @@ -504,13 +504,18 @@ fn compose_postprocess_default_target(rootfs: &Dir, target: &str) -> Result<()> Ok(()) } +pub(crate) enum PostprocessBwrap { + None, + Wrap { unified_core: bool }, +} + /// The treefile format has two kinds of postprocessing scripts; /// there's a single `postprocess-script` as well as inline (anonymous) /// scripts. This function executes both kinds in bwrap containers. -fn compose_postprocess_scripts( +pub(crate) fn compose_postprocess_scripts( rootfs_dfd: &Dir, treefile: &mut Treefile, - unified_core: bool, + bwrap: PostprocessBwrap, ) -> Result<()> { // Execute the anonymous (inline) scripts. for (i, script) in treefile @@ -531,12 +536,19 @@ fn compose_postprocess_scripts( )?; println!("Executing `postprocess` inline script '{}'", i); let child_argv = vec![binpath.to_string()]; - let _ = bwrap::bubblewrap_run_sync( - rootfs_dfd.as_raw_fd(), - &child_argv, - false, - BubblewrapMutability::for_unified_core(unified_core), - )?; + if let PostprocessBwrap::Wrap { unified_core } = bwrap { + let _ = bwrap::bubblewrap_run_sync( + rootfs_dfd.as_raw_fd(), + &child_argv, + false, + BubblewrapMutability::for_unified_core(unified_core), + ) + .context("Executing inline postprocessing script")?; + } else { + Command::new(&binpath) + .run() + .context("Executing inline postprocessing script")?; + } rootfs_dfd.remove_file(target_binpath)?; } @@ -556,13 +568,19 @@ fn compose_postprocess_scripts( println!("Executing postprocessing script"); let child_argv = &vec![binpath.to_string()]; - let _ = crate::bwrap::bubblewrap_run_sync( - rootfs_dfd.as_raw_fd(), - child_argv, - false, - BubblewrapMutability::for_unified_core(unified_core), - ) - .context("Executing postprocessing script")?; + if let PostprocessBwrap::Wrap { unified_core } = bwrap { + let _ = crate::bwrap::bubblewrap_run_sync( + rootfs_dfd.as_raw_fd(), + child_argv, + false, + BubblewrapMutability::for_unified_core(unified_core), + ) + .context("Executing postprocessing script")?; + } else { + Command::new(&binpath) + .run() + .context("Executing postprocessing script")?; + } rootfs_dfd.remove_file(target_binpath)?; println!("Finished postprocessing script"); @@ -720,7 +738,7 @@ pub fn compose_postprocess( compose_postprocess_add_files(rootfs, treefile)?; etc_guard.undo()?; - compose_postprocess_scripts(rootfs, treefile, unified_core)?; + compose_postprocess_scripts(rootfs, treefile, PostprocessBwrap::Wrap { unified_core })?; Ok(()) } diff --git a/rust/src/treefile.rs b/rust/src/treefile.rs index a2908b8ebf..76eb09e787 100644 --- a/rust/src/treefile.rs +++ b/rust/src/treefile.rs @@ -20,13 +20,14 @@ */ use crate::cmdutils::CommandRunExt; -use crate::cxxrsutil::*; +use crate::{compose_postprocess_scripts, cxxrsutil::*}; use anyhow::{anyhow, bail, Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; use cap_std::fs::MetadataExt as _; use cap_std_ext::cap_std::fs::Dir; use cap_std_ext::cmdext::CapStdExtCommandExt; use cap_std_ext::prelude::CapStdExtDirExt; +use clap::Parser; use fn_error_context::context; use nix::unistd::{Gid, Uid}; use once_cell::sync::Lazy; @@ -4673,3 +4674,67 @@ pub(crate) fn treefile_delete_client_etc() -> CxxResult { } Ok(n) } + +/// Apply a treefile to the running environment (usually during an image build) +#[derive(Debug, Parser)] +pub(crate) struct TreefileApplyOpts { + /// Path to the treefile. + #[clap(value_parser)] + treefile: Utf8PathBuf, +} + +impl TreefileApplyOpts { + pub(crate) fn run(self) -> Result<()> { + let mut tf = Treefile::new_boxed(&self.treefile, Some(&utils::get_rpm_basearch())) + .context("parsing treefile")?; + + // we only handle a subset of fields here + if let Some(packages) = &tf.parsed.packages { + let mut install_args: Vec<_> = packages.iter().map(|s| s.as_str()).collect(); + + if tf.parsed.documentation.is_some() { + bail!("cannot apply documentation directive when deriving"); + } + + // map `recommends` to dnf's `install_weak_deps` + match tf.parsed.recommends { + Some(true) => { + install_args.push("--setopt=install_weak_deps=true"); + } + Some(false) => { + install_args.push("--setopt=install_weak_deps=false"); + } + None => {} // implicitly leave environment default if unset + } + + // lock all base packages during installation + // https://gitlab.com/fedora/bootc/tracker/-/issues/59 + run_dnf("versionlock", &["add", "*", "--disablerepo", "*"]) + .context("locking base packages with dnf")?; + run_dnf("install", &install_args).context("installing packages with dnf")?; + run_dnf("versionlock", &["clear"]).context("clearing base packages lock with dnf")?; + }; + + // handle postprocess scripts + let rootfs_dfd = + Dir::open_ambient_dir("/", cap_std::ambient_authority()).context("opening /")?; + compose_postprocess_scripts(&rootfs_dfd, &mut tf, crate::PostprocessBwrap::None) + .context("running postprocess scripts")?; + + Ok(()) + } +} + +fn run_dnf(command: &str, args: &[&str]) -> Result<()> { + let mut cmd = Command::new("dnf"); + cmd.arg(command).args(args); + cmd.arg("--noplugins"); + cmd.arg("-y"); + + let status = cmd.status().context("collecting dnf status")?; + if !status.success() { + bail!("Failed to run dnf {command}: {status:?}"); + } + + Ok(()) +}