1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
|
use std::path::PathBuf;
use anyhow::{Result, bail};
use clap::{Parser, Subcommand};
use crate::build::BuildExecutor;
use crate::compare::compare;
use crate::hasher::{HasherInstaller, ProcessRunner as HasherProcessRunner};
use crate::initramfs::{MakeInitrdBuilder, ProcessRunner as InitramfsProcessRunner};
use crate::manifest::ArtifactManifest;
use crate::{BuildPlan, ImageSpec};
#[derive(Debug, Parser)]
#[command(name = "alt-controller-image", about = "Native ALT image builder")]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
/// Validate an image specification and render its build intent.
Plan {
#[arg(long)]
spec: PathBuf,
},
/// Build an image from a specification.
Build {
#[arg(long)]
spec: PathBuf,
#[arg(long)]
workspace: PathBuf,
#[arg(long)]
output: PathBuf,
},
/// Inspect a built artifact.
Inspect {
#[arg(long)]
artifact: PathBuf,
},
/// Compare two built artifacts semantically.
Compare {
#[arg(long)]
left: PathBuf,
#[arg(long)]
right: PathBuf,
},
}
pub fn run(cli: Cli) -> Result<()> {
match cli.command {
Command::Plan { spec } => {
let plan = BuildPlan::compile(ImageSpec::load(&spec)?)?;
println!("target: {}", plan.spec().target.name);
println!("architecture: {}", plan.spec().target.architecture.as_str());
println!("format: tar");
println!("stages:");
for (index, stage) in plan.stages().iter().enumerate() {
println!(" {}. {stage}", index + 1);
}
Ok(())
}
Command::Build {
spec,
workspace,
output,
} => {
let plan = BuildPlan::compile(ImageSpec::load(&spec)?)?;
let apt_config = spec
.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.join("apt.conf");
let installer = HasherInstaller::new(HasherProcessRunner);
let initramfs = MakeInitrdBuilder::new(InitramfsProcessRunner);
let mut executor = BuildExecutor::new(installer, initramfs);
let result = executor.execute(&plan, workspace, apt_config, output)?;
println!("artifact: {}", result.artifact().display());
println!("manifest: {}", result.manifest().display());
if let Some(initrd) = result.initrd() {
println!(
"initrd: {} sha256:{}",
initrd.initrd_path().display(),
initrd.sha256()
);
}
Ok(())
}
Command::Inspect { artifact } => {
let manifest = load_artifact(&artifact)?;
println!("{}", toml::to_string_pretty(&manifest)?);
Ok(())
}
Command::Compare { left, right } => {
let left = load_artifact(&left)?;
let right = load_artifact(&right)?;
let report = compare(&left, &right);
print!("{}", report.render());
if report.is_equivalent() {
Ok(())
} else {
bail!("artifacts differ semantically")
}
}
}
}
fn load_artifact(path: &std::path::Path) -> Result<ArtifactManifest> {
if matches!(
path.extension().and_then(|extension| extension.to_str()),
Some("toml")
) {
return ArtifactManifest::load(path);
}
// A completed native build writes richer package and initramfs facts beside
// its tarball. Prefer that project-owned semantic record while retaining
// direct legacy-tar inspection when no companion is available.
let companion = ArtifactManifest::path_beside(path)?;
if companion.is_file() {
ArtifactManifest::load(companion)
} else {
ArtifactManifest::collect_archive(path)
}
}
|