diff --git a/src/commands/ext/build.rs b/src/commands/ext/build.rs index 2da6214..18b2965 100644 --- a/src/commands/ext/build.rs +++ b/src/commands/ext/build.rs @@ -21,6 +21,8 @@ use crate::utils::tui::{TaskId, TuiGuard}; struct OverlayConfig { dir: String, mode: OverlayMode, + /// Opt-in `{{ }}` preprocessing of overlay file contents. + preprocess: crate::utils::overlay_preprocess::PreprocessSpec, } #[derive(Debug, Clone, PartialEq)] @@ -554,12 +556,14 @@ impl ExtBuildCommand { })?; // Get overlay configuration - let overlay_config = ext_config.get("overlay").map(|v| { + let mut overlay_config = ext_config.get("overlay").map(|v| { + use crate::utils::overlay_preprocess::PreprocessSpec; if let Some(dir_str) = v.as_str() { // Simple string format: overlay = "directory" OverlayConfig { dir: dir_str.to_string(), mode: OverlayMode::Merge, // Default to merge mode + preprocess: PreprocessSpec::None, } } else if let Some(table) = v.as_mapping() { // Table format: overlay = {dir = "directory", mode = "opaque"} @@ -574,12 +578,17 @@ impl ExtBuildCommand { _ => OverlayMode::Merge, // Default to merge mode }; - OverlayConfig { dir, mode } + OverlayConfig { + dir, + mode, + preprocess: PreprocessSpec::from_overlay_value(v), + } } else { // Fallback for invalid format OverlayConfig { dir: "overlay".to_string(), mode: OverlayMode::Merge, + preprocess: PreprocessSpec::None, } } }); @@ -616,6 +625,58 @@ impl ExtBuildCommand { ExtensionLocation::Local { .. } => "/opt/src".to_string(), }; + // Opt-in overlay preprocessing (local extensions only). Materialize an + // interpolated copy of the overlay on the host under + // `.avocado/overlay-staging/` and redirect the in-container copy to it, + // so `{{ ... }}` in overlay files is substituted without mutating the + // working tree. Remote-extension overlays live in the SDK volume (not on + // the host), so they can't be preprocessed here — warn and skip. + if let Some(oc) = overlay_config.as_mut() { + if oc.preprocess.is_enabled() { + match &extension_location { + ExtensionLocation::Local { .. } => { + let project_root = config.project_root(&self.config_path); + let context = crate::utils::interpolation::AvocadoContext::from_main_config( + parsed, + Some(target.as_str()), + ); + let label = format!( + "ext-{}", + self.extension + .chars() + .map(|c| if c.is_alphanumeric() || c == '-' || c == '_' { + c + } else { + '_' + }) + .collect::() + ); + if let Some(staging_rel_dir) = + crate::utils::overlay_preprocess::materialize_preprocessed_overlay( + &project_root, + &oc.dir, + &label, + &oc.preprocess, + parsed, + &context, + )? + { + oc.dir = staging_rel_dir; + } + } + ExtensionLocation::Remote { name, .. } => { + print_warning( + &format!( + "Overlay preprocessing is not supported for remote extension \ + '{name}'; copying overlay verbatim." + ), + OutputLevel::Normal, + ); + } + } + } + } + // Run the post_build hook before sealing any .raw images. By this // point the ext sysroot has been populated by `avocado install` and // any compile-dep install scripts above, so the hook can freely @@ -1913,6 +1974,7 @@ mod tests { let overlay_config = OverlayConfig { dir: "peridio".to_string(), mode: OverlayMode::Merge, + preprocess: crate::utils::overlay_preprocess::PreprocessSpec::None, }; let script = cmd.create_sysext_build_script( "1.0", @@ -1955,6 +2017,7 @@ mod tests { let overlay_config = OverlayConfig { dir: "peridio".to_string(), mode: OverlayMode::Merge, + preprocess: crate::utils::overlay_preprocess::PreprocessSpec::None, }; let script = cmd.create_confext_build_script( "1.0", @@ -1997,6 +2060,7 @@ mod tests { let overlay_config = OverlayConfig { dir: "peridio".to_string(), mode: OverlayMode::Opaque, + preprocess: crate::utils::overlay_preprocess::PreprocessSpec::None, }; let script = cmd.create_sysext_build_script( "1.0", @@ -2040,6 +2104,7 @@ mod tests { let overlay_config = OverlayConfig { dir: "peridio".to_string(), mode: OverlayMode::Opaque, + preprocess: crate::utils::overlay_preprocess::PreprocessSpec::None, }; let script = cmd.create_confext_build_script( "1.0", diff --git a/src/commands/rootfs/install.rs b/src/commands/rootfs/install.rs index cb21bdb..76d5f24 100644 --- a/src/commands/rootfs/install.rs +++ b/src/commands/rootfs/install.rs @@ -656,21 +656,46 @@ pub async fn install_sysroot(params: &mut SysrootInstallParams<'_>) -> Result<() // Build optional overlay snippet — appended to the install command so it // runs in the same container invocation immediately after DNF finishes. - let overlay_snippet = params - .parsed - .and_then(|parsed| { + let overlay_snippet = { + let overlay_value = params.parsed.and_then(|parsed| { let key = match params.sysroot_type { SysrootType::Rootfs => "rootfs", SysrootType::Initramfs => "initramfs", _ => return None, }; - parsed.get(key)?.get("overlay") - }) - .map(|v| { - let (dir, opaque) = parse_overlay_config(v); - build_overlay_script(&dir, opaque, sysroot_dir) - }) - .unwrap_or_default(); + parsed.get(key)?.get("overlay").cloned() + }); + if let (Some(v), Some(parsed)) = (overlay_value, params.parsed) { + let (dir, opaque) = parse_overlay_config(&v); + // Opt-in preprocessing: materialize an interpolated copy of the + // overlay on the host under `.avocado/overlay-staging/` and copy + // from there, so `{{ ... }}` in overlay files is substituted without + // mutating the working tree. + let spec = crate::utils::overlay_preprocess::PreprocessSpec::from_overlay_value(&v); + let effective_dir = if spec.is_enabled() { + let context = crate::utils::interpolation::AvocadoContext::from_main_config( + parsed, + Some(params.target), + ); + match crate::utils::overlay_preprocess::materialize_preprocessed_overlay( + params.src_dir, + &dir, + sysroot_dir, + &spec, + parsed, + &context, + )? { + Some(staging_rel_dir) => staging_rel_dir, + None => dir, + } + } else { + dir + }; + build_overlay_script(&effective_dir, opaque, sysroot_dir) + } else { + String::new() + } + }; let exclude_str = if off_kernel_excludes.is_empty() { String::new() diff --git a/src/utils/interpolation/mod.rs b/src/utils/interpolation/mod.rs index e555169..899345c 100644 --- a/src/utils/interpolation/mod.rs +++ b/src/utils/interpolation/mod.rs @@ -194,6 +194,29 @@ pub fn interpolate_config_with_context( Ok(()) } +/// Preprocess `{{ ... }}` templates inside arbitrary file contents (e.g. an +/// overlay file), using the same `env.`/`config.`/`avocado.` resolution as the +/// YAML interpolator. +/// +/// `root` is the composed config tree (for `{{ config.* }}`); `context` carries +/// the resolved target/board/distro values (for `{{ avocado.* }}`). Missing +/// `env.*` variables warn and resolve to an empty string, identical to +/// `avocado.yaml` interpolation. Input that contains no templates is returned +/// unchanged. +pub fn preprocess_text(input: &str, root: &Value, context: &AvocadoContext) -> Result { + let mut resolving_stack = HashSet::new(); + let path: Vec = Vec::new(); + Ok(interpolate_string( + input, + root, + context, + &mut resolving_stack, + &path, + &YamlLocation::Value, + )? + .unwrap_or_else(|| input.to_string())) +} + /// Represents where in the YAML structure a template was found. #[derive(Clone, Debug)] enum YamlLocation { diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 13e41cb..221a8ed 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -16,6 +16,7 @@ pub mod lockfile; pub mod nfs_server; pub mod output; pub mod output_format; +pub mod overlay_preprocess; pub mod permissions; pub mod pkcs11_devices; pub mod prerequisites; diff --git a/src/utils/overlay_preprocess.rs b/src/utils/overlay_preprocess.rs new file mode 100644 index 0000000..b98a651 --- /dev/null +++ b/src/utils/overlay_preprocess.rs @@ -0,0 +1,480 @@ +//! Host-side materialization of preprocessed overlay trees. +//! +//! Overlay files are normally copied verbatim into the rootfs / initramfs / +//! extension sysroot by a shell `cp` that runs inside the SDK container, +//! reading the project tree bind-mounted at `/opt/src`. When an overlay opts +//! into preprocessing (`overlay: { dir, preprocess: ... }`), we must apply +//! `{{ ... }}` substitution to file contents *without* mutating the user's +//! working tree. To do that we materialize a processed copy on the host into a +//! scratch dir under the project root (`.avocado/overlay-staging/