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_host::{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 retained = load_retained_application_artifact_union(root, release_build_id)?;
226 retained.union.validate_against(topology)?;
227 Ok(retained)
228}
229
230pub(in crate::release_set) fn load_retained_application_artifact_union(
232 root: &Path,
233 release_build_id: ReleaseBuildId,
234) -> Result<PersistedApplicationArtifactUnion, ApplicationArtifactUnionPersistenceError> {
235 let path = application_artifact_union_path(root, release_build_id);
236 load_optional_retained_union(&path, release_build_id)?
237 .ok_or(ApplicationArtifactUnionPersistenceError::MissingUnion { path })
238}
239
240fn materialize_build_output(
241 root: &Path,
242 release_build_id: ReleaseBuildId,
243 output: &ApplicationArtifactFileBuildOutput,
244) -> Result<ApplicationArtifactBuildOutput, ApplicationArtifactUnionPersistenceError> {
245 if output.release_build_id != release_build_id {
246 return Err(ApplicationReleaseSetError::ReleaseBuildMismatch {
247 role: output.role.clone(),
248 expected: release_build_id,
249 actual: output.release_build_id,
250 }
251 .into());
252 }
253
254 let wasm = materialize_artifact(root, &output.role, "raw Wasm", &output.wasm_path)?;
255 if !contains_release_build_identity(&wasm.bytes, release_build_id) {
256 return Err(
257 ApplicationArtifactUnionPersistenceError::MissingReleaseBuildIdentity {
258 role: output.role.clone(),
259 release_build_id,
260 path: output.wasm_path.clone(),
261 },
262 );
263 }
264 let wasm_gz = materialize_artifact(root, &output.role, "gzip Wasm", &output.wasm_gz_path)?;
265 Ok(ApplicationArtifactBuildOutput {
266 role: output.role.clone(),
267 package: output.package.clone(),
268 release_build_id: output.release_build_id,
269 wasm_relative_path: wasm.relative_path,
270 wasm: wasm.bytes,
271 wasm_gz_relative_path: wasm_gz.relative_path,
272 wasm_gz: wasm_gz.bytes,
273 candid_sha256: output.candid_sha256,
274 protocol_profile_digest: output.protocol_profile_digest,
275 })
276}
277
278fn materialize_artifact(
279 root: &Path,
280 role: &CanisterRole,
281 kind: &'static str,
282 path: &Path,
283) -> Result<
284 crate::release_set::artifact::MaterializedReleaseArtifact,
285 ApplicationArtifactUnionPersistenceError,
286> {
287 materialize_qualified_release_artifact(root, path).map_err(|error| match error {
288 ReleaseArtifactMaterializationError::InvalidPath => {
289 ApplicationArtifactUnionPersistenceError::InvalidArtifactPath {
290 role: role.clone(),
291 kind,
292 path: path.to_path_buf(),
293 }
294 }
295 ReleaseArtifactMaterializationError::NonUtf8Path => {
296 ApplicationArtifactUnionPersistenceError::NonUtf8ArtifactPath {
297 role: role.clone(),
298 kind,
299 path: path.to_path_buf(),
300 }
301 }
302 ReleaseArtifactMaterializationError::OutsideRoot => {
303 ApplicationArtifactUnionPersistenceError::ArtifactOutsideRoot {
304 role: role.clone(),
305 kind,
306 path: path.to_path_buf(),
307 }
308 }
309 ReleaseArtifactMaterializationError::Read(source) => {
310 ApplicationArtifactUnionPersistenceError::ArtifactRead {
311 role: role.clone(),
312 kind,
313 path: path.to_path_buf(),
314 source,
315 }
316 }
317 ReleaseArtifactMaterializationError::UnsafeFile => {
318 ApplicationArtifactUnionPersistenceError::UnsafeArtifact {
319 role: role.clone(),
320 kind,
321 path: path.to_path_buf(),
322 }
323 }
324 })
325}
326
327fn persisted_union(
328 root: &Path,
329 topology: &ComponentTopology,
330 union: ApplicationArtifactUnion,
331) -> Result<PersistedApplicationArtifactUnion, ApplicationArtifactUnionPersistenceError> {
332 let digest = union.digest(topology)?;
333 let path = application_artifact_union_path(root, union.release_build_id);
334 Ok(PersistedApplicationArtifactUnion {
335 union,
336 digest,
337 path,
338 })
339}
340
341fn load_optional_persisted_union(
342 path: &Path,
343 topology: &ComponentTopology,
344 release_build_id: ReleaseBuildId,
345) -> Result<Option<PersistedApplicationArtifactUnion>, ApplicationArtifactUnionPersistenceError> {
346 let retained = load_optional_retained_union(path, release_build_id)?;
347 if let Some(retained) = &retained {
348 retained.union.validate_against(topology)?;
349 }
350 Ok(retained)
351}
352
353fn load_optional_retained_union(
354 path: &Path,
355 release_build_id: ReleaseBuildId,
356) -> Result<Option<PersistedApplicationArtifactUnion>, ApplicationArtifactUnionPersistenceError> {
357 let bytes = match read_optional_regular_bytes(path) {
358 Ok(bytes) => bytes,
359 Err(RegularFileReadError::NotRegular) => {
360 return Err(ApplicationArtifactUnionPersistenceError::UnsafeUnion {
361 path: path.to_path_buf(),
362 });
363 }
364 Err(RegularFileReadError::Io(source)) => {
365 return Err(ApplicationArtifactUnionPersistenceError::UnionIo {
366 path: path.to_path_buf(),
367 source,
368 });
369 }
370 #[cfg(not(unix))]
371 Err(RegularFileReadError::UnsupportedPlatform) => {
372 return Err(ApplicationArtifactUnionPersistenceError::UnionIo {
373 path: path.to_path_buf(),
374 source: io::Error::new(
375 io::ErrorKind::Unsupported,
376 "regular no-follow union reads are unsupported",
377 ),
378 });
379 }
380 };
381 let Some(bytes) = bytes else {
382 return Ok(None);
383 };
384
385 let union: ApplicationArtifactUnion = serde_json::from_slice(&bytes).map_err(|error| {
386 ApplicationArtifactUnionPersistenceError::InvalidUnionDocument {
387 path: path.to_path_buf(),
388 reason: error.to_string(),
389 }
390 })?;
391 if union.release_build_id != release_build_id {
392 return Err(
393 ApplicationArtifactUnionPersistenceError::InvalidUnionDocument {
394 path: path.to_path_buf(),
395 reason: format!(
396 "union release build {} does not match path release build {release_build_id}",
397 union.release_build_id
398 ),
399 },
400 );
401 }
402 union.validate_retained_shape()?;
403 let canonical =
404 serde_json::to_vec(&union).map_err(ApplicationReleaseSetError::Serialization)?;
405 if canonical != bytes {
406 return Err(
407 ApplicationArtifactUnionPersistenceError::InvalidUnionDocument {
408 path: path.to_path_buf(),
409 reason: "union bytes are not canonical".to_string(),
410 },
411 );
412 }
413
414 Ok(Some(PersistedApplicationArtifactUnion {
415 digest: Sha256::digest(&bytes).into(),
416 union,
417 path: path.to_path_buf(),
418 }))
419}
420
421fn application_artifact_union_path(root: &Path, release_build_id: ReleaseBuildId) -> PathBuf {
422 root.join(".canic")
423 .join("release-builds")
424 .join(release_build_id.to_string())
425 .join(APPLICATION_ARTIFACT_UNION_FILE)
426}