fabro/lib/crates/fabro-devcontainer/src/dockerfile.rs
Bryan Helmkamp 28884ae093 rename Arc to Fabro in all Rust crates, symbols, env vars, and supporting files
- Rename 20 crate directories lib/crates/arc-* → fabro-*
- Update all Cargo.toml: crate names, dep paths, feature flags, bin name
- Rename arc_server module → fabro_server in fabro-llm
- ArcError → FabroError across 30+ files
- ARC_VERSION/ARC_GIT_SHA/ARC_BUILD_DATE → FABRO_* constants
- All use/qualified paths: arc_agent:: → fabro_agent::, etc. (~1500 occurrences)
- Env vars ARC_* → FABRO_* in string literals and shell scripts
- String literals: X-Arc-Demo, arc-bot, arc@local, arc-web, arc-mcp, etc.
- Path strings: .arc/ → .fabro/, arc.toml → fabro.toml, refs/arc/ → refs/fabro/
- arc-api.yaml → fabro-api.yaml (OpenAPI spec)
- skills/arc-create-workflow → fabro-create-workflow
- trycmd fixtures: $ arc → $ fabro
- Inline snapshots (insta) updated
- CI, Docker, install.sh, scripts, CLAUDE.md, AGENTS.md
- TypeScript app: env vars, headers, JWT issuer
- Docs: page slugs, git refs, config paths, sandbox names, repo URLs
- Repo references: brynary/arc → fabro-sh/fabro

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 12:25:58 -04:00

221 lines
6.1 KiB
Rust

use std::collections::HashMap;
use crate::features::FeatureLayer;
/// Generate a combined Dockerfile from base + features + env + user.
pub fn generate(
base_dockerfile: &str,
feature_layers: &[FeatureLayer],
container_env: &HashMap<String, String>,
remote_user: Option<&str>,
) -> String {
let mut sections: Vec<String> = Vec::new();
sections.push("# Generated by fabro-devcontainer".to_string());
sections.push(base_dockerfile.to_string());
for layer in feature_layers {
sections.push(layer.dockerfile_snippet.clone());
}
if !container_env.is_empty() {
let mut keys: Vec<&String> = container_env.keys().collect();
keys.sort();
let env_lines: Vec<String> = keys
.iter()
.map(|k| format!("ENV {}={}", k, container_env[*k]))
.collect();
sections.push(env_lines.join("\n"));
}
if let Some(user) = remote_user {
sections.push(format!("USER {}", user));
}
let mut result = sections.join("\n\n");
result.push('\n');
result
}
#[cfg(test)]
mod tests {
use super::*;
fn make_layer(id: &str, dir_name: &str, snippet: &str) -> FeatureLayer {
FeatureLayer {
id: id.to_string(),
dir_name: dir_name.to_string(),
dockerfile_snippet: snippet.to_string(),
}
}
#[test]
fn base_image_only() {
let result = generate("FROM ubuntu:22.04", &[], &HashMap::new(), None);
insta::assert_snapshot!(result, @r"
# Generated by fabro-devcontainer
FROM ubuntu:22.04
");
}
#[test]
fn base_dockerfile_preserved_as_is() {
let base = "FROM ubuntu:22.04\nRUN apt-get update\nRUN apt-get install -y curl";
let result = generate(base, &[], &HashMap::new(), None);
insta::assert_snapshot!(result, @r"
# Generated by fabro-devcontainer
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y curl
");
}
#[test]
fn with_feature_layers() {
let layers = vec![
make_layer("node", "node-1", "RUN install-node.sh"),
make_layer("python", "python-1", "RUN install-python.sh"),
];
let result = generate("FROM ubuntu:22.04", &layers, &HashMap::new(), None);
insta::assert_snapshot!(result, @r"
# Generated by fabro-devcontainer
FROM ubuntu:22.04
RUN install-node.sh
RUN install-python.sh
");
}
#[test]
fn with_env_sorted() {
let mut env = HashMap::new();
env.insert("ZEBRA".to_string(), "stripes".to_string());
env.insert("APPLE".to_string(), "red".to_string());
env.insert("MANGO".to_string(), "yellow".to_string());
let result = generate("FROM alpine", &[], &env, None);
insta::assert_snapshot!(result, @r"
# Generated by fabro-devcontainer
FROM alpine
ENV APPLE=red
ENV MANGO=yellow
ENV ZEBRA=stripes
");
}
#[test]
fn with_remote_user() {
let result = generate("FROM alpine", &[], &HashMap::new(), Some("vscode"));
insta::assert_snapshot!(result, @r"
# Generated by fabro-devcontainer
FROM alpine
USER vscode
");
}
#[test]
fn all_combined() {
let layers = vec![make_layer("node", "node-1", "RUN install-node.sh")];
let mut env = HashMap::new();
env.insert("PATH".to_string(), "/usr/local/bin".to_string());
env.insert("HOME".to_string(), "/home/vscode".to_string());
let result = generate("FROM ubuntu:22.04", &layers, &env, Some("vscode"));
insta::assert_snapshot!(result, @r"
# Generated by fabro-devcontainer
FROM ubuntu:22.04
RUN install-node.sh
ENV HOME=/home/vscode
ENV PATH=/usr/local/bin
USER vscode
");
}
#[test]
fn empty_feature_layers_no_extra_blank_lines() {
let result = generate("FROM alpine", &[], &HashMap::new(), Some("dev"));
insta::assert_snapshot!(result, @r"
# Generated by fabro-devcontainer
FROM alpine
USER dev
");
}
#[test]
fn empty_env_map_treated_as_none() {
let env = HashMap::new();
let result = generate("FROM alpine", &[], &env, None);
insta::assert_snapshot!(result, @r"
# Generated by fabro-devcontainer
FROM alpine
");
}
#[test]
fn multiline_base_dockerfile() {
let base = "FROM ubuntu:22.04 AS builder\n\
RUN apt-get update && apt-get install -y build-essential\n\
COPY . /app\n\
RUN make\n\
\n\
FROM ubuntu:22.04\n\
COPY --from=builder /app/bin /usr/local/bin";
let result = generate(base, &[], &HashMap::new(), None);
insta::assert_snapshot!(result, @r"
# Generated by fabro-devcontainer
FROM ubuntu:22.04 AS builder
RUN apt-get update && apt-get install -y build-essential
COPY . /app
RUN make
FROM ubuntu:22.04
COPY --from=builder /app/bin /usr/local/bin
");
}
#[test]
fn container_env_only() {
let mut cenv = HashMap::new();
cenv.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string());
let result = generate("FROM alpine", &[], &cenv, None);
insta::assert_snapshot!(result, @r"
# Generated by fabro-devcontainer
FROM alpine
ENV DEBIAN_FRONTEND=noninteractive
");
}
#[test]
fn container_env_with_multiple_keys() {
let mut cenv = HashMap::new();
cenv.insert("ALPHA".to_string(), "first".to_string());
cenv.insert("BETA".to_string(), "second".to_string());
cenv.insert("GAMMA".to_string(), "third".to_string());
let result = generate("FROM alpine", &[], &cenv, None);
insta::assert_snapshot!(result, @r"
# Generated by fabro-devcontainer
FROM alpine
ENV ALPHA=first
ENV BETA=second
ENV GAMMA=third
");
}
}