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 collections::BTreeMap,
21 io,
22 path::{Path, PathBuf},
23};
24
25use canic_core::{
26 bootstrap::compiled::ComponentTopology,
27 ids::{CanisterRole, ReleaseBuildId},
28};
29use sha2::{Digest, Sha256};
30use thiserror::Error as ThisError;
31
32use super::{
33 ApplicationArtifactBuildOutput, ApplicationArtifactBuildTarget, ApplicationArtifactUnion,
34 ApplicationReleaseSetError,
35};
36
37const APPLICATION_ARTIFACT_UNION_FILE: &str = "application-artifact-union.json";
38
39#[derive(Clone, Debug, Eq, PartialEq)]
46pub struct ApplicationArtifactFileBuildOutput {
47 pub role: CanisterRole,
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 targets_by_role = targets
156 .iter()
157 .map(|target| (&target.role, target))
158 .collect::<BTreeMap<_, _>>();
159 let materialized = outputs
160 .iter()
161 .map(|output| materialize_build_output(root, release_build_id, &targets_by_role, output))
162 .collect::<Result<Vec<_>, _>>()?;
163 let union =
164 ApplicationArtifactUnion::compile(topology, release_build_id, targets, &materialized)?;
165 let expected = persisted_union(root, topology, union)?;
166 let existing = load_optional_persisted_union(&expected.path, topology, release_build_id)?;
167
168 if matches!(release_build.state, ReleaseBuildPlanState::Finalized { .. }) {
169 return match existing {
170 Some(observed) if observed.union == expected.union => Ok(observed),
171 _ => Err(
172 ApplicationArtifactUnionPersistenceError::FinalizedWithoutExactUnion {
173 release_build_id,
174 },
175 ),
176 };
177 }
178
179 if let Some(observed) = existing {
180 return if observed.union == expected.union {
181 Ok(observed)
182 } else {
183 Err(ApplicationArtifactUnionPersistenceError::ConflictingUnion {
184 path: expected.path,
185 })
186 };
187 }
188
189 let canonical_bytes = expected.union.canonical_bytes(topology)?;
190 if let Err(source) = create_new_bytes_with_parents(&expected.path, &canonical_bytes) {
191 match load_optional_persisted_union(&expected.path, topology, release_build_id) {
192 Ok(Some(observed)) if observed.union == expected.union => return Ok(observed),
193 Ok(Some(_)) if source.kind() == io::ErrorKind::AlreadyExists => {
194 return Err(ApplicationArtifactUnionPersistenceError::ConflictingUnion {
195 path: expected.path,
196 });
197 }
198 _ => {
199 return Err(ApplicationArtifactUnionPersistenceError::UnionIo {
200 path: expected.path,
201 source,
202 });
203 }
204 }
205 }
206
207 load_persisted_application_artifact_union(root, topology, release_build_id)
208}
209
210pub fn load_persisted_application_artifact_union(
212 root: &Path,
213 topology: &ComponentTopology,
214 release_build_id: ReleaseBuildId,
215) -> Result<PersistedApplicationArtifactUnion, ApplicationArtifactUnionPersistenceError> {
216 let path = application_artifact_union_path(root, release_build_id);
217 load_optional_persisted_union(&path, topology, release_build_id)?
218 .ok_or(ApplicationArtifactUnionPersistenceError::MissingUnion { path })
219}
220
221fn materialize_build_output(
222 root: &Path,
223 release_build_id: ReleaseBuildId,
224 targets: &BTreeMap<&CanisterRole, &ApplicationArtifactBuildTarget>,
225 output: &ApplicationArtifactFileBuildOutput,
226) -> Result<ApplicationArtifactBuildOutput, ApplicationArtifactUnionPersistenceError> {
227 if output.release_build_id != release_build_id {
228 return Err(ApplicationReleaseSetError::ReleaseBuildMismatch {
229 role: output.role.clone(),
230 expected: release_build_id,
231 actual: output.release_build_id,
232 }
233 .into());
234 }
235
236 let package = targets
237 .get(&output.role)
238 .ok_or_else(|| ApplicationReleaseSetError::MissingBuildTarget {
239 role: output.role.clone(),
240 })?
241 .package
242 .clone();
243 let wasm = materialize_artifact(root, &output.role, "raw Wasm", &output.wasm_path)?;
244 let wasm_gz = materialize_artifact(root, &output.role, "gzip Wasm", &output.wasm_gz_path)?;
245 Ok(ApplicationArtifactBuildOutput {
246 role: output.role.clone(),
247 package,
248 release_build_id: output.release_build_id,
249 wasm_relative_path: wasm.relative_path,
250 wasm: wasm.bytes,
251 wasm_gz_relative_path: wasm_gz.relative_path,
252 wasm_gz: wasm_gz.bytes,
253 })
254}
255
256fn materialize_artifact(
257 root: &Path,
258 role: &CanisterRole,
259 kind: &'static str,
260 path: &Path,
261) -> Result<
262 crate::release_set::artifact::MaterializedReleaseArtifact,
263 ApplicationArtifactUnionPersistenceError,
264> {
265 materialize_qualified_release_artifact(root, path).map_err(|error| match error {
266 ReleaseArtifactMaterializationError::InvalidPath => {
267 ApplicationArtifactUnionPersistenceError::InvalidArtifactPath {
268 role: role.clone(),
269 kind,
270 path: path.to_path_buf(),
271 }
272 }
273 ReleaseArtifactMaterializationError::NonUtf8Path => {
274 ApplicationArtifactUnionPersistenceError::NonUtf8ArtifactPath {
275 role: role.clone(),
276 kind,
277 path: path.to_path_buf(),
278 }
279 }
280 ReleaseArtifactMaterializationError::OutsideRoot => {
281 ApplicationArtifactUnionPersistenceError::ArtifactOutsideRoot {
282 role: role.clone(),
283 kind,
284 path: path.to_path_buf(),
285 }
286 }
287 ReleaseArtifactMaterializationError::Read(source) => {
288 ApplicationArtifactUnionPersistenceError::ArtifactRead {
289 role: role.clone(),
290 kind,
291 path: path.to_path_buf(),
292 source,
293 }
294 }
295 ReleaseArtifactMaterializationError::UnsafeFile => {
296 ApplicationArtifactUnionPersistenceError::UnsafeArtifact {
297 role: role.clone(),
298 kind,
299 path: path.to_path_buf(),
300 }
301 }
302 })
303}
304
305fn persisted_union(
306 root: &Path,
307 topology: &ComponentTopology,
308 union: ApplicationArtifactUnion,
309) -> Result<PersistedApplicationArtifactUnion, ApplicationArtifactUnionPersistenceError> {
310 let digest = union.digest(topology)?;
311 let path = application_artifact_union_path(root, union.release_build_id);
312 Ok(PersistedApplicationArtifactUnion {
313 union,
314 digest,
315 path,
316 })
317}
318
319fn load_optional_persisted_union(
320 path: &Path,
321 topology: &ComponentTopology,
322 release_build_id: ReleaseBuildId,
323) -> Result<Option<PersistedApplicationArtifactUnion>, ApplicationArtifactUnionPersistenceError> {
324 let bytes = match read_optional_regular_bytes(path) {
325 Ok(bytes) => bytes,
326 Err(RegularFileReadError::NotRegular) => {
327 return Err(ApplicationArtifactUnionPersistenceError::UnsafeUnion {
328 path: path.to_path_buf(),
329 });
330 }
331 Err(RegularFileReadError::Io(source)) => {
332 return Err(ApplicationArtifactUnionPersistenceError::UnionIo {
333 path: path.to_path_buf(),
334 source,
335 });
336 }
337 #[cfg(not(unix))]
338 Err(RegularFileReadError::UnsupportedPlatform) => {
339 return Err(ApplicationArtifactUnionPersistenceError::UnionIo {
340 path: path.to_path_buf(),
341 source: io::Error::new(
342 io::ErrorKind::Unsupported,
343 "regular no-follow union reads are unsupported",
344 ),
345 });
346 }
347 };
348 let Some(bytes) = bytes else {
349 return Ok(None);
350 };
351
352 let union: ApplicationArtifactUnion = serde_json::from_slice(&bytes).map_err(|error| {
353 ApplicationArtifactUnionPersistenceError::InvalidUnionDocument {
354 path: path.to_path_buf(),
355 reason: error.to_string(),
356 }
357 })?;
358 if union.release_build_id != release_build_id {
359 return Err(
360 ApplicationArtifactUnionPersistenceError::InvalidUnionDocument {
361 path: path.to_path_buf(),
362 reason: format!(
363 "union release build {} does not match path release build {release_build_id}",
364 union.release_build_id
365 ),
366 },
367 );
368 }
369 let canonical = union.canonical_bytes(topology)?;
370 if canonical != bytes {
371 return Err(
372 ApplicationArtifactUnionPersistenceError::InvalidUnionDocument {
373 path: path.to_path_buf(),
374 reason: "union bytes are not canonical".to_string(),
375 },
376 );
377 }
378
379 Ok(Some(PersistedApplicationArtifactUnion {
380 digest: Sha256::digest(&bytes).into(),
381 union,
382 path: path.to_path_buf(),
383 }))
384}
385
386fn application_artifact_union_path(root: &Path, release_build_id: ReleaseBuildId) -> PathBuf {
387 root.join(".canic")
388 .join("release-builds")
389 .join(release_build_id.to_string())
390 .join(APPLICATION_ARTIFACT_UNION_FILE)
391}