This file provides guidance to AI coding agents when working with code in this repository.
# Build
cargo build --quiet
# Run tests
cargo test --quiet
# Check for errors without building
cargo check --all-targets --all-features --quiet
# Lint
cargo clippy --all-targets --all-features --quiet
# Format
cargo fmt --all --quiet
# Run the CLI
cargo run -- <command>
# Examples
cargo run -- apply -f examples/debian_trixie_mmdebstrap.yml --dry-run
cargo run -- validate -f examples/debian_trixie_mmdebstrap.ymlrsdebstrap is a declarative CLI tool for building Debian-based rootfs images using YAML manifest files. It wraps bootstrap tools (mmdebstrap, debootstrap) and provides post-bootstrap provisioning with privilege escalation support.
- CLI (
src/cli.rs) - Parses arguments using clap, providesapply,validate, andcompletionssubcommands - Config (
src/config.rs) - Loads and validates YAML profiles, resolves relative paths, applies defaults - Privilege (
src/privilege.rs) - Privilege escalation configuration and resolution (sudo/doas) - Error (
src/error.rs) - Typed error handling withRsdebstrapError - Bootstrap (
src/bootstrap/) - Executes bootstrap backends to create the rootfs - Pipeline (
src/pipeline.rs) - Orchestratesprepare,provision, andassemblephases in order
-
Privilegeenum (src/privilege.rs) - Privilege escalation configurationPrivilegeMethodenum:Sudo,Doas— the actual escalation commandPrivilegeDefaultsstruct — default privilege settings for the profilePrivilegeenum:Inherit(use defaults if available),UseDefault(require defaults),Disabled,Method(PrivilegeMethod)(explicit)resolve()collapses against profile defaults toOption<PrivilegeMethod>resolve_in_place()mutates self toMethodorDisabledresolved_method()returnsOption<PrivilegeMethod>for already-resolved states- Custom Serialize/Deserialize:
true→ UseDefault,false→ Disabled,{ method: sudo }→ Method, absent → Inherit
-
RsdebstrapErrorenum (src/error.rs) - Domain-specific typed errors#[non_exhaustive]enum usingthiserror, withanyhowat trait boundaries- Variants:
Validation,Execution,Isolation,Config,CommandNotFound,Io - Factory methods:
execution(spec, status),execution_in_isolation(command, name, status),io(context, source),command_not_found(command, label) Iovariant usesio_error_kind_message()for human-readable display
-
BootstrapBackendtrait (src/bootstrap/mod.rs) - Interface for bootstrap toolsMmdebstrapConfig- mmdebstrap implementationDebootstrapConfig- debootstrap implementation- Each backend builds command arguments and determines output type (directory vs archive)
-
Bootstrapenum (src/config.rs) - Bootstrap backend wrapperresolve_privilege()resolves privilege settings against profile defaultsresolved_privilege_method()returns the resolvedOption<PrivilegeMethod>
-
PhaseItemtrait (src/phase/mod.rs) - Internal trait for generic phase processingpub(crate)— used by Pipeline to process tasks uniformly across phases- Methods:
name(),validate(),execute(),resolved_isolation_config() - Implemented by
PrepareTask,ProvisionTask,AssembleTask
-
PrepareTaskenum (src/phase/prepare/mod.rs) - Preparation tasks before provisioningMountvariant (src/phase/prepare/mount.rs) - Declares filesystem mounts for the rootfsResolvConfvariant (src/phase/prepare/resolv_conf.rs) - Declares resolv.conf setup for DNS resolutionmount_task()returnsOption<&MountTask>for accessing the inner mount taskresolv_conf_task()returnsOption<&ResolvConfTask>for accessing the inner resolv_conf taskPhaseItem::execute()is a no-op for both — lifecycle managed at pipeline level
-
MountTaskstruct (src/phase/prepare/mount.rs) - Mount declaration for prepare phasepreset: Option<MountPreset>,mounts: Vec<MountEntry>resolved_mounts()merges preset + custom mounts (same logic as formerIsolationConfig::resolved_mounts())has_mounts()returns true if preset or custom mounts are specifiedvalidate()checks entries and mount ordername()returns "preset", "custom", "preset+custom", or "empty"- At most one mount task allowed in prepare phase (validated by
Profile::validate_mounts())
-
ResolvConfTaskstruct (src/phase/prepare/resolv_conf.rs) - resolv.conf declaration for prepare phase- Fields:
copy: bool,name_servers: Vec<IpAddr>,search: Vec<String>(flat, same asResolvConfConfig) #[serde(deny_unknown_fields)]for strict YAML parsingname()returns"copy"or"generate"config()converts toResolvConfConfigfor use withRootfsResolvConfvalidate()delegates toconfig().validate()- At most one resolv_conf task allowed in prepare phase (validated by
Profile::validate_resolv_conf()) - Mount tasks must come before resolv_conf tasks (validated by
Profile::validate_prepare_order())
- Fields:
-
AssembleTaskenum (src/phase/assemble/mod.rs) - Finalization tasks after provisioningResolvConfvariant (src/phase/assemble/resolv_conf.rs) - Writes a permanent/etc/resolv.confresolv_conf_task()returnsOption<&AssembleResolvConfTask>for accessing the inner taskPhaseItem::execute()delegates to inner task'sexecute()resolved_isolation_config()returnsNone(operates directly on rootfs filesystem viaDirectProvider)
-
AssembleResolvConfTaskstruct (src/phase/assemble/resolv_conf.rs) - Permanent resolv.conf for assemble phase- Fields:
privilege: Privilege,link: Option<String>,name_servers: Vec<IpAddr>,search: Vec<String> #[serde(deny_unknown_fields)]for strict YAML parsinglinkandname_servers/searchare mutually exclusivename()returns"link"or"generate"resolve_privilege()/resolved_privilege_method()— same pattern as provision tasksvalidate()checks mutual exclusivity, link validation (empty/newline/null), delegates toResolvConfConfig::validate()for generate modeexecute()uses TOCTOU-safe/etcvalidation viaopenat(O_NOFOLLOW),CommandExecutorviactx.executor()forcp/chmod/rm/lnwith privilege escalation, atomic file operations via temp file +cp- At most one
resolv_conftask allowed in assemble phase (validated byProfile::validate_assemble_resolv_conf())
- Fields:
-
ProvisionTaskenum (src/phase/provision/mod.rs) - Declarative task definition for provision pipeline stepsShellvariant (src/phase/provision/shell.rs) - Runs shell scripts within an isolation contextMitamaevariant (src/phase/provision/mitamae.rs) - Runs mitamae recipes within an isolation context- Each task has a
privilege: Privilegefield resolved during defaults application - Each task has an
isolation: TaskIsolationfield resolved viaresolve_isolation() resolved_isolation_config()returnsOption<&IsolationConfig>after resolution- Enum-based dispatch with compile-time exhaustive matching
- Shared utilities in
src/phase/mod.rs:ScriptSource,TempFileGuard,validate_tmp_directory() - Helper functions:
execute_in_context(),check_execution_result(),prepare_files_with_toctou_check()
-
MitamaeTask(src/phase/provision/mitamae.rs) - Mitamae recipe executionbinaryfield:Option<Utf8PathBuf>— can be omitted and resolved fromdefaults.mitamae- Copies binary to rootfs /tmp with 0o700 permissions, runs
mitamae local <recipe> - RAII cleanup of both binary and recipe temp files via
TempFileGuard
-
Pipelinestruct (src/pipeline.rs) - Orchestrates task execution in three phases- Holds
&[PrepareTask],&[ProvisionTask],&[AssembleTask]slices - Creates per-task isolation contexts based on each task's
resolved_isolation_config() - Generic
run_phase_items<T: PhaseItem>()andvalidate_phase_items<T: PhaseItem>()free functions run_task_item()creates provider → setup → execute → teardown for each task independently- Executes prepare, provision, assemble phases in order
- Guarantees teardown even on task execution errors
- Holds
-
TaskIsolationenum (src/isolation/mod.rs) - Task-level isolation settingInherit(default): use profile defaultsUseDefault: explicitly use defaults (isolation: true)Disabled: no isolation, direct execution (isolation: false)Config(IsolationConfig): explicit config (isolation: { type: chroot })resolve()/resolve_in_place()collapse against profileIsolationConfigdefaultsresolved_config()returnsOption<&IsolationConfig>—Somefor isolation,Nonefor disabled- Custom Serialize/Deserialize:
true→ UseDefault,false→ Disabled,{ type: chroot }→ Config, absent → Inherit - Note:
UseDefaultandInheritproduce identical behavior becauseIsolationConfigalways has a default (Chroot). Both exist for API symmetry withPrivilegeenum.
-
IsolationConfigenum (src/config.rs) - Isolation backend configurationChroot- chroot isolation (unit variant, no fields)chroot()convenience constructor returnsSelf::Chrootas_provider()returns the correspondingIsolationProvider- Note:
mountandresolv_confconfiguration have moved to the prepare phase
-
ResolvConfConfigstruct (src/config.rs) - resolv.conf configurationcopy: bool— copy host's /etc/resolv.conf into the chroot (following symlinks)name_servers: Vec<IpAddr>— explicit nameserver IP addressessearch: Vec<String>— search domainscopy: trueandname_servers/searchare mutually exclusivevalidate()enforces resolv.conf spec limits (max 3 nameservers, max 6 search domains, 256 char total)
-
MountEntrystruct (src/config.rs) - Filesystem mount specification- Fields:
source(device/path),target(absolute path in rootfs),options(mount -o flags) is_pseudo_fs()— checks if source is a known pseudo-filesystem (proc, sysfs, etc.)is_bind_mount()— checks if options contain "bind"build_mount_spec_with_path()/build_umount_spec_with_path()— constructCommandSpecfor mount/umount using a pre-validated absolute target pathvalidate()checks..components in targets, bind mount sources, and regular mount sources- Pseudo-fs uses
mount -t <type>, bind mounts usemount -o bind
- Fields:
-
MountPresetenum (src/config.rs) - Predefined mount setsRecommends— common mounts: proc -> /proc, sysfs -> /sys, devtmpfs -> /dev, devpts -> /dev/pts, tmpfs -> /tmp, tmpfs -> /run
-
RootfsMountsstruct (src/isolation/mount.rs) - RAII mount lifecycle manager- Mounts entries in order, unmounts in reverse order
mount()usessafe_create_mount_point()to create directories withopenat/mkdirat+O_NOFOLLOW(TOCTOU-safe)- Stores verified absolute paths in
mounted_paths: Vec<Option<Utf8PathBuf>>and reuses them forumount(avoids re-traversal) unmount()is idempotent, collects errors from all entriesDropimpl guarantees cleanup even on error paths- Used by
run_pipeline_phase()to bracket the entire pipeline execution
-
RootfsResolvConfstruct (src/isolation/resolv_conf.rs) - RAII resolv.conf lifecycle managersetup()backs up existing resolv.conf, writes new content (copy from host or generate)teardown()restores original resolv.conf from backup- Write failure triggers rename rollback to prevent data loss
- Drop guard ensures cleanup even on error paths
host_resolv_confparameter enables test-time host path injection- Validates
/etcis not a symlink, checks for leftover backup files - Used by
run_pipeline_phase()between mount and pipeline execution
-
safe_create_mount_point()fn (src/isolation/mount.rs) - Symlink-safe directory creation- Opens rootfs with
O_NOFOLLOWto verify it's not a symlink - Traverses each path component with
openat(O_NOFOLLOW), creates missing dirs withmkdirat - Returns
ELOOP/ENOTDIR→RsdebstrapError::Isolationon symlink detection - Handles race conditions (
EEXISTonmkdirat→ re-open) - Uses
rustixcrate (direct dependency,features = ["fs"]) for memory-safe syscall wrappers
- Opens rootfs with
-
IsolationProvider/IsolationContexttraits (src/isolation/mod.rs) - Isolation backendsChrootProvider/ChrootContext- chroot-based isolationDirectProvider/DirectContext(src/isolation/direct.rs) - No isolation, direct execution on host- Translates absolute paths to rootfs-prefixed paths (e.g.,
/bin/sh→<rootfs>/bin/sh) - Guards against empty commands and post-teardown execution
- Translates absolute paths to rootfs-prefixed paths (e.g.,
IsolationContext::execute()takesprivilege: Option<PrivilegeMethod>parameterIsolationContext::executor()returns&dyn CommandExecutorfor direct command execution with privilege support
-
CommandExecutortrait /CommandSpecstruct (src/executor/mod.rs) - Command executionRealCommandExecutor- Actual execution with dry-run supportCommandSpecfields:command,args,cwd,env,privilege: Option<PrivilegeMethod>- Builder methods:
with_privilege(),with_cwd(),with_env(),with_envs() format_args_lossy()utility for consistent argument formatting in errors and dry-run output- Tests use mock executors to verify command construction without running real commands
dir: /output/path # Base output directory
defaults: # Optional default settings
isolation:
type: chroot # Isolation backend: chroot (default)
privilege: # Optional default privilege escalation
method: sudo # Method: sudo | doas
mitamae: # Optional mitamae defaults
binary:
x86_64: /path/to/mitamae-x86_64
aarch64: /path/to/mitamae-aarch64
bootstrap:
type: mmdebstrap # Backend type: mmdebstrap | debootstrap
suite: trixie # Debian suite
target: rootfs # Output name (directory or archive)
privilege: true # Use default privilege method
# Backend-specific options...
prepare: # Optional preparation steps
- type: mount # Filesystem mounts for the rootfs
preset: recommends # Optional: predefined mount set
mounts: # Optional: custom mount entries
- source: /dev
target: /dev
options: [bind]
- type: resolv_conf # resolv.conf setup for DNS in chroot
copy: true # Copy host's /etc/resolv.conf
# OR
# name_servers: [8.8.8.8] # Generate with explicit nameservers
# search: [example.com] # Optional search domains
provision: # Optional main provisioning steps
- type: shell
content: "..." # Inline script
# OR
script: ./script.sh # External script path
privilege: false # Disable privilege escalation for this task
isolation: false # Disable isolation (direct execution on host)
- type: mitamae
script: ./recipe.rb # Mitamae recipe file
# OR
content: "..." # Inline recipe
binary: /path/to/mitamae # Optional: override defaults.mitamae
privilege: # Optional: override defaults.privilege
method: doas
isolation: # Optional: override defaults.isolation
type: chroot
assemble: # Optional finalization steps
- type: resolv_conf # Permanent /etc/resolv.conf in final rootfs
name_servers: [8.8.8.8, 8.8.4.4] # Generate resolv.conf with nameservers
search: [example.com] # Optional search domains
privilege: true # Optional: use default privilege method
# OR
# link: ../run/systemd/resolve/stub-resolv.conf # Create symlink instead- Absent (field not specified) →
Inherit: use defaults if available, no escalation otherwise privilege: true→UseDefault: requiredefaults.privilege.method(error if not configured)privilege: false→Disabled: no privilege escalationprivilege: { method: sudo }→Method: use the specified method explicitly
- Absent (field not specified) →
Inherit: usedefaults.isolation(defaults to chroot) isolation: true→UseDefault: usedefaults.isolationexplicitly (same behavior asInherit)isolation: false→Disabled: no isolation (direct execution on host viaDirectProvider)isolation: { type: chroot }→Config: use the specified isolation backend explicitly
copy: true→ copy host's /etc/resolv.conf into thechrootname_servers: [...]→ generateresolv.confwith specified nameserversname_servers: [...], search: [...]→ generate with nameservers + search domainscopyandname_servers/searchare mutually exclusive
- Mounts are configured in the
preparephase as atype: mounttask - At most one mount task is allowed in the prepare phase
- When mounts are specified,
defaults.isolationmust bechroot(validated byvalidate_mounts()) - When mounts are specified,
defaults.privilegemust be configured - Mount targets must be absolute paths without
..components - Bind mount sources must exist on the host
- Mount order must satisfy parent-before-child ordering
- Custom mounts override preset entries with the same target at their original position (preserving mount order)
RootfsMountshandles mount/unmount lifecycle around the entire pipeline phaseRootfsResolvConfhandles resolv.conf setup/restore between mount and pipeline executionresolv_confis configured in thepreparephase as atype: resolv_conftask- At most one
resolv_conftask is allowed in the prepare phase - Mount tasks must come before
resolv_conftasks in the prepare phase (validated byvalidate_prepare_order()) - Assemble
resolv_confwrites a permanent/etc/resolv.conf(file or symlink) to the final rootfs - At most one
resolv_conftask is allowed in the assemble phase (validated byvalidate_assemble_resolv_conf()) linkandname_servers/searchare mutually exclusive in assembleresolv_conf- Prepare and assemble can both have
resolv_conftasks (no constraint — different roles: temporary DNS vs permanent config)
Tests use a mock executor pattern defined in tests/helpers/mod.rs:
MockContext- Shared mock isolation context (used by shell_task_test.rs and mitamae_task_test.rs) with configurable failure modes (should_fail,should_error,return_no_status)MockContexttracksexecuted_commandsandexecuted_privilegesfor assertionhelpers::load_profile_from_yaml()/load_profile_from_yaml_typed()- Load profiles from YAML strings in temp files- Test builders:
MmdebstrapConfigBuilder,DebootstrapConfigBuilderwith fluent API - Privilege-related tests verify resolution, inheritance, and error handling across tasks and bootstrap backends
run_task_item()teardown failure paths (patterns 3 and 4:Ok/ErrandErr/Err) are not currently testable because the pipeline internally creates providers fromtask.resolved_isolation_config(), making failure injection impractical. BothChrootProviderandDirectProviderhave infallible teardown, so these paths are unreachable with current backends. Tests should be added when backends with fallible teardown (e.g., bwrap, systemd-nspawn) are introduced.run_pipeline_phase()4-way error matrix (Ok/Ok,Err/Ok,Ok/Err,Err/Err) is not directly tested as an integration test because the function is private and requires real filesystem mounts.RootfsMountsunit tests cover mount/unmount error paths independently usingMockMountExecutor.