diff --git a/src/commands/ext/build.rs b/src/commands/ext/build.rs index effface..e80509c 100644 --- a/src/commands/ext/build.rs +++ b/src/commands/ext/build.rs @@ -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//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() { diff --git a/src/commands/ext/image.rs b/src/commands/ext/image.rs index d16075c..fee40f1 100644 --- a/src/commands/ext/image.rs +++ b/src/commands/ext/image.rs @@ -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//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() { diff --git a/src/commands/runtime/provision.rs b/src/commands/runtime/provision.rs index 8b68115..a9ae8b3 100644 --- a/src/commands/runtime/provision.rs +++ b/src/commands/runtime/provision.rs @@ -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//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 @@ -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//` 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( diff --git a/src/utils/config.rs b/src/utils/config.rs index 401d852..677b846 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -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 @@ -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() { @@ -11390,6 +11409,61 @@ rootfs: ); } + #[test] + fn test_image_section_path_source_resolves_relative_to_src_dir() { + // Main config in a subdir (runtimes//) 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();