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