1#[cfg(test)]
8mod tests;
9
10use crate::{
11 durable_io::{
12 RegularFileReadError, create_new_bytes_with_parents, read_optional_regular_bytes,
13 },
14 release_build::{ReleaseBuildPlanError, ReleaseBuildPlanState, load_release_build_plan},
15 release_set::artifact::{
16 ReleaseArtifactMaterializationError, contains_release_build_identity,
17 materialize_qualified_release_artifact,
18 },
19};
20use std::{
21 io,
22 path::{Path, PathBuf},
23};
24
25use canic_core::{
26 bootstrap::compiled::ComponentTopology,
27 ids::{CanisterRole, ReleaseBuildId},
28 role_contract::ProtocolProfileDigest,
29};
30use sha2::{Digest, Sha256};
31use thiserror::Error as ThisError;
32
33use super::{
34 ApplicationArtifactBuildOutput, ApplicationArtifactBuildTarget, ApplicationArtifactUnion,
35 ApplicationReleaseSetError,
36};
37
38pub const APPLICATION_ARTIFACT_UNION_FILE: &str = "application-artifact-union.json";
39
40#[derive(Clone, Debug, Eq, PartialEq)]
47pub struct ApplicationArtifactFileBuildOutput {
48 pub role: CanisterRole,
49 pub package: String,
50 pub release_build_id: ReleaseBuildId,
51 pub wasm_path: PathBuf,
52 pub wasm_gz_path: PathBuf,
53 pub candid_sha256: [u8; 32],
54 pub protocol_profile_digest: ProtocolProfileDigest,
55}
56
57#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct PersistedApplicationArtifactUnion {
65 pub union: ApplicationArtifactUnion,
66 pub digest: [u8; 32],
67 pub path: PathBuf,
68}
69
70#[derive(Debug, ThisError)]
77pub enum ApplicationArtifactUnionPersistenceError {
78 #[error("application artifact {role} {kind} path is outside the ICP root: {path}")]
79 ArtifactOutsideRoot {
80 role: CanisterRole,
81 kind: &'static str,
82 path: PathBuf,
83 },
84
85 #[error("failed to read application artifact {role} {kind} at {path}: {source}")]
86 ArtifactRead {
87 role: CanisterRole,
88 kind: &'static str,
89 path: PathBuf,
90 #[source]
91 source: io::Error,
92 },
93
94 #[error("application artifact union already exists with different canonical bytes: {path}")]
95 ConflictingUnion { path: PathBuf },
96
97 #[error("finalized release build {release_build_id} has no exact application artifact union")]
98 FinalizedWithoutExactUnion { release_build_id: ReleaseBuildId },
99
100 #[error("application artifact {role} has an invalid {kind} path: {path}")]
101 InvalidArtifactPath {
102 role: CanisterRole,
103 kind: &'static str,
104 path: PathBuf,
105 },
106
107 #[error("invalid application artifact union {path}: {reason}")]
108 InvalidUnionDocument { path: PathBuf, reason: String },
109
110 #[error("application artifact union is missing: {path}")]
111 MissingUnion { path: PathBuf },
112
113 #[error(
114 "application artifact {role} raw Wasm at {path} does not embed release build {release_build_id}"
115 )]
116 MissingReleaseBuildIdentity {
117 role: CanisterRole,
118 release_build_id: ReleaseBuildId,
119 path: PathBuf,
120 },
121
122 #[error("application artifact {role} has a non-UTF-8 {kind} path: {path}")]
123 NonUtf8ArtifactPath {
124 role: CanisterRole,
125 kind: &'static str,
126 path: PathBuf,
127 },
128
129 #[error(transparent)]
130 ReleaseBuild(#[from] ReleaseBuildPlanError),
131
132 #[error(transparent)]
133 ReleaseSet(#[from] ApplicationReleaseSetError),
134
135 #[error("application artifact {role} {kind} is not a regular no-follow file: {path}")]
136 UnsafeArtifact {
137 role: CanisterRole,
138 kind: &'static str,
139 path: PathBuf,
140 },
141
142 #[error("application artifact union is not a regular no-follow file: {path}")]
143 UnsafeUnion { path: PathBuf },
144
145 #[error("failed to access application artifact union {path}: {source}")]
146 UnionIo {
147 path: PathBuf,
148 #[source]
149 source: io::Error,
150 },
151}
152
153pub fn compile_and_persist_application_artifact_union(
155 root: &Path,
156 topology: &ComponentTopology,
157 release_build_id: ReleaseBuildId,
158 targets: &[ApplicationArtifactBuildTarget],
159 outputs: &[ApplicationArtifactFileBuildOutput],
160) -> Result<PersistedApplicationArtifactUnion, ApplicationArtifactUnionPersistenceError> {
161 let release_build = load_release_build_plan(root, release_build_id)?;
162 ApplicationArtifactUnion::validate_build_targets(topology, targets)?;
163 let mut output_roles = outputs
164 .iter()
165 .map(|output| output.role.clone())
166 .collect::<Vec<_>>();
167 ApplicationArtifactUnion::validate_build_output_roles(topology, &mut output_roles)?;
168 let materialized = outputs
169 .iter()
170 .map(|output| materialize_build_output(root, release_build_id, output))
171 .collect::<Result<Vec<_>, _>>()?;
172 let union =
173 ApplicationArtifactUnion::compile(topology, release_build_id, targets, &materialized)?;
174 let expected = persisted_union(root, topology, union)?;
175 let existing = load_optional_persisted_union(&expected.path, topology, release_build_id)?;
176
177 if matches!(release_build.state, ReleaseBuildPlanState::Finalized { .. }) {
178 return match existing {
179 Some(observed) if observed.union == expected.union => Ok(observed),
180 _ => Err(
181 ApplicationArtifactUnionPersistenceError::FinalizedWithoutExactUnion {
182 release_build_id,
183 },
184 ),
185 };
186 }
187
188 if let Some(observed) = existing {
189 return if observed.union == expected.union {
190 Ok(observed)
191 } else {
192 Err(ApplicationArtifactUnionPersistenceError::ConflictingUnion {
193 path: expected.path,
194 })
195 };
196 }
197
198 let canonical_bytes = expected.union.canonical_bytes(topology)?;
199 if let Err(source) = create_new_bytes_with_parents(&expected.path, &canonical_bytes) {
200 match load_optional_persisted_union(&expected.path, topology, release_build_id) {
201 Ok(Some(observed)) if observed.union == expected.union => return Ok(observed),
202 Ok(Some(_)) if source.kind() == io::ErrorKind::AlreadyExists => {
203 return Err(ApplicationArtifactUnionPersistenceError::ConflictingUnion {
204 path: expected.path,
205 });
206 }
207 _ => {
208 return Err(ApplicationArtifactUnionPersistenceError::UnionIo {
209 path: expected.path,
210 source,
211 });
212 }
213 }
214 }
215
216 load_persisted_application_artifact_union(root, topology, release_build_id)
217}
218
219pub fn load_persisted_application_artifact_union(
221 root: &Path,
222 topology: &ComponentTopology,
223 release_build_id: ReleaseBuildId,
224) -> Result<PersistedApplicationArtifactUnion, ApplicationArtifactUnionPersistenceError> {
225 let path = application_artifact_union_path(root, release_build_id);
226 load_optional_persisted_union(&path, topology, release_build_id)?
227 .ok_or(ApplicationArtifactUnionPersistenceError::MissingUnion { path })
228}
229
230fn materialize_build_output(
231 root: &Path,
232 release_build_id: ReleaseBuildId,
233 output: &ApplicationArtifactFileBuildOutput,
234) -> Result<ApplicationArtifactBuildOutput, ApplicationArtifactUnionPersistenceError> {
235 if output.release_build_id != release_build_id {
236 return Err(ApplicationReleaseSetError::ReleaseBuildMismatch {
237 role: output.role.clone(),
238 expected: release_build_id,
239 actual: output.release_build_id,
240 }
241 .into());
242 }
243
244 let wasm = materialize_artifact(root, &output.role, "raw Wasm", &output.wasm_path)?;
245 if !contains_release_build_identity(&wasm.bytes, release_build_id) {
246 return Err(
247 ApplicationArtifactUnionPersistenceError::MissingReleaseBuildIdentity {
248 role: output.role.clone(),
249 release_build_id,
250 path: output.wasm_path.clone(),
251 },
252 );
253 }
254 let wasm_gz = materialize_artifact(root, &output.role, "gzip Wasm", &output.wasm_gz_path)?;
255 Ok(ApplicationArtifactBuildOutput {
256 role: output.role.clone(),
257 package: output.package.clone(),
258 release_build_id: output.release_build_id,
259 wasm_relative_path: wasm.relative_path,
260 wasm: wasm.bytes,
261 wasm_gz_relative_path: wasm_gz.relative_path,
262 wasm_gz: wasm_gz.bytes,
263 candid_sha256: output.candid_sha256,
264 protocol_profile_digest: output.protocol_profile_digest,
265 })
266}
267
268fn materialize_artifact(
269 root: &Path,
270 role: &CanisterRole,
271 kind: &'static str,
272 path: &Path,
273) -> Result<
274 crate::release_set::artifact::MaterializedReleaseArtifact,
275 ApplicationArtifactUnionPersistenceError,
276> {
277 materialize_qualified_release_artifact(root, path).map_err(|error| match error {
278 ReleaseArtifactMaterializationError::InvalidPath => {
279 ApplicationArtifactUnionPersistenceError::InvalidArtifactPath {
280 role: role.clone(),
281 kind,
282 path: path.to_path_buf(),
283 }
284 }
285 ReleaseArtifactMaterializationError::NonUtf8Path => {
286 ApplicationArtifactUnionPersistenceError::NonUtf8ArtifactPath {
287 role: role.clone(),
288 kind,
289 path: path.to_path_buf(),
290 }
291 }
292 ReleaseArtifactMaterializationError::OutsideRoot => {
293 ApplicationArtifactUnionPersistenceError::ArtifactOutsideRoot {
294 role: role.clone(),
295 kind,
296 path: path.to_path_buf(),
297 }
298 }
299 ReleaseArtifactMaterializationError::Read(source) => {
300 ApplicationArtifactUnionPersistenceError::ArtifactRead {
301 role: role.clone(),
302 kind,
303 path: path.to_path_buf(),
304 source,
305 }
306 }
307 ReleaseArtifactMaterializationError::UnsafeFile => {
308 ApplicationArtifactUnionPersistenceError::UnsafeArtifact {
309 role: role.clone(),
310 kind,
311 path: path.to_path_buf(),
312 }
313 }
314 })
315}
316
317fn persisted_union(
318 root: &Path,
319 topology: &ComponentTopology,
320 union: ApplicationArtifactUnion,
321) -> Result<PersistedApplicationArtifactUnion, ApplicationArtifactUnionPersistenceError> {
322 let digest = union.digest(topology)?;
323 let path = application_artifact_union_path(root, union.release_build_id);
324 Ok(PersistedApplicationArtifactUnion {
325 union,
326 digest,
327 path,
328 })
329}
330
331fn load_optional_persisted_union(
332 path: &Path,
333 topology: &ComponentTopology,
334 release_build_id: ReleaseBuildId,
335) -> Result<Option<PersistedApplicationArtifactUnion>, ApplicationArtifactUnionPersistenceError> {
336 let bytes = match read_optional_regular_bytes(path) {
337 Ok(bytes) => bytes,
338 Err(RegularFileReadError::NotRegular) => {
339 return Err(ApplicationArtifactUnionPersistenceError::UnsafeUnion {
340 path: path.to_path_buf(),
341 });
342 }
343 Err(RegularFileReadError::Io(source)) => {
344 return Err(ApplicationArtifactUnionPersistenceError::UnionIo {
345 path: path.to_path_buf(),
346 source,
347 });
348 }
349 #[cfg(not(unix))]
350 Err(RegularFileReadError::UnsupportedPlatform) => {
351 return Err(ApplicationArtifactUnionPersistenceError::UnionIo {
352 path: path.to_path_buf(),
353 source: io::Error::new(
354 io::ErrorKind::Unsupported,
355 "regular no-follow union reads are unsupported",
356 ),
357 });
358 }
359 };
360 let Some(bytes) = bytes else {
361 return Ok(None);
362 };
363
364 let union: ApplicationArtifactUnion = serde_json::from_slice(&bytes).map_err(|error| {
365 ApplicationArtifactUnionPersistenceError::InvalidUnionDocument {
366 path: path.to_path_buf(),
367 reason: error.to_string(),
368 }
369 })?;
370 if union.release_build_id != release_build_id {
371 return Err(
372 ApplicationArtifactUnionPersistenceError::InvalidUnionDocument {
373 path: path.to_path_buf(),
374 reason: format!(
375 "union release build {} does not match path release build {release_build_id}",
376 union.release_build_id
377 ),
378 },
379 );
380 }
381 let canonical = union.canonical_bytes(topology)?;
382 if canonical != bytes {
383 return Err(
384 ApplicationArtifactUnionPersistenceError::InvalidUnionDocument {
385 path: path.to_path_buf(),
386 reason: "union bytes are not canonical".to_string(),
387 },
388 );
389 }
390
391 Ok(Some(PersistedApplicationArtifactUnion {
392 digest: Sha256::digest(&bytes).into(),
393 union,
394 path: path.to_path_buf(),
395 }))
396}
397
398fn application_artifact_union_path(root: &Path, release_build_id: ReleaseBuildId) -> PathBuf {
399 root.join(".canic")
400 .join("release-builds")
401 .join(release_build_id.to_string())
402 .join(APPLICATION_ARTIFACT_UNION_FILE)
403}