Skip to main content

amiss_bootstrap/
build.rs

1use amiss_wire::controls::ConstraintPlatform;
2use amiss_wire::digest::{Digest, RAW_EVIDENCE_DOMAIN, hb, hj, sha256};
3use amiss_wire::json::{Value, canonical};
4use amiss_wire::manifest::{
5    DEPENDENCY_LOCK_DOMAIN, DEPENDENCY_LOCK_SCHEMA, ENVIRONMENT_CONTRACT, MANIFEST_DOMAIN,
6    MANIFEST_SCHEMA, RUNTIME_CONTRACT, RuntimeRole,
7};
8
9use crate::ENGINE_DOMAIN;
10
11/// One staged runtime file: its action-tree path, its role, whether Git will
12/// record the execute bit, and its exact bytes.
13pub struct StagedFile<'bytes> {
14    pub path: String,
15    pub role: RuntimeRole,
16    pub executable: bool,
17    pub bytes: &'bytes [u8],
18}
19
20/// One staged platform artifact: the closed platform row, the published
21/// artifact name, and its complete runtime closure. Exactly one file must
22/// carry the `executable` role.
23pub struct StagedArtifact<'bytes> {
24    pub platform: ConstraintPlatform,
25    pub artifact_name: String,
26    pub files: Vec<StagedFile<'bytes>>,
27}
28
29/// The build namespace and the lockfiles that pinned it.
30pub struct StagedBuild<'bytes> {
31    pub engine_version: String,
32    pub host: String,
33    pub owner: String,
34    pub repository: String,
35    pub object_format: &'static str,
36    pub commit_oid: String,
37    pub locks: Vec<(String, &'bytes [u8])>,
38}
39
40/// Builds the strict release manifest from the staged action tree: every
41/// digest is computed from the exact staged bytes, artifacts sort by
42/// platform, runtime files and lockfiles sort by path, and the lock-set
43/// digest is `HJ` over the complete lock object. Returns the manifest bytes
44/// (`JCS || LF`, the blob the action tree carries) and the semantic digest
45/// the execution constraint pins.
46///
47/// # Errors
48///
49/// A staged artifact without exactly one executable row, which is a
50/// malformed release rather than a runtime condition.
51pub fn build_manifest(
52    build: &StagedBuild<'_>,
53    artifacts: &mut [StagedArtifact<'_>],
54) -> Result<(Vec<u8>, Digest), &'static str> {
55    let mut locks: Vec<(String, Digest)> = build
56        .locks
57        .iter()
58        .map(|(path, bytes)| (path.clone(), hb(RAW_EVIDENCE_DOMAIN, bytes)))
59        .collect();
60    locks.sort_by(|a, b| a.0.cmp(&b.0));
61    let lock_value = object(vec![
62        ("schema", string(DEPENDENCY_LOCK_SCHEMA)),
63        (
64            "files",
65            Value::Array(
66                locks
67                    .iter()
68                    .map(|(path, digest)| {
69                        object(vec![
70                            ("path", string(path)),
71                            ("raw_digest", string(&digest.to_string())),
72                        ])
73                    })
74                    .collect(),
75            ),
76        ),
77    ]);
78    let lock_digest = hj(DEPENDENCY_LOCK_DOMAIN, &lock_value);
79
80    artifacts.sort_by_key(|artifact| artifact.platform.as_str());
81    let mut rows: Vec<Value> = Vec::with_capacity(artifacts.len());
82    for artifact in artifacts.iter_mut() {
83        rows.push(artifact_value(artifact)?);
84    }
85
86    let manifest = object(vec![
87        ("schema", string(MANIFEST_SCHEMA)),
88        ("engine_version", string(&build.engine_version)),
89        (
90            "build_source",
91            object(vec![
92                (
93                    "repository",
94                    object(vec![
95                        ("host", string(&build.host)),
96                        ("owner", string(&build.owner)),
97                        ("name", string(&build.repository)),
98                    ]),
99                ),
100                ("object_format", string(build.object_format)),
101                ("commit_oid", string(&build.commit_oid)),
102            ]),
103        ),
104        ("dependency_lock", lock_value),
105        ("dependency_lock_digest", string(&lock_digest.to_string())),
106        ("artifacts", Value::Array(rows)),
107    ]);
108    let digest = hj(MANIFEST_DOMAIN, &manifest);
109    let mut bytes = canonical(&manifest);
110    bytes.push(b'\n');
111    Ok((bytes, digest))
112}
113
114fn artifact_value(artifact: &mut StagedArtifact<'_>) -> Result<Value, &'static str> {
115    artifact.files.sort_by(|a, b| a.path.cmp(&b.path));
116    let mut executables = artifact
117        .files
118        .iter()
119        .filter(|file| file.role == RuntimeRole::Executable);
120    let engine = executables.next().ok_or("no executable row")?;
121    if executables.next().is_some() {
122        return Err("more than one executable row");
123    }
124    if !engine.executable {
125        return Err("the executable row is not mode 100755");
126    }
127    let mut launchers = artifact
128        .files
129        .iter()
130        .filter(|file| file.role == RuntimeRole::Launcher);
131    let launcher = launchers.next().ok_or("no launcher row")?;
132    if launchers.next().is_some() {
133        return Err("more than one launcher row");
134    }
135    if launcher.executable {
136        return Err("the launcher row is not mode 100644");
137    }
138    let binary_sha256 = sha256(engine.bytes);
139    let engine_digest = hb(ENGINE_DOMAIN, engine.bytes);
140    let tree_path = engine.path.clone();
141
142    let files: Vec<Value> = artifact
143        .files
144        .iter()
145        .map(|file| {
146            object(vec![
147                ("path", string(&file.path)),
148                ("role", string(file.role.as_str())),
149                (
150                    "git_mode",
151                    string(if file.executable { "100755" } else { "100644" }),
152                ),
153                ("file_sha256", string(&sha256(file.bytes).to_string())),
154            ])
155        })
156        .collect();
157
158    Ok(object(vec![
159        ("platform", string(artifact.platform.as_str())),
160        ("artifact_name", string(&artifact.artifact_name)),
161        ("tree_path", string(&tree_path)),
162        ("binary_sha256", string(&binary_sha256.to_string())),
163        ("engine_digest", string(&engine_digest.to_string())),
164        ("runtime_contract", string(RUNTIME_CONTRACT)),
165        ("environment_contract", string(ENVIRONMENT_CONTRACT)),
166        ("runtime_files", Value::Array(files)),
167    ]))
168}
169
170/// The action tree's root `action.yml`: JCS JSON plus LF, exactly `name`,
171/// `description`, and `runs`. The declared launcher is manifest-listed and
172/// exists for experimental convenience; the required path never executes it.
173#[must_use]
174fn string(text: &str) -> Value {
175    Value::String(text.to_owned())
176}
177
178fn object(members: Vec<(&str, Value)>) -> Value {
179    Value::Object(
180        members
181            .into_iter()
182            .map(|(key, value)| (key.to_owned(), value))
183            .collect(),
184    )
185}