Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions src/commands/ext/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,21 @@ impl ExtBuildCommand {
// `avocado install`/`runtime install`. The lockfile is the host-
// side source of truth here — the actual sysroots live in the
// docker volume and aren't directly inspectable from the host.
let lock_src_dir = std::path::Path::new(&self.config_path)
.parent()
.unwrap_or_else(|| std::path::Path::new("."));
let lock_file = LockFile::load(lock_src_dir)
// Resolve the lockfile against the same `src_dir` that
// `install`/`runtime install` use to *write* it (see
// `get_resolved_src_dir`), not the config file's directory. With a
// per-board runtime laid out as `runtimes/<board>/avocado.yaml` +
// `src_dir: ../..`, the lock lives at the repo root, not next to
// the config — so `config_path.parent()` would miss it.
let lock_src_dir = config
.get_resolved_src_dir(&self.config_path)
.unwrap_or_else(|| {
std::path::Path::new(&self.config_path)
.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.to_path_buf()
});
let lock_file = LockFile::load(&lock_src_dir)
.with_context(|| "Failed to load lock file for sysroot precondition check")?;
if let Some(target_locks) = lock_file.targets.get(&target) {
if target_locks.rootfs.is_empty() {
Expand Down
19 changes: 15 additions & 4 deletions src/commands/ext/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,21 @@ impl ExtImageCommand {
));
}

let lock_src_dir = std::path::Path::new(&self.config_path)
.parent()
.unwrap_or_else(|| std::path::Path::new("."));
let lock_file = LockFile::load(lock_src_dir)
// Resolve the lockfile against the same `src_dir` that
// `install`/`runtime install` use to *write* it (see
// `get_resolved_src_dir`), not the config file's directory. With a
// per-board runtime laid out as `runtimes/<board>/avocado.yaml` +
// `src_dir: ../..`, the lock lives at the repo root, not next to
// the config — so `config_path.parent()` would miss it.
let lock_src_dir = config
.get_resolved_src_dir(&self.config_path)
.unwrap_or_else(|| {
std::path::Path::new(&self.config_path)
.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.to_path_buf()
});
let lock_file = LockFile::load(&lock_src_dir)
.with_context(|| "Failed to load lock file for sysroot precondition check")?;
if let Some(target_locks) = lock_file.targets.get(&target) {
if target_locks.rootfs.is_empty() {
Expand Down
19 changes: 16 additions & 3 deletions src/commands/runtime/provision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,14 @@ impl RuntimeProvisionCommand {
// Surface the container-side path so provision scripts (e.g.
// stone-provision-tegraflash.sh) can repack boot.img with the resolver-
// pinned kernel instead of relying on the build-baked one.
if let Ok(src_dir) = std::env::current_dir() {
// Resolve against the configured `src_dir` (where install wrote the
// lock + docker volume), falling back to the CWD. A per-board runtime
// (`runtimes/<board>/avocado.yaml` + `src_dir: ../..`) is provisioned
// from the board dir, so the CWD is not the src_dir.
let lock_src_dir = config
.get_resolved_src_dir(&self.config.config_path)
.or_else(|| std::env::current_dir().ok());
if let Some(src_dir) = lock_src_dir {
if let Ok(lock_file) = LockFile::load(&src_dir) {
// Validate kernel consistency before provision. Any drift between
// rootfs/initramfs kvers, or a pinned kver without a populated
Expand Down Expand Up @@ -410,8 +417,14 @@ impl RuntimeProvisionCommand {
Some(env_vars)
};

// Copy state file to container volume if it exists
let src_dir = std::env::current_dir()?;
// Copy state file to container volume if it exists. Use the same
// resolved `src_dir` as the docker volume / lockfile (not the CWD), so
// a per-board runtime provisioned from `runtimes/<board>/` reads the
// state file from — and attaches the volume keyed at — the repo root.
let src_dir = match config.get_resolved_src_dir(&self.config.config_path) {
Some(dir) => dir,
None => std::env::current_dir()?,
};
let state_file_existed =
if let Some((ref state_file_path, ref container_state_path)) = state_file_info {
self.copy_state_to_container(
Expand Down
78 changes: 76 additions & 2 deletions src/utils/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1713,7 +1713,26 @@ impl Config {
main_config: &mut serde_yaml::Value,
config_path: &Path,
) -> Result<()> {
let main_config_dir = config_path.parent().unwrap_or(Path::new(".")).to_path_buf();
let config_dir = config_path.parent().unwrap_or(Path::new(".")).to_path_buf();

// Resolve path sources relative to `src_dir` when set (falling back to
// the config file's directory), so rootfs/initramfs/kernel `source.path`
// use the SAME base as extension `source: { type: path }` sources (see
// ext_fetch::ExtensionPathState). Without this they resolve against the
// config dir, which breaks configs that live in a subdirectory whose
// `src_dir` points at a parent (e.g. one runtime per directory).
let base_dir = match main_config.get("src_dir").and_then(|v| v.as_str()) {
Some(s) => {
let p = Path::new(s);
if p.is_absolute() {
p.to_path_buf()
} else {
let joined = config_dir.join(p);
joined.canonicalize().unwrap_or(joined)
}
}
None => config_dir,
};

for key in ["rootfs", "initramfs", "kernel"] {
// Read the source.path declaration; skip the section entirely
Expand Down Expand Up @@ -1746,7 +1765,7 @@ impl Config {
};

// Resolve the fragment file (.yaml preferred, .yml fallback).
let fragment_dir = main_config_dir.join(&path_str);
let fragment_dir = base_dir.join(&path_str);
let fragment_yaml = fragment_dir.join("avocado.yaml");
let fragment_yml = fragment_dir.join("avocado.yml");
let fragment_path = if fragment_yaml.is_file() {
Expand Down Expand Up @@ -11390,6 +11409,61 @@ rootfs:
);
}

#[test]
fn test_image_section_path_source_resolves_relative_to_src_dir() {
// Main config in a subdir (runtimes/<board>/) with `src_dir: ../..`
// pointing at the repo root, where os-kabs/ lives. The rootfs
// source.path must resolve against src_dir (root), NOT the config dir
// — matching how extension `source: { type: path }` paths resolve.
let tmp = tempfile::TempDir::new().unwrap();
let root = tmp.path();

let board_dir = root.join("runtimes").join("flex");
std::fs::create_dir_all(&board_dir).unwrap();
let main_yaml = r#"
src_dir: ../..
default_target: raspberrypi4

sdk:
image: test-image

runtimes:
kos-bootloader-flex:
type: kos

rootfs:
source:
type: path
path: os-kabs/avocado-os-rootfs
"#;
let main_path = board_dir.join("avocado.yaml");
std::fs::write(&main_path, main_yaml).unwrap();

// Fragment at the repo root (== src_dir), NOT under the board dir.
let fragment_dir = root.join("os-kabs").join("avocado-os-rootfs");
std::fs::create_dir_all(&fragment_dir).unwrap();
let fragment_yaml = r#"
rootfs:
filesystem: erofs-lz4
packages:
avocado-pkg-rootfs: '*'
image:
type: kab
args: '-b -t kos.layer.basefs -v 2024.1.0 --tag raspberrypi4'
"#;
std::fs::write(fragment_dir.join("avocado.yaml"), fragment_yaml).unwrap();

let composed = Config::load_composed(&main_path, Some("raspberrypi4")).unwrap();
assert!(
composed
.config
.get_rootfs_packages()
.contains_key("avocado-pkg-rootfs"),
"rootfs source.path must resolve relative to src_dir (repo root), not the config dir"
);
assert_eq!(composed.config.get_rootfs_filesystem(), "erofs-lz4");
}

#[test]
fn test_image_section_path_source_missing_dir_errors() {
let tmp = tempfile::TempDir::new().unwrap();
Expand Down
Loading