1
0
mirror of https://github.com/containers/bootc.git synced 2026-02-05 15:45:53 +01:00

Gate composefs-native behind composefs-backend flag

Refactor to use `#[cfg(feature = "composefs-backend")]` to gate
composefs native features behind the flag.

Gate the following features
- `--composefs-native` and its corresponding cli args
- Installing/Switching/Upgrading/RollingBack of composefs native system
- Create separate install, rollback functions for ostree for a bit
  cleaner of a setup

Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
This commit is contained in:
Pragyan Poudyal
2025-09-09 14:34:23 +05:30
parent 6f51546d6d
commit cf60f89be9
10 changed files with 243 additions and 153 deletions

View File

@@ -1,17 +1,17 @@
use std::io::Read;
use std::{io::Read, sync::OnceLock};
use anyhow::{Context, Result};
use bootc_kernel_cmdline::Cmdline;
use fn_error_context::context;
use crate::{
bootc_composefs::boot::BootType,
composefs_consts::{BOOT_LOADER_ENTRIES, USER_CFG},
composefs_consts::{BOOT_LOADER_ENTRIES, COMPOSEFS_CMDLINE, USER_CFG},
parsers::{
bls_config::{parse_bls_config, BLSConfig},
grub_menuconfig::{parse_grub_menuentry_file, MenuEntry},
},
spec::{BootEntry, BootOrder, Host, HostSpec, ImageReference, ImageStatus},
status::composefs_booted,
};
use std::str::FromStr;
@@ -35,6 +35,49 @@ use crate::composefs_consts::{
use crate::install::EFIVARFS;
use crate::spec::Bootloader;
/// A parsed composefs command line
pub(crate) struct ComposefsCmdline {
#[allow(dead_code)]
pub insecure: bool,
pub digest: Box<str>,
}
impl ComposefsCmdline {
pub(crate) fn new(s: &str) -> Self {
let (insecure, digest_str) = s
.strip_prefix('?')
.map(|v| (true, v))
.unwrap_or_else(|| (false, s));
ComposefsCmdline {
insecure,
digest: digest_str.into(),
}
}
}
impl std::fmt::Display for ComposefsCmdline {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let insecure = if self.insecure { "?" } else { "" };
write!(f, "{}={}{}", COMPOSEFS_CMDLINE, insecure, self.digest)
}
}
/// Detect if we have composefs=<digest> in /proc/cmdline
pub(crate) fn composefs_booted() -> Result<Option<&'static ComposefsCmdline>> {
static CACHED_DIGEST_VALUE: OnceLock<Option<ComposefsCmdline>> = OnceLock::new();
if let Some(v) = CACHED_DIGEST_VALUE.get() {
return Ok(v.as_ref());
}
let cmdline = Cmdline::from_proc()?;
let Some(kv) = cmdline.find_str(COMPOSEFS_CMDLINE) else {
return Ok(None);
};
let Some(v) = kv.value else { return Ok(None) };
let v = ComposefsCmdline::new(v);
let r = CACHED_DIGEST_VALUE.get_or_init(|| Some(v));
Ok(r.as_ref())
}
// Need str to store lifetime
pub(crate) fn get_sorted_uki_boot_entries<'a>(
boot_dir: &Dir,
@@ -336,6 +379,17 @@ mod tests {
use super::*;
#[test]
fn test_composefs_parsing() {
const DIGEST: &str = "8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52";
let v = ComposefsCmdline::new(DIGEST);
assert!(!v.insecure);
assert_eq!(v.digest.as_ref(), DIGEST);
let v = ComposefsCmdline::new(&format!("?{}", DIGEST));
assert!(v.insecure);
assert_eq!(v.digest.as_ref(), DIGEST);
}
#[test]
fn test_sorted_bls_boot_entries() -> Result<()> {
let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;

View File

@@ -28,15 +28,16 @@ use ostree_ext::sysroot::SysrootLock;
use schemars::schema_for;
use serde::{Deserialize, Serialize};
use crate::bootc_composefs::rollback::composefs_rollback;
use crate::bootc_composefs::switch::switch_composefs;
use crate::bootc_composefs::update::upgrade_composefs;
#[cfg(feature = "composefs-backend")]
use crate::bootc_composefs::{
rollback::composefs_rollback, status::composefs_booted, switch::switch_composefs,
update::upgrade_composefs,
};
use crate::deploy::RequiredHostSpec;
use crate::lints;
use crate::progress_jsonl::{ProgressWriter, RawProgressFd};
use crate::spec::Host;
use crate::spec::ImageReference;
use crate::status::composefs_booted;
use crate::utils::sigpolicy_from_opt;
/// Shared progress options
@@ -1128,29 +1129,21 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
/// Implementation of the `bootc rollback` CLI command.
#[context("Rollback")]
async fn rollback(opts: RollbackOpts) -> Result<()> {
if composefs_booted()?.is_some() {
composefs_rollback().await?
} else {
let sysroot = &get_storage().await?;
let ostree = sysroot.get_ostree()?;
crate::deploy::rollback(sysroot).await?;
async fn rollback(opts: &RollbackOpts) -> Result<()> {
let sysroot = &get_storage().await?;
let ostree = sysroot.get_ostree()?;
crate::deploy::rollback(sysroot).await?;
if opts.soft_reboot.is_some() {
// Get status of rollback deployment to check soft-reboot capability
let host = crate::status::get_status_require_booted(ostree)?.2;
if opts.soft_reboot.is_some() {
// Get status of rollback deployment to check soft-reboot capability
let host = crate::status::get_status_require_booted(ostree)?.2;
handle_soft_reboot(
opts.soft_reboot,
host.status.rollback.as_ref(),
"rollback",
|| soft_reboot_rollback(ostree),
)?;
}
};
if opts.apply {
crate::reboot::reboot()?;
handle_soft_reboot(
opts.soft_reboot,
host.status.rollback.as_ref(),
"rollback",
|| soft_reboot_rollback(ostree),
)?;
}
Ok(())
@@ -1298,20 +1291,44 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
match opt {
Opt::Upgrade(opts) => {
#[cfg(feature = "composefs-backend")]
if composefs_booted()?.is_some() {
upgrade_composefs(opts).await
} else {
upgrade(opts).await
}
#[cfg(not(feature = "composefs-backend"))]
upgrade(opts).await
}
Opt::Switch(opts) => {
#[cfg(feature = "composefs-backend")]
if composefs_booted()?.is_some() {
switch_composefs(opts).await
} else {
switch(opts).await
}
#[cfg(not(feature = "composefs-backend"))]
switch(opts).await
}
Opt::Rollback(opts) => {
#[cfg(feature = "composefs-backend")]
if composefs_booted()?.is_some() {
composefs_rollback().await?
} else {
rollback(&opts).await?
}
#[cfg(not(feature = "composefs-backend"))]
rollback(&opts).await?;
if opts.apply {
crate::reboot::reboot()?;
}
Ok(())
}
Opt::Rollback(opts) => rollback(opts).await,
Opt::Edit(opts) => edit(opts).await,
Opt::UsrOverlay => usroverlay().await,
Opt::Container(opts) => match opts {

View File

@@ -1,3 +1,5 @@
#![allow(dead_code)]
/// composefs= paramter in kernel cmdline
pub const COMPOSEFS_CMDLINE: &str = "composefs";

View File

@@ -41,7 +41,6 @@ use cap_std_ext::prelude::CapStdExtDirExt;
use clap::ValueEnum;
use fn_error_context::context;
use ostree::gio;
use ostree_ext::composefs::fsverity::FsVerityHashValue;
use ostree_ext::ostree;
use ostree_ext::ostree_prepareroot::{ComposefsState, Tristate};
use ostree_ext::prelude::Cast;
@@ -54,8 +53,8 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "install-to-disk")]
use self::baseline::InstallBlockDeviceOpts;
use crate::bootc_composefs::boot::setup_composefs_boot;
use crate::bootc_composefs::repo::initialize_composefs_repository;
#[cfg(feature = "composefs-backend")]
use crate::bootc_composefs::{boot::setup_composefs_boot, repo::initialize_composefs_repository};
use crate::boundimage::{BoundImage, ResolvedBoundImage};
use crate::containerenv::ContainerExecutionInfo;
use crate::deploy::{prepare_for_pull, pull_from_prepared, PreparedImportMeta, PreparedPullResult};
@@ -67,6 +66,8 @@ use crate::task::Task;
use crate::utils::sigpolicy_from_opt;
use bootc_kernel_cmdline::Cmdline;
use bootc_mount::Filesystem;
#[cfg(feature = "composefs-backend")]
use composefs::fsverity::FsVerityHashValue;
/// The toplevel boot directory
const BOOT: &str = "boot";
@@ -263,10 +264,12 @@ pub(crate) struct InstallToDiskOpts {
#[clap(long)]
#[serde(default)]
#[cfg(feature = "composefs-backend")]
pub(crate) composefs_native: bool,
#[clap(flatten)]
#[serde(flatten)]
#[cfg(feature = "composefs-backend")]
pub(crate) composefs_opts: InstallComposefsOpts,
}
@@ -410,6 +413,7 @@ pub(crate) struct State {
pub(crate) tempdir: TempDir,
// If Some, then --composefs_native is passed
#[cfg(feature = "composefs-backend")]
pub(crate) composefs_options: Option<InstallComposefsOpts>,
}
@@ -537,7 +541,7 @@ impl FromStr for MountSpec {
}
}
#[cfg(feature = "install-to-disk")]
#[cfg(all(feature = "install-to-disk", feature = "composefs-backend"))]
impl InstallToDiskOpts {
pub(crate) fn validate(&self) -> Result<()> {
if !self.composefs_native {
@@ -1196,7 +1200,7 @@ async fn prepare_install(
config_opts: InstallConfigOpts,
source_opts: InstallSourceOpts,
target_opts: InstallTargetOpts,
composefs_opts: Option<InstallComposefsOpts>,
_composefs_opts: Option<InstallComposefsOpts>,
) -> Result<Arc<State>> {
tracing::trace!("Preparing install");
let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())
@@ -1341,7 +1345,8 @@ async fn prepare_install(
container_root: rootfs,
tempdir,
host_is_container,
composefs_options: composefs_opts,
#[cfg(feature = "composefs-backend")]
composefs_options: _composefs_opts,
});
Ok(state)
@@ -1440,6 +1445,48 @@ impl BoundImages {
}
}
async fn ostree_install(state: &State, rootfs: &RootSetup, cleanup: Cleanup) -> Result<()> {
// We verify this upfront because it's currently required by bootupd
let boot_uuid = rootfs
.get_boot_uuid()?
.or(rootfs.rootfs_uuid.as_deref())
.ok_or_else(|| anyhow!("No uuid for boot/root"))?;
tracing::debug!("boot uuid={boot_uuid}");
let bound_images = BoundImages::from_state(state).await?;
// Initialize the ostree sysroot (repo, stateroot, etc.)
{
let (sysroot, has_ostree) = initialize_ostree_root(state, rootfs).await?;
install_with_sysroot(
state,
rootfs,
&sysroot,
&boot_uuid,
bound_images,
has_ostree,
)
.await?;
let ostree = sysroot.get_ostree()?;
if matches!(cleanup, Cleanup::TriggerOnNextBoot) {
let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
tracing::debug!("Writing {DESTRUCTIVE_CLEANUP}");
sysroot_dir.atomic_write(format!("etc/{}", DESTRUCTIVE_CLEANUP), b"")?;
}
// We must drop the sysroot here in order to close any open file
// descriptors.
};
// Run this on every install as the penultimate step
install_finalize(&rootfs.physical_root_path).await?;
Ok(())
}
async fn install_to_filesystem_impl(
state: &State,
rootfs: &mut RootSetup,
@@ -1463,57 +1510,20 @@ async fn install_to_filesystem_impl(
}
}
// We verify this upfront because it's currently required by bootupd
let boot_uuid = rootfs
.get_boot_uuid()?
.or(rootfs.rootfs_uuid.as_deref())
.ok_or_else(|| anyhow!("No uuid for boot/root"))?;
tracing::debug!("boot uuid={boot_uuid}");
let bound_images = BoundImages::from_state(state).await?;
#[cfg(feature = "composefs-backend")]
if state.composefs_options.is_some() {
// Load a fd for the mounted target physical root
let (id, verity) = initialize_composefs_repository(state, rootfs).await?;
tracing::info!(
"id = {id}, verity = {verity}",
id = hex::encode(id),
verity = verity.to_hex()
);
tracing::info!("id: {}, verity: {}", hex::encode(id), verity.to_hex());
setup_composefs_boot(rootfs, state, &hex::encode(id))?;
} else {
// Initialize the ostree sysroot (repo, stateroot, etc.)
{
let (sysroot, has_ostree) = initialize_ostree_root(state, rootfs).await?;
install_with_sysroot(
state,
rootfs,
&sysroot,
&boot_uuid,
bound_images,
has_ostree,
)
.await?;
let ostree = sysroot.get_ostree()?;
if matches!(cleanup, Cleanup::TriggerOnNextBoot) {
let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
tracing::debug!("Writing {DESTRUCTIVE_CLEANUP}");
sysroot_dir.atomic_write(format!("etc/{}", DESTRUCTIVE_CLEANUP), b"")?;
}
// We must drop the sysroot here in order to close any open file
// descriptors.
};
// Run this on every install as the penultimate step
install_finalize(&rootfs.physical_root_path).await?;
ostree_install(state, rootfs, cleanup).await?;
}
#[cfg(not(feature = "composefs-backend"))]
ostree_install(state, rootfs, cleanup).await?;
// Finalize mounted filesystems
if !rootfs.skip_finalize {
let bootfs = rootfs.boot.as_ref().map(|_| ("boot", "boot"));
@@ -1533,6 +1543,7 @@ fn installation_complete() {
#[context("Installing to disk")]
#[cfg(feature = "install-to-disk")]
pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
#[cfg(feature = "composefs-backend")]
opts.validate()?;
// Log the disk installation operation to systemd journal
@@ -1576,15 +1587,22 @@ pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
} else if !target_blockdev_meta.file_type().is_block_device() {
anyhow::bail!("Not a block device: {}", block_opts.device);
}
#[cfg(feature = "composefs-backend")]
let composefs_arg = if opts.composefs_native {
Some(opts.composefs_opts)
} else {
None
};
#[cfg(not(feature = "composefs-backend"))]
let composefs_arg = None;
let state = prepare_install(
opts.config_opts,
opts.source_opts,
opts.target_opts,
if opts.composefs_native {
Some(opts.composefs_opts)
} else {
None
},
composefs_arg,
)
.await?;

View File

@@ -4,6 +4,7 @@
//! to provide a fully "container native" tool for using
//! bootable container images.
#[cfg(feature = "composefs-backend")]
mod bootc_composefs;
pub(crate) mod bootc_kargs;
mod bootloader;

View File

@@ -2,6 +2,8 @@
//!
//! This module parses the config files for the spec.
#![allow(dead_code)]
use anyhow::{anyhow, Result};
use std::collections::HashMap;
use std::fmt::Display;

View File

@@ -1,5 +1,7 @@
//! Parser for GRUB menuentry configuration files using nom combinators.
#![allow(dead_code)]
use std::fmt::Display;
use nom::{

View File

@@ -11,6 +11,7 @@ use ostree_ext::{container::OstreeImageReference, oci_spec};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[cfg(feature = "composefs-backend")]
use crate::bootc_composefs::boot::BootType;
use crate::{k8sapitypes, status::Slot};
@@ -198,6 +199,7 @@ impl FromStr for Bootloader {
/// A bootable entry
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[cfg(feature = "composefs-backend")]
pub struct BootEntryComposefs {
/// The erofs verity
pub verity: String,
@@ -228,6 +230,7 @@ pub struct BootEntry {
/// If this boot entry is ostree based, the corresponding state
pub ostree: Option<BootEntryOstree>,
/// If this boot entry is composefs based, the corresponding state
#[cfg(feature = "composefs-backend")]
pub composefs: Option<BootEntryComposefs>,
}
@@ -300,6 +303,7 @@ impl Host {
}
}
#[cfg(feature = "composefs-backend")]
pub(crate) fn require_composefs_booted(&self) -> anyhow::Result<&BootEntryComposefs> {
let cfs = self
.status
@@ -582,6 +586,7 @@ mod tests {
pinned: false,
store: None,
ostree: None,
#[cfg(feature = "composefs-backend")]
composefs: None,
}
}

View File

@@ -3,10 +3,8 @@ use std::collections::VecDeque;
use std::io::IsTerminal;
use std::io::Read;
use std::io::Write;
use std::sync::OnceLock;
use anyhow::{Context, Result};
use bootc_kernel_cmdline::Cmdline;
use canon_json::CanonJsonSerialize;
use fn_error_context::context;
use ostree::glib;
@@ -21,9 +19,9 @@ use ostree_ext::sysroot::SysrootLock;
use ostree_ext::ostree;
use crate::bootc_composefs::status::composefs_deployment_status;
#[cfg(feature = "composefs-backend")]
use crate::bootc_composefs::status::{composefs_booted, composefs_deployment_status};
use crate::cli::OutputFormat;
use crate::composefs_consts::COMPOSEFS_CMDLINE;
use crate::spec::ImageStatus;
use crate::spec::{BootEntry, BootOrder, Host, HostSpec, HostStatus, HostType};
use crate::spec::{ImageReference, ImageSignature};
@@ -51,49 +49,6 @@ impl From<ImageSignature> for ostree_container::SignatureSource {
}
}
/// A parsed composefs command line
pub(crate) struct ComposefsCmdline {
#[allow(dead_code)]
pub insecure: bool,
pub digest: Box<str>,
}
impl ComposefsCmdline {
pub(crate) fn new(s: &str) -> Self {
let (insecure, digest_str) = s
.strip_prefix('?')
.map(|v| (true, v))
.unwrap_or_else(|| (false, s));
ComposefsCmdline {
insecure,
digest: digest_str.into(),
}
}
}
impl std::fmt::Display for ComposefsCmdline {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let insecure = if self.insecure { "?" } else { "" };
write!(f, "{}={}{}", COMPOSEFS_CMDLINE, insecure, self.digest)
}
}
/// Detect if we have composefs=<digest> in /proc/cmdline
pub(crate) fn composefs_booted() -> Result<Option<&'static ComposefsCmdline>> {
static CACHED_DIGEST_VALUE: OnceLock<Option<ComposefsCmdline>> = OnceLock::new();
if let Some(v) = CACHED_DIGEST_VALUE.get() {
return Ok(v.as_ref());
}
let cmdline = Cmdline::from_proc()?;
let Some(kv) = cmdline.find_str(COMPOSEFS_CMDLINE) else {
return Ok(None);
};
let Some(v) = kv.value else { return Ok(None) };
let v = ComposefsCmdline::new(v);
let r = CACHED_DIGEST_VALUE.get_or_init(|| Some(v));
Ok(r.as_ref())
}
/// Fixme lower serializability into ostree-ext
fn transport_to_string(transport: ostree_container::Transport) -> String {
match transport {
@@ -255,6 +210,7 @@ fn boot_entry_from_deployment(
deploy_serial: deployment.deployserial().try_into().unwrap(),
stateroot: deployment.stateroot().into(),
}),
#[cfg(feature = "composefs-backend")]
composefs: None,
};
Ok(r)
@@ -384,15 +340,9 @@ pub(crate) fn get_status(
Ok((deployments, host))
}
/// Implementation of the `bootc status` CLI command.
#[context("Status")]
pub(crate) async fn status(opts: super::cli::StatusOpts) -> Result<()> {
match opts.format_version.unwrap_or_default() {
// For historical reasons, both 0 and 1 mean "v1".
0 | 1 => {}
o => anyhow::bail!("Unsupported format version: {o}"),
};
let mut host = if ostree_booted()? {
#[cfg(feature = "composefs-backend")]
async fn get_host() -> Result<Host> {
let host = if ostree_booted()? {
let sysroot = super::cli::get_storage().await?;
let ostree = sysroot.get_ostree()?;
let booted_deployment = ostree.booted_deployment();
@@ -404,6 +354,34 @@ pub(crate) async fn status(opts: super::cli::StatusOpts) -> Result<()> {
Default::default()
};
Ok(host)
}
#[cfg(not(feature = "composefs-backend"))]
async fn get_host() -> Result<Host> {
let host = if ostree_booted()? {
let sysroot = super::cli::get_storage().await?;
let ostree = sysroot.get_ostree()?;
let booted_deployment = ostree.booted_deployment();
let (_deployments, host) = get_status(&ostree, booted_deployment.as_ref())?;
host
} else {
Default::default()
};
Ok(host)
}
/// Implementation of the `bootc status` CLI command.
#[context("Status")]
pub(crate) async fn status(opts: super::cli::StatusOpts) -> Result<()> {
match opts.format_version.unwrap_or_default() {
// For historical reasons, both 0 and 1 mean "v1".
0 | 1 => {}
o => anyhow::bail!("Unsupported format version: {o}"),
};
let mut host = get_host().await?;
// We could support querying the staged or rollback deployments
// here too, but it's not a common use case at the moment.
if opts.booted {
@@ -537,6 +515,7 @@ fn human_render_slot(
writeln!(out, "{digest} ({arch})")?;
// Write the EROFS verity if present
#[cfg(feature = "composefs-backend")]
if let Some(composefs) = &entry.composefs {
write_row_name(&mut out, "Verity", prefix_len)?;
writeln!(out, "{}", composefs.verity)?;
@@ -643,6 +622,7 @@ fn human_render_slot_ostree(
}
/// Output a rendering of a non-container composefs boot entry.
#[cfg(feature = "composefs-backend")]
fn human_render_slot_composefs(
mut out: impl Write,
slot: Slot,
@@ -676,6 +656,8 @@ fn human_readable_output_booted(mut out: impl Write, host: &Host, verbose: bool)
} else {
writeln!(out)?;
}
#[cfg(feature = "composefs-backend")]
if let Some(image) = &host_status.image {
human_render_slot(&mut out, Some(slot_name), host_status, image, verbose)?;
} else if let Some(ostree) = host_status.ostree.as_ref() {
@@ -691,6 +673,21 @@ fn human_readable_output_booted(mut out: impl Write, host: &Host, verbose: bool)
} else {
writeln!(out, "Current {slot_name} state is unknown")?;
}
#[cfg(not(feature = "composefs-backend"))]
if let Some(image) = &host_status.image {
human_render_slot(&mut out, Some(slot_name), host_status, image, verbose)?;
} else if let Some(ostree) = host_status.ostree.as_ref() {
human_render_slot_ostree(
&mut out,
Some(slot_name),
host_status,
&ostree.checksum,
verbose,
)?;
} else {
writeln!(out, "Current {slot_name} state is unknown")?;
}
}
}
@@ -882,15 +879,4 @@ mod tests {
assert!(w.contains("Commit:"));
assert!(w.contains("Soft-reboot:"));
}
#[test]
fn test_composefs_parsing() {
const DIGEST: &str = "8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52";
let v = ComposefsCmdline::new(DIGEST);
assert!(!v.insecure);
assert_eq!(v.digest.as_ref(), DIGEST);
let v = ComposefsCmdline::new(&format!("?{}", DIGEST));
assert!(v.insecure);
assert_eq!(v.digest.as_ref(), DIGEST);
}
}

View File

@@ -1,9 +1,10 @@
use std::future::Future;
use std::io::Write;
use std::os::fd::BorrowedFd;
use std::path::{Path, PathBuf};
#[cfg(feature = "composefs-backend")]
use std::path::{Component, Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use std::{future::Future, path::Component};
use anyhow::{Context, Result};
use bootc_utils::CommandRunExt;
@@ -190,6 +191,7 @@ pub(crate) fn digested_pullspec(image: &str, digest: &str) -> String {
/// Computes a relative path from `from` to `to`.
///
/// Both `from` and `to` must be absolute paths.
#[cfg(feature = "composefs-backend")]
pub(crate) fn path_relative_to(from: &Path, to: &Path) -> Result<PathBuf> {
if !from.is_absolute() || !to.is_absolute() {
anyhow::bail!("Paths must be absolute");
@@ -248,6 +250,7 @@ mod tests {
}
#[test]
#[cfg(feature = "composefs-backend")]
fn test_relative_path() {
let from = Path::new("/sysroot/state/deploy/image_id");
let to = Path::new("/sysroot/state/os/default/var");