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