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