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