1use std::collections::{BTreeMap, BTreeSet};
20use std::io::{BufRead, BufReader};
21use std::path::{Path, PathBuf};
22use std::process::{Command, Stdio};
23
24use crate::digest;
26use serde::Deserialize;
27
28use super::{BuildCancellation, Error, metadata::Metadata, process::Process};
30use crate::config::{BuildBackend, BuildProfile, BuildSettings};
31
32type BuildMessages = (Vec<Artifact>, BTreeSet<(String, PathBuf)>);
39
40#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
43#[serde(deny_unknown_fields)]
44pub struct Artifact {
45 pub producer: String,
46 pub name: String,
47 pub path: PathBuf,
48 pub sha256: String,
49}
50
51#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
52#[serde(deny_unknown_fields)]
53pub struct Schema {
54 pub producer: String,
55 pub catalog: String,
56 pub directory: PathBuf,
57 pub files: BTreeMap<String, String>,
58}
59
60#[derive(Debug, serde::Serialize, serde::Deserialize)]
61#[serde(deny_unknown_fields)]
62pub struct Build {
63 pub backend: BuildBackend,
64 pub profile: BuildProfile,
65 pub target: String,
66 pub artifacts: Vec<Artifact>,
67 pub schemas: Vec<Schema>,
68}
69
70#[derive(Debug, serde::Serialize, serde::Deserialize)]
71pub struct PackageOutput {
72 pub producer: String,
73 pub path: PathBuf,
74 pub sha256: String,
75}
76
77#[derive(Deserialize)]
79struct Capture {
80 schema: u32,
81 profile: BuildProfile,
82 target: String,
83 command: Vec<String>,
84 inputs: CaptureInputs,
85 messages: CaptureFile,
86 metadata: CaptureFile,
87 artifacts: BTreeMap<PathBuf, String>,
88 generated: BTreeMap<PathBuf, String>,
89}
90
91#[derive(Deserialize)]
92struct CaptureInputs {
93 rustc: String,
94}
95
96#[derive(Deserialize)]
97struct CaptureFile {
98 sha256: String,
99}
100
101#[derive(Deserialize)]
102struct Message {
103 reason: String,
104 package_id: Option<String>,
105 executable: Option<PathBuf>,
106
107 #[serde(default)]
108 filenames: Vec<PathBuf>,
109
110 target: Option<Target>,
111 out_dir: Option<PathBuf>,
112 message: Option<Diagnostic>,
113 success: Option<bool>,
114 profile: Option<Profile>,
115}
116
117#[derive(Deserialize)]
118struct Diagnostic {
119 rendered: Option<String>,
120}
121
122#[derive(Deserialize)]
123struct Profile {
124 test: bool,
125}
126
127#[derive(Deserialize)]
128struct Target {
129 name: String,
130 kind: Vec<String>,
131}
132
133impl Artifact {
140 pub fn read(producer: String, name: String, path: PathBuf) -> Result<Self, Error> {
142 safe_name(&producer)?;
143 safe_name(&name)?;
144 use sha2::{Digest, Sha256};
145 if path.is_symlink() || !path.is_file() {
146 return Err(Error::Settings("artifact must be a regular file".into()));
147 }
148 let mut file = BufReader::new(std::fs::File::open(&path)?);
149 let mut hash = Sha256::new();
150 loop {
151 let bytes = file.fill_buf()?;
152 if bytes.is_empty() {
153 break;
154 }
155 hash.update(bytes);
156 let length = bytes.len();
157 file.consume(length);
158 }
159 let sha256 = hash.finalize().iter().map(|byte| format!("{byte:02x}")).collect();
160 Ok(Self { producer, name, path, sha256 })
161 }
162}
163
164impl Schema {
165 pub fn read(producer: String, catalog: String, directory: PathBuf) -> Result<Self, Error> {
166 safe_name(&producer)?;
167 safe_name(&catalog)?;
168 let files = schema_files(&directory)?.into_iter().map(|(name, bytes)| (name, digest(&bytes))).collect();
169 Ok(Self { producer, catalog, directory, files })
170 }
171}
172
173impl Build {
174 pub fn from_cargo_outputs(
177 mut messages: impl BufRead, mut metadata: impl std::io::Read, receipt: impl std::io::Read, settings: &BuildSettings,
178 ) -> Result<Self, Error> {
179 validate_backend(settings)?;
180 let receipt: Capture = serde_json::from_reader(receipt)?;
181 if receipt.schema != 1 || receipt.command.is_empty() || receipt.target.is_empty() {
182 return Err(Error::Settings("unsupported or incomplete archive producer receipt".into()));
183 }
184 if receipt.profile != settings.profile || settings.target.as_ref().is_some_and(|target| target != &receipt.target) {
185 return Err(Error::Settings("requested profile or target disagrees with archive producer receipt".into()));
186 }
187 let mut message_bytes = Vec::new();
188 messages.read_to_end(&mut message_bytes)?;
189 let mut metadata_bytes = Vec::new();
190 metadata.read_to_end(&mut metadata_bytes)?;
191 if digest(&message_bytes) != receipt.messages.sha256 || digest(&metadata_bytes) != receipt.metadata.sha256 {
192 return Err(Error::Settings("captured Cargo streams disagree with archive producer byte stamps".into()));
193 }
194 let metadata: Metadata = serde_json::from_slice(&metadata_bytes)?;
195 let packages = metadata.select(settings)?;
196 if packages.is_empty() || packages.values().collect::<BTreeSet<_>>().len() != packages.len() {
197 return Err(Error::Settings("collection needs unambiguous selected Cargo packages".into()));
198 }
199 if (settings.backend == BuildBackend::Guest) != receipt.target.starts_with("wasm32-") {
200 return Err(Error::Settings("backend and producer target disagree".into()));
201 }
202 let (artifacts, outputs) = read_messages(std::io::Cursor::new(message_bytes), &packages)?;
203 let profile_directory = match receipt.profile {
204 BuildProfile::Dev | BuildProfile::Test => "debug",
205 BuildProfile::Release | BuildProfile::Bench => "release",
206 };
207 let native = receipt.inputs.rustc.lines().find_map(|line| line.strip_prefix("host: "));
208 let expected = metadata.target_directory.join(&receipt.target).join(profile_directory);
209 let host = metadata.target_directory.join(profile_directory);
210 let matches = |path: &Path| path.starts_with(&expected) || (native == Some(receipt.target.as_str()) && path.starts_with(&host));
211 if artifacts.iter().any(|artifact| !matches(&artifact.path)) || outputs.iter().any(|(_, directory)| !matches(directory)) {
212 return Err(Error::Settings("captured artifacts do not match the producer target/profile output directories".into()));
213 }
214 let provided = artifacts.iter().map(|artifact| artifact.producer.as_str()).collect::<BTreeSet<_>>();
215 if packages.values().any(|name| !provided.contains(name.as_str())) {
216 return Err(Error::Settings("captured build is missing a selected package artifact".into()));
217 }
218 if provided.len() != artifacts.len() {
219 return Err(Error::Settings("captured build contains ambiguous package artifacts".into()));
220 }
221 for artifact in &artifacts {
222 if receipt.artifacts.get(&artifact.path) != Some(&artifact.sha256) {
223 return Err(Error::ArtifactChanged(artifact.path.clone()));
224 }
225 }
226 finish_build(
227 settings.backend,
228 receipt.profile,
229 receipt.target,
230 artifacts,
231 outputs,
232 &BuildCancellation::default(),
233 Some(&receipt.generated),
234 )
235 }
236
237 pub fn relocate(&mut self, from: &Path, to: &Path) -> Result<(), Error> {
239 let relocate = |path: &Path| {
240 path.strip_prefix(from)
241 .map(|relative| to.join(relative))
242 .map_err(|_| Error::Settings("build receipt paths do not share the selected source root".into()))
243 };
244 let artifacts = self.artifacts.iter().map(|artifact| relocate(&artifact.path)).collect::<Result<Vec<_>, _>>()?;
245 let schemas = self.schemas.iter().map(|schema| relocate(&schema.directory)).collect::<Result<Vec<_>, _>>()?;
246 for (artifact, path) in self.artifacts.iter_mut().zip(artifacts) {
247 artifact.path = path;
248 }
249 for (schema, directory) in self.schemas.iter_mut().zip(schemas) {
250 schema.directory = directory;
251 }
252 Ok(())
253 }
254}
255
256pub fn build(settings: &BuildSettings) -> Result<Build, Error> {
259 build_with_cancellation(settings, &BuildCancellation::default())
260}
261
262pub fn build_with_cancellation(settings: &BuildSettings, cancellation: &BuildCancellation) -> Result<Build, Error> {
263 validate_backend(settings)?;
264 let metadata = Metadata::read(settings, cancellation)?;
265 build_resolved(settings, cancellation, metadata)
266}
267
268pub(super) fn build_resolved(settings: &BuildSettings, cancellation: &BuildCancellation, metadata: Metadata) -> Result<Build, Error> {
269 let manifest = settings.manifest.canonicalize()?;
270 let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
271 let directory = manifest.parent().ok_or_else(|| Error::Settings("manifest has no project directory".into()))?;
272 let selected = metadata.select(settings)?;
273 let target = match (&settings.backend, &settings.target) {
274 (BuildBackend::Guest, None) => "wasm32-unknown-unknown".to_owned(),
275 (_, Some(target)) => target.clone(),
276 (BuildBackend::Sidecar, None) => {
277 let mut command = Command::new("rustc");
278 command.args(["-vV"]).stdout(Stdio::piped()).stderr(Stdio::piped());
279 let version = Process::spawn(command, cancellation)?.output()?;
280 cancellation.check()?;
281 if !version.status.success() {
282 return Err(Error::Cargo("rustc host discovery failed".into()));
283 }
284 String::from_utf8_lossy(&version.stdout)
285 .lines()
286 .find_map(|line| line.strip_prefix("host: "))
287 .ok_or_else(|| Error::Cargo("rustc did not report its host target".into()))?
288 .to_owned()
289 }
290 };
291 if (settings.backend == BuildBackend::Guest) != target.starts_with("wasm32-") {
292 return Err(Error::Settings("backend and selected target disagree".into()));
293 }
294 let mut command = Command::new(cargo);
295 command.current_dir(directory);
296 command.args(["build", "--locked", "--message-format=json-render-diagnostics", "--manifest-path"]).arg(&manifest);
297 for package in selected.values() {
298 command.args(["--package", package]);
299 }
300 command.args(["--profile", settings.profile.name()]);
301 if settings.timings {
302 command.arg("--timings");
303 }
304 command.args(["--target", &target]);
305 if let Some(directory) = &settings.target_directory {
306 command.arg("--target-dir").arg(directory);
307 }
308 if !settings.default_features {
309 command.arg("--no-default-features");
310 }
311 if !settings.features.is_empty() {
312 command.args(["--features", &settings.features.join(",")]);
313 }
314 command.stdout(Stdio::piped()).stderr(Stdio::inherit());
315 let mut child = Process::spawn(command, cancellation)?;
316 let parsed = read_messages(BufReader::new(child.stdout()), &selected);
317 if parsed.is_err() {
318 child.kill()?;
319 }
320 let status = child.wait()?;
321 cancellation.check()?;
322 let (artifacts, outputs) = parsed?;
323 if !status.success() {
324 return Err(Error::Cargo(status.to_string()));
325 }
326
327 finish_build(settings.backend, settings.profile, target, artifacts, outputs, cancellation, None)
328}
329
330fn finish_build(
331 backend: BuildBackend, profile: BuildProfile, target: String, mut artifacts: Vec<Artifact>, outputs: BTreeSet<(String, PathBuf)>,
332 cancellation: &BuildCancellation, generated: Option<&BTreeMap<PathBuf, String>>,
333) -> Result<Build, Error> {
334 artifacts
335 .retain(|artifact| (artifact.path.extension().is_some_and(|extension| extension == "wasm")) == (backend == BuildBackend::Guest));
336 if artifacts.is_empty() {
337 return Err(Error::Settings("selected packages contain no artifact for this backend".into()));
338 }
339 let mut shipping = BTreeMap::new();
341 for artifact in &mut artifacts {
342 cancellation.check()?;
343 let source = Artifact::read(artifact.producer.clone(), artifact.name.clone(), artifact.path.clone())?;
344 if source.sha256 != artifact.sha256 {
345 return Err(Error::ArtifactChanged(artifact.path.clone()));
346 }
347 let parent = artifact.path.parent().ok_or_else(|| Error::Settings("artifact has no parent directory".into()))?;
348 let file_name = artifact.path.file_name().ok_or_else(|| Error::Settings("artifact has no file name".into()))?;
349 let output = parent.join("harmos").join(&artifact.producer).join(file_name);
350 #[cfg(feature = "guest")]
351 let bytes = if backend == BuildBackend::Guest {
352 let module = std::fs::read(&artifact.path)?;
353 super::component::strip_debug(&module).map_err(Error::Component)?
354 } else {
355 native_shipping_bytes(&artifact.path, cancellation)?
356 };
357 #[cfg(not(feature = "guest"))]
358 let bytes = native_shipping_bytes(&artifact.path, cancellation)?;
359 if bytes.len() > crate::bundle::Limits::default().file_bytes {
360 return Err(Error::Settings("artifact exceeds bundle byte limit".into()));
361 }
362 let permissions = std::fs::metadata(&artifact.path)?.permissions();
363 artifact.sha256 = digest(&bytes);
364 shipping.insert(output.clone(), (bytes, permissions));
365 artifact.path = output;
366 }
367 #[cfg(feature = "guest")]
368 let mut components: BTreeMap<PathBuf, Vec<u8>> = BTreeMap::new();
369 #[cfg(not(feature = "guest"))]
370 let components: BTreeMap<PathBuf, Vec<u8>> = BTreeMap::new();
371 #[cfg(feature = "guest")]
373 for artifact in &mut artifacts {
374 if artifact.path.extension().is_none_or(|extension| extension != "wasm") {
375 continue;
376 }
377 let module = &shipping[&artifact.path].0;
378 if digest(module) != artifact.sha256 {
379 return Err(Error::ArtifactChanged(artifact.path.clone()));
380 }
381 super::Declaration::inspect(module, super::Facility::Guest)?;
382 if module.starts_with(b"\0asm\x0d\0\x01\0") {
383 continue;
384 }
385 let component = super::componentize(module).map_err(Error::Component)?;
386 super::Declaration::inspect(&component, super::Facility::Guest)?;
387 let output = artifact.path.with_extension("component.wasm");
388 artifact.sha256 = digest(&component);
389 components.insert(output.clone(), component);
390 artifact.path = output;
391 }
392
393 let expected_schemas = generated.map(|generated| {
394 generated
395 .iter()
396 .filter(|(path, _)| outputs.iter().any(|(_, directory)| path.starts_with(directory.join("grpc"))))
397 .map(|(path, sha256)| (path.clone(), sha256.clone()))
398 .collect::<BTreeMap<_, _>>()
399 });
400 let mut schemas = BTreeMap::new();
401 for (producer, directory) in outputs {
402 let root = directory.join("grpc");
403 if !root.exists() {
404 continue;
405 }
406 for entry in std::fs::read_dir(root)? {
407 let entry = entry?;
408 if !entry.file_type()?.is_dir() {
409 continue;
410 }
411 let catalog = entry.file_name().into_string().map_err(|_| Error::Settings("schema catalog is not UTF-8".into()))?;
412 let key = (producer.clone(), catalog.clone());
413 if schemas
414 .insert(
415 key.clone(),
416 Schema {
417 producer: producer.clone(),
418 catalog,
419 files: schema_files(&entry.path())?.into_iter().map(|(name, bytes)| (name, digest(&bytes))).collect(),
420 directory: entry.path(),
421 },
422 )
423 .is_some()
424 {
425 return Err(Error::Collision(format!("{}/{}", key.0, key.1)));
426 }
427 }
428 }
429 let schemas: Vec<_> = schemas.into_values().collect();
430 if let Some(expected) = expected_schemas {
431 let actual = schemas
432 .iter()
433 .flat_map(|schema| schema.files.iter().map(|(name, sha256)| (schema.directory.join(name), sha256.clone())))
434 .collect::<BTreeMap<_, _>>();
435 if actual != expected {
436 return Err(Error::Settings("generated schema set differs from the archive producer snapshot".into()));
437 }
438 }
439 cancellation.check()?;
440 for (path, (bytes, permissions)) in shipping {
441 publish(&path, &bytes)?;
442 std::fs::set_permissions(path, permissions)?;
443 }
444 for (path, bytes) in components {
445 publish(&path, &bytes)?;
446 }
447 Ok(Build {
448 backend,
449 profile,
450 target,
451 artifacts,
452 schemas,
453 })
454}
455
456fn native_shipping_bytes(path: &Path, cancellation: &BuildCancellation) -> Result<Vec<u8>, Error> {
459 let mut rustc = Command::new("rustc");
460 rustc.args(["--print", "sysroot"]).stdout(Stdio::piped()).stderr(Stdio::piped());
461 let sysroot = Process::spawn(rustc, cancellation)?.output()?;
462 if !sysroot.status.success() {
463 return Err(Error::Cargo("rustc sysroot discovery failed".into()));
464 }
465 let tools = PathBuf::from(String::from_utf8_lossy(&sysroot.stdout).trim()).join("lib/rustlib");
466 let executable = format!("llvm-objcopy{}", std::env::consts::EXE_SUFFIX);
467 let objcopy = std::fs::read_dir(tools)?
468 .filter_map(Result::ok)
469 .map(|entry| entry.path().join("bin").join(&executable))
470 .find(|path| path.is_file())
471 .ok_or_else(|| Error::Settings("packaging requires the Rust llvm-tools component".into()))?;
472
473 let temporary = tempfile::NamedTempFile::new()?;
474 let mut command = Command::new(&objcopy);
475 command.arg("--strip-debug").arg(path).arg(temporary.path()).stdout(Stdio::piped()).stderr(Stdio::piped());
476 let result = Process::spawn(command, cancellation)?.output()?;
477 if !result.status.success() {
478 return Err(Error::Cargo(format!("debug separation failed: {}", String::from_utf8_lossy(&result.stderr))));
479 }
480 read_artifact(temporary.path())
481}
482
483pub(super) fn validate_backend(settings: &BuildSettings) -> Result<(), Error> {
484 let enabled = match settings.backend {
485 BuildBackend::Sidecar => cfg!(feature = "sidecar"),
486 BuildBackend::Guest => cfg!(feature = "guest"),
487 };
488 if !enabled {
489 return Err(Error::Settings("selected packaging backend is disabled".into()));
490 }
491 Ok(())
492}
493
494pub fn package(build: &Build, output: &Path) -> Result<Vec<PackageOutput>, Error> {
496 package_with_cancellation(build, output, &BuildCancellation::default())
497}
498
499pub fn package_with_cancellation(build: &Build, output: &Path, cancellation: &BuildCancellation) -> Result<Vec<PackageOutput>, Error> {
500 if build.artifacts.is_empty() || build.target.is_empty() {
501 return Err(Error::Settings("a stamped build with artifacts and a target is required".into()));
502 }
503 let artifacts = &build.artifacts;
504 let schemas = &build.schemas;
505 let backend = build.backend;
506 let target = &build.target;
507 use crate::bundle::{Bundle, Executable, Facility, Identity, Limits, Source};
508 let facility = match backend {
509 BuildBackend::Sidecar => Facility::Sidecar,
510 BuildBackend::Guest => Facility::Guest,
511 };
512 let inspected = match backend {
513 BuildBackend::Sidecar => super::Facility::Sidecar,
514 BuildBackend::Guest => super::Facility::Guest,
515 };
516 let mut prepared = BTreeMap::new();
517 for artifact in artifacts {
518 cancellation.check()?;
519 safe_name(&artifact.producer)?;
520 safe_name(&artifact.name)?;
521 let content = read_artifact(&artifact.path)?;
522 if digest(&content) != artifact.sha256 {
523 return Err(Error::ArtifactChanged(artifact.path.clone()));
524 }
525 let declaration = super::Declaration::inspect(&content, inspected)?;
526 let executable = artifact
527 .path
528 .file_name()
529 .and_then(|name| name.to_str())
530 .ok_or_else(|| Error::Settings("artifact filename is not UTF-8".into()))?
531 .to_owned();
532 let mut files = BTreeMap::from([(executable.clone(), content)]);
533 for schema in schemas.iter().filter(|schema| schema.producer == artifact.producer) {
534 safe_name(&schema.catalog)?;
535 let contents = schema_files(&schema.directory)?;
536 let hashes: BTreeMap<_, _> = contents.iter().map(|(name, bytes)| (name.clone(), digest(bytes))).collect();
537 if hashes != schema.files {
538 return Err(Error::ArtifactChanged(schema.directory.clone()));
539 }
540 for (name, bytes) in contents {
541 let name = format!("grpc/{}/{name}", schema.catalog);
542 if files.insert(name.clone(), bytes).is_some() {
543 return Err(Error::Collision(name));
544 }
545 }
546 }
547 let sources: Vec<_> = files
548 .iter()
549 .map(|(path, bytes)| Source {
550 path,
551 bytes,
552 executable: (path == &executable).then(|| Executable {
553 facility,
554 target: target.to_owned(),
555 registrations: Vec::new(),
556 }),
557 })
558 .collect();
559 let identity = Identity {
560 name: declaration.id,
561 version: declaration.version,
562 description: declaration.description,
563 package: Default::default(),
564 };
565 let bundle = Bundle::build(identity, &sources, Limits::default())?;
566 let destination = output.join(&artifact.producer).join(format!("{}.harmos", artifact.name));
567 if prepared.insert(destination.clone(), (artifact.producer.clone(), bundle)).is_some() {
568 return Err(Error::Collision(destination.display().to_string()));
569 }
570 }
571 cancellation.check()?;
572 let mut packages = Vec::new();
574 for (path, (producer, bundle)) in prepared {
575 let sha256 = digest(bundle.bytes());
576 publish(&path, bundle.bytes())?;
577 packages.push(PackageOutput { producer, path, sha256 });
578 }
579 Ok(packages)
580}
581
582fn safe_name(name: &str) -> Result<(), Error> {
583 if name.is_empty()
584 || name.len() > 255
585 || !name.bytes().all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
586 || matches!(name, "." | "..")
587 {
588 return Err(Error::Settings("artifact names must be single path components".into()));
589 }
590 Ok(())
591}
592
593fn read_artifact(path: &Path) -> Result<Vec<u8>, Error> {
594 use std::io::Read as _;
595 let limit = crate::bundle::Limits::default().file_bytes;
596 if path.is_symlink() {
597 return Err(Error::Settings("artifact must be a regular file".into()));
598 }
599 let file = std::fs::File::open(path)?;
600 if !file.metadata()?.is_file() {
601 return Err(Error::Settings("artifact must be a regular file".into()));
602 }
603 let mut bytes = Vec::new();
604 file.take(limit as u64 + 1).read_to_end(&mut bytes)?;
605 if bytes.len() > limit {
606 return Err(Error::Settings("artifact exceeds bundle byte limit".into()));
607 }
608 Ok(bytes)
609}
610
611fn schema_files(directory: &Path) -> Result<BTreeMap<String, Vec<u8>>, Error> {
612 let manifest = read_artifact(&directory.join("manifest.txt"))?;
613 let text = std::str::from_utf8(&manifest).map_err(|_| Error::Settings("schema manifest must be UTF-8".into()))?;
614 let names: Vec<_> = text.lines().collect();
615 if names.first() != Some(&"descriptors.bin")
616 || names.len() > crate::bundle::Limits::default().files
617 || names.iter().copied().collect::<BTreeSet<_>>().len() != names.len()
618 || names.contains(&"manifest.txt")
619 {
620 return Err(Error::Settings("schema manifest is incomplete or duplicated".into()));
621 }
622 let mut files = BTreeMap::new();
623 for name in names {
624 let relative = Path::new(name);
625 if relative.components().any(|part| !matches!(part, std::path::Component::Normal(_))) || name.contains(['\\', ':']) {
626 return Err(Error::Settings("schema manifest contains an unsafe path".into()));
627 }
628 files.insert(name.to_owned(), read_artifact(&directory.join(relative))?);
629 }
630 files.insert("manifest.txt".into(), manifest);
631 Ok(files)
632}
633
634fn publish(path: &std::path::Path, bytes: &[u8]) -> Result<(), Error> {
635 use std::io::Write;
636 let parent = path.parent().ok_or_else(|| Error::Settings("output has no parent directory".into()))?;
637 std::fs::create_dir_all(parent)?;
638 let mut temporary = tempfile::NamedTempFile::new_in(parent)?;
639 temporary.write_all(bytes)?;
640 temporary.persist(path).map_err(|error| Error::Io(error.error))?;
641 Ok(())
642}
643
644fn read_messages(reader: impl BufRead, selected: &BTreeMap<String, String>) -> Result<BuildMessages, Error> {
645 let mut artifacts = Vec::new();
646 let mut outputs = BTreeSet::new();
647 let mut finished = false;
648 for line in reader.lines() {
649 let message: Message = serde_json::from_str(&line?)?;
650 if finished {
651 return Err(Error::Cargo("messages follow the completed Cargo build".into()));
652 }
653 if message.reason == "build-finished" {
654 if message.success != Some(true) {
655 return Err(Error::Cargo("captured Cargo build did not succeed".into()));
656 }
657 finished = true;
658 continue;
659 }
660 if let Some(rendered) = message.message.as_ref().and_then(|diagnostic| diagnostic.rendered.as_ref()) {
661 eprint!("{rendered}");
662 }
663 let Some(producer) = message.package_id.as_ref().and_then(|id| selected.get(id)) else {
664 continue;
665 };
666 if message.reason == "build-script-executed" {
667 if let Some(directory) = message.out_dir {
668 outputs.insert((producer.clone(), directory));
669 }
670 continue;
671 }
672 if message.reason != "compiler-artifact" || message.profile.is_some_and(|profile| profile.test) {
673 continue;
674 }
675 let Some(target) = message.target else {
676 continue;
677 };
678 if target.kind.iter().any(|kind| kind == "custom-build" || kind == "example" || kind == "test" || kind == "bench") {
679 continue;
680 }
681 let files = message
682 .executable
683 .into_iter()
684 .chain(message.filenames.into_iter().filter(|path| path.extension().is_some_and(|extension| extension == "wasm")));
685 for path in files {
686 artifacts.push(Artifact::read(producer.clone(), target.name.clone(), path)?);
687 }
688 }
689 if !finished {
690 return Err(Error::Cargo("captured Cargo stream has no successful build-finished event".into()));
691 }
692 artifacts.sort_by(|left, right| (&left.producer, &left.name, &left.path).cmp(&(&right.producer, &right.name, &right.path)));
693 artifacts.dedup_by(|left, right| left.producer == right.producer && left.name == right.name && left.path == right.path);
694 Ok((artifacts, outputs))
695}
696
697