Skip to content

Commit 6151a33

Browse files
overlay: opt-in build-time preprocessing of overlay files
Add `overlay: { preprocess: [globs] | true }` to run the existing {{ env. }} / {{ config. }} / {{ avocado. }} interpolation over overlay file *contents* at build time. This lets projects template a secret (such as a claim token) into overlay files without an in-tree sed + restore dance that risks leaving secrets in the working tree. Overlay files are copied verbatim inside the SDK container, so a processed copy is materialized on the host into `.avocado/overlay-staging/<label>/` (gitignored scratch, inside the /opt/src bind mount) and the copy is pointed there. Binary (non-UTF-8) files pass through verbatim; symlinks and mode bits are preserved. Default off = byte-identical to today. - interpolation: add preprocess_text (thin wrapper over interpolate_string) - new utils/overlay_preprocess: PreprocessSpec, glob matcher, materializer, and a deterministic post-preprocess content digest - wire into rootfs/initramfs (rootfs/install.rs) and sysext/confext (ext/build.rs) copy sites; local overlays only (remote-ext overlays warn+skip) - stamps: fold the post-preprocess content digest into rootfs/initramfs/ext input hashes when preprocess is on, so a changed template value or an edited overlay file invalidates the build (verbatim overlays unchanged)
1 parent 849458f commit 6151a33

6 files changed

Lines changed: 743 additions & 16 deletions

File tree

src/commands/ext/build.rs

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use crate::utils::tui::{TaskId, TuiGuard};
2121
struct OverlayConfig {
2222
dir: String,
2323
mode: OverlayMode,
24+
/// Opt-in `{{ }}` preprocessing of overlay file contents.
25+
preprocess: crate::utils::overlay_preprocess::PreprocessSpec,
2426
}
2527

2628
#[derive(Debug, Clone, PartialEq)]
@@ -554,12 +556,14 @@ impl ExtBuildCommand {
554556
})?;
555557

556558
// Get overlay configuration
557-
let overlay_config = ext_config.get("overlay").map(|v| {
559+
let mut overlay_config = ext_config.get("overlay").map(|v| {
560+
use crate::utils::overlay_preprocess::PreprocessSpec;
558561
if let Some(dir_str) = v.as_str() {
559562
// Simple string format: overlay = "directory"
560563
OverlayConfig {
561564
dir: dir_str.to_string(),
562565
mode: OverlayMode::Merge, // Default to merge mode
566+
preprocess: PreprocessSpec::None,
563567
}
564568
} else if let Some(table) = v.as_mapping() {
565569
// Table format: overlay = {dir = "directory", mode = "opaque"}
@@ -574,12 +578,17 @@ impl ExtBuildCommand {
574578
_ => OverlayMode::Merge, // Default to merge mode
575579
};
576580

577-
OverlayConfig { dir, mode }
581+
OverlayConfig {
582+
dir,
583+
mode,
584+
preprocess: PreprocessSpec::from_overlay_value(v),
585+
}
578586
} else {
579587
// Fallback for invalid format
580588
OverlayConfig {
581589
dir: "overlay".to_string(),
582590
mode: OverlayMode::Merge,
591+
preprocess: PreprocessSpec::None,
583592
}
584593
}
585594
});
@@ -616,6 +625,58 @@ impl ExtBuildCommand {
616625
ExtensionLocation::Local { .. } => "/opt/src".to_string(),
617626
};
618627

628+
// Opt-in overlay preprocessing (local extensions only). Materialize an
629+
// interpolated copy of the overlay on the host under
630+
// `.avocado/overlay-staging/` and redirect the in-container copy to it,
631+
// so `{{ ... }}` in overlay files is substituted without mutating the
632+
// working tree. Remote-extension overlays live in the SDK volume (not on
633+
// the host), so they can't be preprocessed here — warn and skip.
634+
if let Some(oc) = overlay_config.as_mut() {
635+
if oc.preprocess.is_enabled() {
636+
match &extension_location {
637+
ExtensionLocation::Local { .. } => {
638+
let project_root = config.project_root(&self.config_path);
639+
let context = crate::utils::interpolation::AvocadoContext::from_main_config(
640+
parsed,
641+
Some(target.as_str()),
642+
);
643+
let label = format!(
644+
"ext-{}",
645+
self.extension
646+
.chars()
647+
.map(|c| if c.is_alphanumeric() || c == '-' || c == '_' {
648+
c
649+
} else {
650+
'_'
651+
})
652+
.collect::<String>()
653+
);
654+
if let Some(staging_rel_dir) =
655+
crate::utils::overlay_preprocess::materialize_preprocessed_overlay(
656+
&project_root,
657+
&oc.dir,
658+
&label,
659+
&oc.preprocess,
660+
parsed,
661+
&context,
662+
)?
663+
{
664+
oc.dir = staging_rel_dir;
665+
}
666+
}
667+
ExtensionLocation::Remote { name, .. } => {
668+
print_warning(
669+
&format!(
670+
"Overlay preprocessing is not supported for remote extension \
671+
'{name}'; copying overlay verbatim."
672+
),
673+
OutputLevel::Normal,
674+
);
675+
}
676+
}
677+
}
678+
}
679+
619680
// Run the post_build hook before sealing any .raw images. By this
620681
// point the ext sysroot has been populated by `avocado install` and
621682
// any compile-dep install scripts above, so the hook can freely
@@ -1913,6 +1974,7 @@ mod tests {
19131974
let overlay_config = OverlayConfig {
19141975
dir: "peridio".to_string(),
19151976
mode: OverlayMode::Merge,
1977+
preprocess: crate::utils::overlay_preprocess::PreprocessSpec::None,
19161978
};
19171979
let script = cmd.create_sysext_build_script(
19181980
"1.0",
@@ -1955,6 +2017,7 @@ mod tests {
19552017
let overlay_config = OverlayConfig {
19562018
dir: "peridio".to_string(),
19572019
mode: OverlayMode::Merge,
2020+
preprocess: crate::utils::overlay_preprocess::PreprocessSpec::None,
19582021
};
19592022
let script = cmd.create_confext_build_script(
19602023
"1.0",
@@ -1997,6 +2060,7 @@ mod tests {
19972060
let overlay_config = OverlayConfig {
19982061
dir: "peridio".to_string(),
19992062
mode: OverlayMode::Opaque,
2063+
preprocess: crate::utils::overlay_preprocess::PreprocessSpec::None,
20002064
};
20012065
let script = cmd.create_sysext_build_script(
20022066
"1.0",
@@ -2040,6 +2104,7 @@ mod tests {
20402104
let overlay_config = OverlayConfig {
20412105
dir: "peridio".to_string(),
20422106
mode: OverlayMode::Opaque,
2107+
preprocess: crate::utils::overlay_preprocess::PreprocessSpec::None,
20432108
};
20442109
let script = cmd.create_confext_build_script(
20452110
"1.0",

src/commands/rootfs/install.rs

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -656,21 +656,46 @@ pub async fn install_sysroot(params: &mut SysrootInstallParams<'_>) -> Result<()
656656

657657
// Build optional overlay snippet — appended to the install command so it
658658
// runs in the same container invocation immediately after DNF finishes.
659-
let overlay_snippet = params
660-
.parsed
661-
.and_then(|parsed| {
659+
let overlay_snippet = {
660+
let overlay_value = params.parsed.and_then(|parsed| {
662661
let key = match params.sysroot_type {
663662
SysrootType::Rootfs => "rootfs",
664663
SysrootType::Initramfs => "initramfs",
665664
_ => return None,
666665
};
667-
parsed.get(key)?.get("overlay")
668-
})
669-
.map(|v| {
670-
let (dir, opaque) = parse_overlay_config(v);
671-
build_overlay_script(&dir, opaque, sysroot_dir)
672-
})
673-
.unwrap_or_default();
666+
parsed.get(key)?.get("overlay").cloned()
667+
});
668+
if let (Some(v), Some(parsed)) = (overlay_value, params.parsed) {
669+
let (dir, opaque) = parse_overlay_config(&v);
670+
// Opt-in preprocessing: materialize an interpolated copy of the
671+
// overlay on the host under `.avocado/overlay-staging/` and copy
672+
// from there, so `{{ ... }}` in overlay files is substituted without
673+
// mutating the working tree.
674+
let spec = crate::utils::overlay_preprocess::PreprocessSpec::from_overlay_value(&v);
675+
let effective_dir = if spec.is_enabled() {
676+
let context = crate::utils::interpolation::AvocadoContext::from_main_config(
677+
parsed,
678+
Some(params.target),
679+
);
680+
match crate::utils::overlay_preprocess::materialize_preprocessed_overlay(
681+
params.src_dir,
682+
&dir,
683+
sysroot_dir,
684+
&spec,
685+
parsed,
686+
&context,
687+
)? {
688+
Some(staging_rel_dir) => staging_rel_dir,
689+
None => dir,
690+
}
691+
} else {
692+
dir
693+
};
694+
build_overlay_script(&effective_dir, opaque, sysroot_dir)
695+
} else {
696+
String::new()
697+
}
698+
};
674699

675700
let exclude_str = if off_kernel_excludes.is_empty() {
676701
String::new()

src/utils/interpolation/mod.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,29 @@ pub fn interpolate_config_with_context(
194194
Ok(())
195195
}
196196

197+
/// Preprocess `{{ ... }}` templates inside arbitrary file contents (e.g. an
198+
/// overlay file), using the same `env.`/`config.`/`avocado.` resolution as the
199+
/// YAML interpolator.
200+
///
201+
/// `root` is the composed config tree (for `{{ config.* }}`); `context` carries
202+
/// the resolved target/board/distro values (for `{{ avocado.* }}`). Missing
203+
/// `env.*` variables warn and resolve to an empty string, identical to
204+
/// `avocado.yaml` interpolation. Input that contains no templates is returned
205+
/// unchanged.
206+
pub fn preprocess_text(input: &str, root: &Value, context: &AvocadoContext) -> Result<String> {
207+
let mut resolving_stack = HashSet::new();
208+
let path: Vec<String> = Vec::new();
209+
Ok(interpolate_string(
210+
input,
211+
root,
212+
context,
213+
&mut resolving_stack,
214+
&path,
215+
&YamlLocation::Value,
216+
)?
217+
.unwrap_or_else(|| input.to_string()))
218+
}
219+
197220
/// Represents where in the YAML structure a template was found.
198221
#[derive(Clone, Debug)]
199222
enum YamlLocation {

src/utils/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub mod lockfile;
1616
pub mod nfs_server;
1717
pub mod output;
1818
pub mod output_format;
19+
pub mod overlay_preprocess;
1920
pub mod permissions;
2021
pub mod pkcs11_devices;
2122
pub mod prerequisites;

0 commit comments

Comments
 (0)