🥇 export from upstream (3b6ef04)
This commit is contained in:
@@ -14,9 +14,7 @@ use crate::config::SandcageConfig;
|
||||
// Bundled Dockerfiles
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub const DOCKERFILE_BASE: &str = include_str!("../../../images/base/Dockerfile");
|
||||
pub const DOCKERFILE_CLAUDE: &str = include_str!("../../../images/claude/Dockerfile");
|
||||
pub const DOCKERFILE_CODEX: &str = include_str!("../../../images/codex/Dockerfile");
|
||||
pub const DOCKERFILE: &str = include_str!("../../../images/base/Dockerfile");
|
||||
|
||||
pub const COMPOSE_YAML: &str = include_str!("../../../compose/docker-compose.yml");
|
||||
|
||||
@@ -179,12 +177,8 @@ fn write_compose_tempfile() -> Result<tempfile::NamedTempFile> {
|
||||
Ok(tmp)
|
||||
}
|
||||
|
||||
fn image_for_service(service: &str) -> &'static str {
|
||||
match service {
|
||||
"claude" => "sandcage-claude",
|
||||
"codex" => "sandcage-codex",
|
||||
_ => "sandcage-base",
|
||||
}
|
||||
fn image_for_service(_service: &str) -> &'static str {
|
||||
"sandcage"
|
||||
}
|
||||
|
||||
fn require_image(docker: &Path, image: &str) -> Result<()> {
|
||||
@@ -202,6 +196,32 @@ fn require_image(docker: &Path, image: &str) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_mount_path(mount: &str) -> String {
|
||||
let Some(colon_pos) = mount.find(':') else {
|
||||
return mount.to_string();
|
||||
};
|
||||
|
||||
let host_path = &mount[..colon_pos];
|
||||
let rest = &mount[colon_pos..];
|
||||
|
||||
let expanded = if host_path == "~" {
|
||||
match dirs::home_dir() {
|
||||
Some(home) => home.to_string_lossy().into_owned(),
|
||||
None => return mount.to_string(),
|
||||
}
|
||||
} else if let Some(suffix) = host_path.strip_prefix("~/") {
|
||||
match dirs::home_dir() {
|
||||
Some(home) => format!("{}/{suffix}", home.to_string_lossy()),
|
||||
None => return mount.to_string(),
|
||||
}
|
||||
} else {
|
||||
return mount.to_string();
|
||||
};
|
||||
|
||||
let normalized = expanded.replace('\\', "/");
|
||||
format!("{normalized}{rest}")
|
||||
}
|
||||
|
||||
pub fn build_run_args(
|
||||
service: &str,
|
||||
compose_path: &str,
|
||||
@@ -228,7 +248,7 @@ pub fn build_run_args(
|
||||
for mount in mount_list {
|
||||
if mount.contains(':') {
|
||||
args.push("-v".to_string());
|
||||
args.push(mount.clone());
|
||||
args.push(expand_mount_path(mount));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -484,9 +504,7 @@ pub fn build_images(force: bool) -> Result<()> {
|
||||
|
||||
|
||||
let images: &[(&str, &str)] = &[
|
||||
("sandcage-base", DOCKERFILE_BASE),
|
||||
("sandcage-claude", DOCKERFILE_CLAUDE),
|
||||
("sandcage-codex", DOCKERFILE_CODEX),
|
||||
("sandcage", DOCKERFILE),
|
||||
];
|
||||
|
||||
let hashes_path = build_hashes_path()?;
|
||||
@@ -630,9 +648,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn dockerfiles_are_bundled() {
|
||||
assert!(!DOCKERFILE_BASE.is_empty(), "base Dockerfile should not be empty");
|
||||
assert!(!DOCKERFILE_CLAUDE.is_empty(), "claude Dockerfile should not be empty");
|
||||
assert!(!DOCKERFILE_CODEX.is_empty(), "codex Dockerfile should not be empty");
|
||||
assert!(!DOCKERFILE.is_empty(), "Dockerfile should not be empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -658,13 +674,13 @@ mod tests {
|
||||
let path = tmp.path().join(".build-hashes");
|
||||
|
||||
let mut hashes = HashMap::new();
|
||||
hashes.insert("sandcage-base".to_string(), "aabbcc".to_string());
|
||||
hashes.insert("sandcage".to_string(), "aabbcc".to_string());
|
||||
hashes.insert("sandcage-claude".to_string(), "ddeeff".to_string());
|
||||
|
||||
write_stored_hashes(&path, &hashes).expect("write hashes");
|
||||
let loaded = read_stored_hashes(&path).expect("read hashes");
|
||||
|
||||
assert_eq!(loaded.get("sandcage-base").map(String::as_str), Some("aabbcc"));
|
||||
assert_eq!(loaded.get("sandcage").map(String::as_str), Some("aabbcc"));
|
||||
assert_eq!(loaded.get("sandcage-claude").map(String::as_str), Some("ddeeff"));
|
||||
}
|
||||
|
||||
@@ -683,9 +699,9 @@ mod tests {
|
||||
let hash = sha256_hex(dockerfile);
|
||||
|
||||
let mut stored: HashMap<String, String> = HashMap::new();
|
||||
stored.insert("sandcage-base".to_string(), hash.clone());
|
||||
stored.insert("sandcage".to_string(), hash.clone());
|
||||
|
||||
let needs_build = stored.get("sandcage-base").map_or(true, |h| h != &hash);
|
||||
let needs_build = stored.get("sandcage").map_or(true, |h| h != &hash);
|
||||
assert!(!needs_build, "same hash should be a cache hit (no rebuild needed)");
|
||||
}
|
||||
|
||||
@@ -695,9 +711,9 @@ mod tests {
|
||||
let current_hash = sha256_hex(dockerfile);
|
||||
|
||||
let mut stored: HashMap<String, String> = HashMap::new();
|
||||
stored.insert("sandcage-base".to_string(), "old_stale_hash".to_string());
|
||||
stored.insert("sandcage".to_string(), "old_stale_hash".to_string());
|
||||
|
||||
let needs_build = stored.get("sandcage-base").map_or(true, |h| h != ¤t_hash);
|
||||
let needs_build = stored.get("sandcage").map_or(true, |h| h != ¤t_hash);
|
||||
assert!(needs_build, "different hash should be a cache miss (rebuild needed)");
|
||||
}
|
||||
|
||||
@@ -707,7 +723,7 @@ mod tests {
|
||||
let current_hash = sha256_hex(dockerfile);
|
||||
let stored: HashMap<String, String> = HashMap::new();
|
||||
|
||||
let needs_build = stored.get("sandcage-base").map_or(true, |h| h != ¤t_hash);
|
||||
let needs_build = stored.get("sandcage").map_or(true, |h| h != ¤t_hash);
|
||||
assert!(needs_build, "missing entry should be treated as a cache miss");
|
||||
}
|
||||
|
||||
@@ -718,10 +734,10 @@ mod tests {
|
||||
let hash = sha256_hex(dockerfile);
|
||||
|
||||
let mut stored: HashMap<String, String> = HashMap::new();
|
||||
stored.insert("sandcage-base".to_string(), hash.clone());
|
||||
stored.insert("sandcage".to_string(), hash.clone());
|
||||
|
||||
let force = true;
|
||||
let needs_build = force || stored.get("sandcage-base").map_or(true, |h| h != &hash);
|
||||
let needs_build = force || stored.get("sandcage").map_or(true, |h| h != &hash);
|
||||
assert!(needs_build, "force flag should always trigger a rebuild");
|
||||
}
|
||||
|
||||
@@ -732,7 +748,7 @@ mod tests {
|
||||
let path = tmp.path().join("a").join("b").join(".build-hashes");
|
||||
|
||||
let mut hashes = HashMap::new();
|
||||
hashes.insert("sandcage-base".to_string(), "abc123".to_string());
|
||||
hashes.insert("sandcage".to_string(), "abc123".to_string());
|
||||
|
||||
write_stored_hashes(&path, &hashes).expect("should create parent dirs and write");
|
||||
assert!(path.exists(), "hash file should have been created");
|
||||
@@ -740,10 +756,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn image_for_service_maps_correctly() {
|
||||
assert_eq!(image_for_service("claude"), "sandcage-claude");
|
||||
assert_eq!(image_for_service("codex"), "sandcage-codex");
|
||||
assert_eq!(image_for_service("shell"), "sandcage-base");
|
||||
assert_eq!(image_for_service("anything-else"), "sandcage-base");
|
||||
assert_eq!(image_for_service("claude"), "sandcage");
|
||||
assert_eq!(image_for_service("codex"), "sandcage");
|
||||
assert_eq!(image_for_service("shell"), "sandcage");
|
||||
assert_eq!(image_for_service("anything-else"), "sandcage");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -864,6 +880,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn build_run_args_with_mounts() {
|
||||
let home = dirs::home_dir().unwrap();
|
||||
let home_str = home.to_string_lossy().replace('\\', "/");
|
||||
let config = SandcageConfig {
|
||||
mounts: Some(vec![
|
||||
"~/.ssh:/home/agent/.ssh:ro".to_string(),
|
||||
@@ -874,8 +892,10 @@ mod tests {
|
||||
let args = build_run_args("claude", "/tmp/compose.yml", &config, false, &[]);
|
||||
|
||||
assert!(args.contains(&"-v".to_string()), "should contain -v flag");
|
||||
assert!(args.contains(&"~/.ssh:/home/agent/.ssh:ro".to_string()));
|
||||
assert!(args.contains(&"~/.gitconfig:/home/agent/.gitconfig:ro".to_string()));
|
||||
let expected_ssh = format!("{home_str}/.ssh:/home/agent/.ssh:ro");
|
||||
let expected_git = format!("{home_str}/.gitconfig:/home/agent/.gitconfig:ro");
|
||||
assert!(args.contains(&expected_ssh), "SSH mount should be expanded");
|
||||
assert!(args.contains(&expected_git), "gitconfig mount should be expanded");
|
||||
|
||||
let service_pos = args.iter().position(|a| a == "claude").unwrap();
|
||||
let first_v = args.iter().position(|a| a == "-v").unwrap();
|
||||
@@ -901,6 +921,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn build_run_args_mounts_with_env_and_extra_args() {
|
||||
let home = dirs::home_dir().unwrap();
|
||||
let home_str = home.to_string_lossy().replace('\\', "/");
|
||||
let mut env = HashMap::new();
|
||||
env.insert("FOO".to_string(), "bar".to_string());
|
||||
let config = SandcageConfig {
|
||||
@@ -912,7 +934,8 @@ mod tests {
|
||||
|
||||
let service_pos = args.iter().position(|a| a == "claude").unwrap();
|
||||
let env_pos = args.iter().position(|a| a == "FOO=bar").unwrap();
|
||||
let mount_pos = args.iter().position(|a| a == "~/.ssh:/home/agent/.ssh:ro").unwrap();
|
||||
let expected_mount = format!("{home_str}/.ssh:/home/agent/.ssh:ro");
|
||||
let mount_pos = args.iter().position(|a| *a == expected_mount).unwrap();
|
||||
let resume_pos = args.iter().position(|a| a == "--resume").unwrap();
|
||||
|
||||
assert!(env_pos < service_pos, "env before service");
|
||||
@@ -968,4 +991,38 @@ mod tests {
|
||||
let env = build_compose_env(&workspace, &config).expect("build_compose_env");
|
||||
assert_eq!(env["SANDCAGE_CONTAINER_DIR"], "/workspace");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_mount_path_expands_tilde_slash() {
|
||||
let home = dirs::home_dir().unwrap();
|
||||
let home_str = home.to_string_lossy().replace('\\', "/");
|
||||
let result = expand_mount_path("~/.ssh:/home/agent/.ssh:ro");
|
||||
assert_eq!(result, format!("{home_str}/.ssh:/home/agent/.ssh:ro"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_mount_path_expands_bare_tilde() {
|
||||
let home = dirs::home_dir().unwrap();
|
||||
let home_str = home.to_string_lossy().replace('\\', "/");
|
||||
let result = expand_mount_path("~:/container");
|
||||
assert_eq!(result, format!("{home_str}:/container"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_mount_path_ignores_absolute() {
|
||||
let result = expand_mount_path("/absolute:/container:ro");
|
||||
assert_eq!(result, "/absolute:/container:ro");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_mount_path_ignores_relative() {
|
||||
let result = expand_mount_path("./relative:/container");
|
||||
assert_eq!(result, "./relative:/container");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_mount_path_ignores_no_colon() {
|
||||
let result = expand_mount_path("no-colon");
|
||||
assert_eq!(result, "no-colon");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user