feat(cli): start the command in the mapped current directory
The default --bind overlay mounted the current directory at /root/<basename>, but the command still started in /root: the library's working-directory fallback only considers read-write binds. Pass the mapped target as the explicit working_dir so relative paths keep meaning what they meant on the host. Bind sources are canonicalized when the targets are built instead of at mount time. This fixes --bind . and other basename-less relative paths, which cli_mount_target rejected, and lets the --bind/--bind-rw dedup match relative and absolute spellings of the same directory.
This commit is contained in:
Generated
+1
@@ -331,6 +331,7 @@ dependencies = [
|
|||||||
"ecr",
|
"ecr",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"log",
|
"log",
|
||||||
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -27,3 +27,7 @@ log = { version = "0.4", features = ["std"] }
|
|||||||
|
|
||||||
# Progress bar rendering of library progress events
|
# Progress bar rendering of library progress events
|
||||||
indicatif = "0.18"
|
indicatif = "0.18"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
# Temp directories for bind-target tests
|
||||||
|
tempfile = "3"
|
||||||
|
|||||||
+149
-10
@@ -112,13 +112,14 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
/// Run in namespace/chroot mode, returning the command's exit code
|
/// Run in namespace/chroot mode, returning the command's exit code
|
||||||
fn namespace_mode(args: &Args, rootfs: &Path, config: &Config, arch: &str) -> Result<i32> {
|
fn namespace_mode(args: &Args, rootfs: &Path, config: &Config, arch: &str) -> Result<i32> {
|
||||||
|
let (binds, working_dir) = cli_bind_targets(args)?;
|
||||||
let opts = ExecOptions {
|
let opts = ExecOptions {
|
||||||
arch: arch.to_string(),
|
arch: arch.to_string(),
|
||||||
binds: cli_bind_targets(args)?,
|
binds,
|
||||||
env: chroot::default_env(rootfs),
|
env: chroot::default_env(rootfs),
|
||||||
dns: config.dns.clone(),
|
dns: config.dns.clone(),
|
||||||
command: args.command.clone(),
|
command: args.command.clone(),
|
||||||
working_dir: None,
|
working_dir,
|
||||||
};
|
};
|
||||||
|
|
||||||
let exit_code = ecr::exec(rootfs, &opts);
|
let exit_code = ecr::exec(rootfs, &opts);
|
||||||
@@ -135,8 +136,15 @@ fn namespace_mode(args: &Args, rootfs: &Path, config: &Config, arch: &str) -> Re
|
|||||||
/// --bind overlays read-only at /root/<basename>, --bind-rw bind-mounts
|
/// --bind overlays read-only at /root/<basename>, --bind-rw bind-mounts
|
||||||
/// read-write at /mnt/<basename> (taking precedence over --bind for the
|
/// read-write at /mnt/<basename> (taking precedence over --bind for the
|
||||||
/// same source path). With no --bind given, the current directory is
|
/// same source path). With no --bind given, the current directory is
|
||||||
/// overlaid by default.
|
/// overlaid by default. Also returns the in-rootfs target where the
|
||||||
fn cli_bind_targets(args: &Args) -> Result<Vec<BindTarget>> {
|
/// current directory is mapped, when it is: the command starts there so
|
||||||
|
/// relative paths keep meaning what they meant on the host.
|
||||||
|
fn cli_bind_targets(args: &Args) -> Result<(Vec<BindTarget>, Option<PathBuf>)> {
|
||||||
|
let cwd = std::env::current_dir().ok();
|
||||||
|
bind_targets(args, cwd.as_deref())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bind_targets(args: &Args, cwd: Option<&Path>) -> Result<(Vec<BindTarget>, Option<PathBuf>)> {
|
||||||
// --no-bind means "skip mounting any directory". Combining it with an
|
// --no-bind means "skip mounting any directory". Combining it with an
|
||||||
// explicit --bind-rw is contradictory; error rather than silently ignoring
|
// explicit --bind-rw is contradictory; error rather than silently ignoring
|
||||||
// the flag the user asked for.
|
// the flag the user asked for.
|
||||||
@@ -147,19 +155,32 @@ fn cli_bind_targets(args: &Args) -> Result<Vec<BindTarget>> {
|
|||||||
--no-bind skips all mounts, including read-write ones"
|
--no-bind skips all mounts, including read-write ones"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return Ok(Vec::new());
|
return Ok((Vec::new(), None));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut binds = Vec::new();
|
let mut binds = Vec::new();
|
||||||
|
|
||||||
|
// Sources are canonicalized here rather than at mount time so that
|
||||||
|
// relative paths ("."), the --bind/--bind-rw dedup and the
|
||||||
|
// current-directory match below all see the same path. A source that
|
||||||
|
// cannot be canonicalized (e.g. missing directory) is kept as typed:
|
||||||
|
// cli_mount_target then fails on basename-less paths, and real paths
|
||||||
|
// fail at mount time with the OS error.
|
||||||
|
let canonical = |path: &Path| path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||||
|
|
||||||
let ro_sources: Vec<PathBuf> = if args.bind.is_empty() {
|
let ro_sources: Vec<PathBuf> = if args.bind.is_empty() {
|
||||||
vec![std::env::current_dir().context("Could not get current directory")?]
|
// current_dir() already returns the canonical path
|
||||||
|
vec![cwd
|
||||||
|
.context("Could not get current directory")?
|
||||||
|
.to_path_buf()]
|
||||||
} else {
|
} else {
|
||||||
args.bind.clone()
|
args.bind.iter().map(|p| canonical(p)).collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let rw_sources: Vec<PathBuf> = args.bind_rw.iter().map(|p| canonical(p)).collect();
|
||||||
|
|
||||||
for source in ro_sources {
|
for source in ro_sources {
|
||||||
if args.bind_rw.contains(&source) {
|
if rw_sources.contains(&source) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
binds.push(BindTarget {
|
binds.push(BindTarget {
|
||||||
@@ -169,7 +190,7 @@ fn cli_bind_targets(args: &Args) -> Result<Vec<BindTarget>> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for source in &args.bind_rw {
|
for source in &rw_sources {
|
||||||
binds.push(BindTarget {
|
binds.push(BindTarget {
|
||||||
target: cli_mount_target(source, "mnt")?,
|
target: cli_mount_target(source, "mnt")?,
|
||||||
source: source.clone(),
|
source: source.clone(),
|
||||||
@@ -177,7 +198,14 @@ fn cli_bind_targets(args: &Args) -> Result<Vec<BindTarget>> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(binds)
|
let working_dir = cwd.map(Path::to_path_buf).and_then(|dir| {
|
||||||
|
binds
|
||||||
|
.iter()
|
||||||
|
.find(|b| b.source == dir)
|
||||||
|
.map(|b| b.target.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok((binds, working_dir))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cli_mount_target(source: &Path, parent: &str) -> Result<PathBuf> {
|
fn cli_mount_target(source: &Path, parent: &str) -> Result<PathBuf> {
|
||||||
@@ -186,3 +214,114 @@ fn cli_mount_target(source: &Path, parent: &str) -> Result<PathBuf> {
|
|||||||
.ok_or_else(|| anyhow::anyhow!("Invalid bind path: {}", source.display()))?;
|
.ok_or_else(|| anyhow::anyhow!("Invalid bind path: {}", source.display()))?;
|
||||||
Ok(Path::new("/").join(parent).join(basename))
|
Ok(Path::new("/").join(parent).join(basename))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn args(bind: Vec<PathBuf>, bind_rw: Vec<PathBuf>, no_bind: bool) -> Args {
|
||||||
|
Args {
|
||||||
|
distro: "ubuntu".into(),
|
||||||
|
arch: None,
|
||||||
|
bind,
|
||||||
|
bind_rw,
|
||||||
|
no_cache: false,
|
||||||
|
no_bind,
|
||||||
|
verbose: false,
|
||||||
|
kernel: None,
|
||||||
|
memory: "2G".into(),
|
||||||
|
command: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_bind_starts_in_mapped_current_directory() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let cwd = dir.path().canonicalize().unwrap();
|
||||||
|
let (_, working_dir) =
|
||||||
|
bind_targets(&args(Vec::new(), Vec::new(), false), Some(&cwd)).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
working_dir,
|
||||||
|
Some(Path::new("/root").join(cwd.file_name().unwrap()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_bind_of_current_directory_starts_there() {
|
||||||
|
// "." must resolve to the current directory the way the mounts
|
||||||
|
// canonicalize the source; relative resolution follows the process
|
||||||
|
// cwd, so this test goes through cli_bind_targets with the process
|
||||||
|
// cwd moved into the temp dir.
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let original = std::env::current_dir().unwrap();
|
||||||
|
std::env::set_current_dir(dir.path()).unwrap();
|
||||||
|
let cwd = std::env::current_dir().unwrap();
|
||||||
|
let result = cli_bind_targets(&args(vec![PathBuf::from(".")], Vec::new(), false));
|
||||||
|
std::env::set_current_dir(original).unwrap();
|
||||||
|
|
||||||
|
let (_, working_dir) = result.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
working_dir,
|
||||||
|
Some(Path::new("/root").join(cwd.file_name().unwrap()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bind_rw_of_current_directory_starts_in_mnt() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let cwd = dir.path().canonicalize().unwrap();
|
||||||
|
let (_, working_dir) =
|
||||||
|
bind_targets(&args(Vec::new(), vec![cwd.clone()], false), Some(&cwd)).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
working_dir,
|
||||||
|
Some(Path::new("/mnt").join(cwd.file_name().unwrap()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bind_rw_takes_precedence_for_the_current_directory() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let cwd = dir.path().canonicalize().unwrap();
|
||||||
|
let (binds, working_dir) = bind_targets(
|
||||||
|
&args(vec![cwd.clone()], vec![cwd.clone()], false),
|
||||||
|
Some(&cwd),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(binds.len(), 1);
|
||||||
|
assert!(!binds[0].read_only);
|
||||||
|
assert_eq!(
|
||||||
|
working_dir,
|
||||||
|
Some(Path::new("/mnt").join(cwd.file_name().unwrap()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unmapped_current_directory_keeps_library_default() {
|
||||||
|
// The current directory is not among the binds: pass None so the
|
||||||
|
// library falls back to its own working-directory resolution.
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let cwd = dir.path().canonicalize().unwrap();
|
||||||
|
let (_, working_dir) = bind_targets(
|
||||||
|
&args(vec![PathBuf::from("/opt/toolchain")], Vec::new(), false),
|
||||||
|
Some(&cwd),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(working_dir, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_bind_keeps_library_default() {
|
||||||
|
let (_, working_dir) = bind_targets(&args(Vec::new(), Vec::new(), true), None).unwrap();
|
||||||
|
assert_eq!(working_dir, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_bind_with_bind_rw_is_rejected() {
|
||||||
|
assert!(bind_targets(&args(Vec::new(), vec![PathBuf::from("/data")], true), None).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_bind_without_current_directory_errors() {
|
||||||
|
assert!(bind_targets(&args(Vec::new(), Vec::new(), false), None).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user