1#![deny(missing_docs)]
4
5use std::{
6 borrow::Cow,
7 collections::HashMap,
8 env,
9 fmt::{self, Write},
10 fs::{self, File},
11 io::{BufRead, BufReader, Read, Seek, SeekFrom},
12 path::{Path, PathBuf},
13 process::{Command, Stdio},
14 sync::Arc,
15 time::SystemTime,
16};
17
18use anyhow::{bail, Context, Result};
19use bindings::BindingsGenerator;
20use cargo_component_core::{
21 lock::{LockFile, LockFileResolver, LockedPackage, LockedPackageVersion},
22 terminal::Colors,
23};
24use cargo_config2::{PathAndArgs, TargetTripleRef};
25use cargo_metadata::{Artifact, CrateType, Message, Metadata, MetadataCommand, Package};
26use semver::Version;
27use shell_escape::escape;
28use tempfile::NamedTempFile;
29use wasm_metadata::{Link, LinkType, RegistryMetadata};
30use wasm_pkg_client::{
31 caching::{CachingClient, FileCache},
32 PackageRef, PublishOpts, Registry,
33};
34use wasmparser::{Parser, Payload};
35use wit_component::ComponentEncoder;
36
37use crate::target::install_wasm32_wasip1;
38
39use config::{CargoArguments, CargoPackageSpec, Config};
40use lock::{acquire_lock_file_ro, acquire_lock_file_rw};
41use metadata::ComponentMetadata;
42use registry::{PackageDependencyResolution, PackageResolutionMap};
43
44mod bindings;
45pub mod commands;
46pub mod config;
47mod generator;
48mod lock;
49mod metadata;
50mod registry;
51mod target;
52
53fn is_wasm_target(target: &str) -> bool {
54 target == "wasm32-wasi" || target == "wasm32-wasip1" || target == "wasm32-unknown-unknown"
55}
56
57#[derive(Debug)]
59pub struct PackageComponentMetadata<'a> {
60 pub package: &'a Package,
62 pub metadata: ComponentMetadata,
64}
65
66impl<'a> PackageComponentMetadata<'a> {
67 pub fn new(package: &'a Package) -> Result<Self> {
69 Ok(Self {
70 package,
71 metadata: ComponentMetadata::from_package(package)?,
72 })
73 }
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
77enum CargoCommand {
78 #[default]
79 Other,
80 Help,
81 Build,
82 Run,
83 Test,
84 Bench,
85 Serve,
86}
87
88impl CargoCommand {
89 fn buildable(self) -> bool {
90 matches!(
91 self,
92 Self::Build | Self::Run | Self::Test | Self::Bench | Self::Serve
93 )
94 }
95
96 fn runnable(self) -> bool {
97 matches!(self, Self::Run | Self::Test | Self::Bench | Self::Serve)
98 }
99
100 fn testable(self) -> bool {
101 matches!(self, Self::Test | Self::Bench)
102 }
103}
104
105impl fmt::Display for CargoCommand {
106 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
107 match self {
108 Self::Help => write!(f, "help"),
109 Self::Build => write!(f, "build"),
110 Self::Run => write!(f, "run"),
111 Self::Test => write!(f, "test"),
112 Self::Bench => write!(f, "bench"),
113 Self::Serve => write!(f, "serve"),
114 Self::Other => write!(f, "<unknown>"),
115 }
116 }
117}
118
119impl From<&str> for CargoCommand {
120 fn from(s: &str) -> Self {
121 match s {
122 "h" | "help" => Self::Help,
123 "b" | "build" | "rustc" => Self::Build,
124 "r" | "run" => Self::Run,
125 "t" | "test" => Self::Test,
126 "bench" => Self::Bench,
127 "serve" => Self::Serve,
128 _ => Self::Other,
129 }
130 }
131}
132
133pub async fn run_cargo_command(
141 client: Arc<CachingClient<FileCache>>,
142 config: &Config,
143 metadata: &Metadata,
144 packages: &[PackageComponentMetadata<'_>],
145 subcommand: Option<&str>,
146 cargo_args: &CargoArguments,
147 spawn_args: &[String],
148) -> Result<Vec<PathBuf>> {
149 let import_name_map = generate_bindings(client, config, metadata, packages, cargo_args).await?;
150
151 let cargo_path = std::env::var("CARGO")
152 .map(PathBuf::from)
153 .ok()
154 .unwrap_or_else(|| PathBuf::from("cargo"));
155
156 let command = if cargo_args.help {
157 CargoCommand::Help
159 } else {
160 subcommand.map(CargoCommand::from).unwrap_or_default()
161 };
162
163 let (build_args, output_args) = match spawn_args.iter().position(|a| a == "--") {
164 Some(position) => spawn_args.split_at(position),
165 None => (spawn_args, &[] as _),
166 };
167 let needs_runner = !build_args.iter().any(|a| a == "--no-run");
168
169 let mut args = build_args.iter().peekable();
170 if let Some(arg) = args.peek() {
171 if *arg == "component" {
172 args.next().unwrap();
173 }
174 }
175
176 log::debug!(
178 "spawning cargo `{path}` with arguments `{args:?}`",
179 path = cargo_path.display(),
180 args = args.clone().collect::<Vec<_>>(),
181 );
182
183 let mut cargo = Command::new(&cargo_path);
184 if matches!(command, CargoCommand::Run | CargoCommand::Serve) {
185 cargo.arg("build");
187 if let Some(arg) = args.peek() {
188 if Some((*arg).as_str()) == subcommand {
189 args.next().unwrap();
190 }
191 }
192 }
193 cargo.args(args);
194
195 let cargo_config = cargo_config2::Config::load()?;
196
197 if command.buildable() {
199 install_wasm32_wasip1(config)?;
200
201 if !cargo_args.targets.iter().any(|t| is_wasm_target(t))
203 && !cargo_config
204 .build
205 .target
206 .as_ref()
207 .is_some_and(|v| v.iter().any(|t| is_wasm_target(t.triple())))
208 {
209 cargo.arg("--target").arg("wasm32-wasip1");
210 }
211
212 if let Some(format) = &cargo_args.message_format {
213 if format != "json-render-diagnostics" {
214 bail!("unsupported cargo message format `{format}`");
215 }
216 }
217
218 cargo.arg("--message-format").arg("json-render-diagnostics");
221 cargo.stdout(Stdio::piped());
222 } else {
223 cargo.stdout(Stdio::inherit());
224 }
225
226 if command == CargoCommand::Help {
228 let mut child = cargo.spawn().context(format!(
229 "failed to spawn `{path}`",
230 path = cargo_path.display()
231 ))?;
232
233 let status = child.wait().context(format!(
234 "failed to wait for `{path}` to finish",
235 path = cargo_path.display()
236 ))?;
237
238 std::process::exit(status.code().unwrap_or(0));
239 }
240
241 if needs_runner && command.testable() {
242 cargo.arg("--no-run");
245 }
246
247 let runner = if needs_runner && command.runnable() {
248 Some(get_runner(&cargo_config, command == CargoCommand::Serve)?)
249 } else {
250 None
251 };
252
253 let artifacts = spawn_cargo(cargo, &cargo_path, cargo_args, command.buildable())?;
254
255 let outputs = componentize_artifacts(
256 config,
257 metadata,
258 &artifacts,
259 packages,
260 &import_name_map,
261 command,
262 output_args,
263 )?;
264
265 if let Some(runner) = runner {
266 spawn_outputs(config, &runner, output_args, &outputs, command)?;
267 }
268
269 Ok(outputs.into_iter().map(|o| o.path).collect())
270}
271
272fn get_runner(cargo_config: &cargo_config2::Config, serve: bool) -> Result<PathAndArgs> {
273 let (runner, using_default) = cargo_config
278 .runner(TargetTripleRef::from("wasm32-wasip1"))
279 .unwrap_or_default()
280 .map(|runner_override| (runner_override, false))
281 .unwrap_or_else(|| {
282 (
283 PathAndArgs::new("wasmtime")
284 .args(if serve {
285 vec!["serve", "-S", "cli", "-S", "http"]
286 } else {
287 vec!["-S", "preview2", "-S", "cli", "-S", "http"]
288 })
289 .to_owned(),
290 true,
291 )
292 });
293
294 let wasi_runner = runner.path.to_string_lossy().into_owned();
299
300 if !using_default {
301 if !(runner.path.exists() || which::which(&runner.path).is_ok()) {
303 bail!(
304 "failed to find `{wasi_runner}` specified by either the `CARGO_TARGET_WASM32_WASIP1_RUNNER`\
305 environment variable or as the `wasm32-wasip1` runner in `.cargo/config.toml`"
306 );
307 }
308 } else if which::which(&runner.path).is_err() {
309 bail!(
310 "failed to find `{wasi_runner}` on PATH\n\n\
311 ensure Wasmtime is installed before running this command\n\n\
312 {msg}:\n\n {instructions}",
313 msg = if cfg!(unix) {
314 "Wasmtime can be installed via a shell script"
315 } else {
316 "Wasmtime can be installed via the GitHub releases page"
317 },
318 instructions = if cfg!(unix) {
319 "curl https://wasmtime.dev/install.sh -sSf | bash"
320 } else {
321 "https://github.com/bytecodealliance/wasmtime/releases"
322 },
323 );
324 }
325
326 Ok(runner)
327}
328
329fn spawn_cargo(
330 mut cmd: Command,
331 cargo: &Path,
332 cargo_args: &CargoArguments,
333 process_messages: bool,
334) -> Result<Vec<Artifact>> {
335 log::debug!("spawning command {:?}", cmd);
336
337 let mut child = cmd.spawn().context(format!(
338 "failed to spawn `{cargo}`",
339 cargo = cargo.display()
340 ))?;
341
342 let mut artifacts = Vec::new();
343 if process_messages {
344 let stdout = child.stdout.take().expect("no stdout");
345 let reader = BufReader::new(stdout);
346 for line in reader.lines() {
347 let line = line.context("failed to read output from `cargo`")?;
348
349 if cargo_args.message_format.is_some() {
351 println!("{line}");
352 }
353
354 if line.is_empty() {
355 continue;
356 }
357
358 for message in Message::parse_stream(line.as_bytes()) {
359 if let Message::CompilerArtifact(artifact) =
360 message.context("unexpected JSON message from cargo")?
361 {
362 for path in &artifact.filenames {
363 match path.extension() {
364 Some("wasm") => {
365 artifacts.push(artifact);
366 break;
367 }
368 _ => continue,
369 }
370 }
371 }
372 }
373 }
374 }
375
376 let status = child.wait().context(format!(
377 "failed to wait for `{cargo}` to finish",
378 cargo = cargo.display()
379 ))?;
380
381 if !status.success() {
382 std::process::exit(status.code().unwrap_or(1));
383 }
384
385 Ok(artifacts)
386}
387
388struct Output {
389 path: PathBuf,
391 display: Option<String>,
393}
394
395fn componentize_artifacts(
396 config: &Config,
397 cargo_metadata: &Metadata,
398 artifacts: &[Artifact],
399 packages: &[PackageComponentMetadata<'_>],
400 import_name_map: &HashMap<String, HashMap<String, String>>,
401 command: CargoCommand,
402 output_args: &[String],
403) -> Result<Vec<Output>> {
404 let mut outputs = Vec::new();
405 let cwd =
406 env::current_dir().with_context(|| "couldn't get the current directory of the process")?;
407
408 let _file_lock = acquire_lock_file_ro(config.terminal(), cargo_metadata)?;
410
411 for artifact in artifacts {
412 for path in artifact
413 .filenames
414 .iter()
415 .filter(|p| p.extension() == Some("wasm") && p.exists())
416 {
417 let (package, metadata) = match packages
418 .iter()
419 .find(|p| p.package.id == artifact.package_id)
420 {
421 Some(PackageComponentMetadata { package, metadata }) => (package, metadata),
422 _ => continue,
423 };
424
425 match read_artifact(path.as_std_path(), metadata.section_present)? {
426 ArtifactKind::Module => {
427 log::debug!(
428 "output file `{path}` is a WebAssembly module that will not be componentized"
429 );
430 continue;
431 }
432 ArtifactKind::Componentizable(bytes) => {
433 componentize(
434 config,
435 (cargo_metadata, metadata),
436 import_name_map
437 .get(&package.name)
438 .expect("package already processed"),
439 artifact,
440 path.as_std_path(),
441 &cwd,
442 &bytes,
443 )?;
444 }
445 ArtifactKind::Component => {
446 log::debug!("output file `{path}` is already a WebAssembly component");
447 }
448 ArtifactKind::Other => {
449 log::debug!("output file `{path}` is not a WebAssembly module or component");
450 continue;
451 }
452 }
453
454 let mut output = Output {
455 path: path.as_std_path().into(),
456 display: None,
457 };
458
459 if command.testable() && artifact.profile.test
460 || (matches!(command, CargoCommand::Run | CargoCommand::Serve)
461 && !artifact.profile.test)
462 {
463 output.display = Some(output_display_name(
464 cargo_metadata,
465 artifact,
466 path.as_std_path(),
467 &cwd,
468 command,
469 output_args,
470 ));
471 }
472
473 outputs.push(output);
474 }
475 }
476
477 Ok(outputs)
478}
479
480fn output_display_name(
481 metadata: &Metadata,
482 artifact: &Artifact,
483 path: &Path,
484 cwd: &Path,
485 command: CargoCommand,
486 output_args: &[String],
487) -> String {
488 let test_path = &artifact.target.src_path;
491 let short_test_path = test_path
492 .strip_prefix(&metadata.workspace_root)
493 .unwrap_or(test_path);
494
495 if artifact.target.is_test() || artifact.target.is_bench() {
496 format!(
497 "{short_test_path} ({path})",
498 path = path.strip_prefix(cwd).unwrap_or(path).display()
499 )
500 } else if command == CargoCommand::Test {
501 format!(
502 "unittests {short_test_path} ({path})",
503 path = path.strip_prefix(cwd).unwrap_or(path).display()
504 )
505 } else if command == CargoCommand::Bench {
506 format!(
507 "benches {short_test_path} ({path})",
508 path = path.strip_prefix(cwd).unwrap_or(path).display()
509 )
510 } else {
511 let mut s = String::new();
512 write!(&mut s, "`").unwrap();
513
514 write!(
515 &mut s,
516 "{}",
517 path.strip_prefix(cwd).unwrap_or(path).display()
518 )
519 .unwrap();
520
521 for arg in output_args.iter().skip(1) {
522 write!(&mut s, " {}", escape(arg.into())).unwrap();
523 }
524
525 write!(&mut s, "`").unwrap();
526 s
527 }
528}
529
530fn spawn_outputs(
531 config: &Config,
532 runner: &PathAndArgs,
533 output_args: &[String],
534 outputs: &[Output],
535 command: CargoCommand,
536) -> Result<()> {
537 let executables = outputs
538 .iter()
539 .filter_map(|output| {
540 output
541 .display
542 .as_ref()
543 .map(|display| (display, &output.path))
544 })
545 .collect::<Vec<_>>();
546
547 if matches!(command, CargoCommand::Run | CargoCommand::Serve) && executables.len() > 1 {
548 config.terminal().error(format!(
549 "`cargo component {command}` can run at most one component, but multiple were specified",
550 ))
551 } else if executables.is_empty() {
552 config.terminal().error(format!(
553 "a component {ty} target must be available for `cargo component {command}`",
554 ty = if matches!(command, CargoCommand::Run | CargoCommand::Serve) {
555 "bin"
556 } else {
557 "test"
558 }
559 ))
560 } else {
561 for (display, executable) in executables {
562 config.terminal().status("Running", display)?;
563
564 let mut cmd = Command::new(&runner.path);
565 cmd.args(&runner.args)
566 .arg("--")
567 .arg(executable)
568 .args(output_args.iter().skip(1))
569 .stdout(Stdio::inherit())
570 .stderr(Stdio::inherit());
571 log::debug!("spawning command {:?}", cmd);
572
573 let mut child = cmd.spawn().context(format!(
574 "failed to spawn `{runner}`",
575 runner = runner.path.display()
576 ))?;
577
578 let status = child.wait().context(format!(
579 "failed to wait for `{runner}` to finish",
580 runner = runner.path.display()
581 ))?;
582
583 if !status.success() {
584 std::process::exit(status.code().unwrap_or(1));
585 }
586 }
587
588 Ok(())
589 }
590}
591
592enum ArtifactKind {
593 Module,
595 Componentizable(Vec<u8>),
597 Component,
599 Other,
601}
602
603fn read_artifact(path: &Path, mut componentizable: bool) -> Result<ArtifactKind> {
604 let mut file = File::open(path).with_context(|| {
605 format!(
606 "failed to open build output `{path}`",
607 path = path.display()
608 )
609 })?;
610
611 let mut header = [0; 8];
612 if file.read_exact(&mut header).is_err() {
613 return Ok(ArtifactKind::Other);
614 }
615
616 if Parser::is_core_wasm(&header) {
617 file.seek(SeekFrom::Start(0)).with_context(|| {
618 format!(
619 "failed to seek to the start of `{path}`",
620 path = path.display()
621 )
622 })?;
623
624 let mut bytes = Vec::new();
625 file.read_to_end(&mut bytes).with_context(|| {
626 format!(
627 "failed to read output WebAssembly module `{path}`",
628 path = path.display()
629 )
630 })?;
631
632 if !componentizable {
633 let parser = Parser::new(0);
634 for payload in parser.parse_all(&bytes) {
635 if let Payload::CustomSection(reader) = payload.with_context(|| {
636 format!(
637 "failed to parse output WebAssembly module `{path}`",
638 path = path.display()
639 )
640 })? {
641 if reader.name().starts_with("component-type") {
642 componentizable = true;
643 break;
644 }
645 }
646 }
647 }
648
649 if componentizable {
650 Ok(ArtifactKind::Componentizable(bytes))
651 } else {
652 Ok(ArtifactKind::Module)
653 }
654 } else if Parser::is_component(&header) {
655 Ok(ArtifactKind::Component)
656 } else {
657 Ok(ArtifactKind::Other)
658 }
659}
660
661fn last_modified_time(path: &Path) -> Result<SystemTime> {
662 path.metadata()
663 .with_context(|| {
664 format!(
665 "failed to read file metadata for `{path}`",
666 path = path.display()
667 )
668 })?
669 .modified()
670 .with_context(|| {
671 format!(
672 "failed to retrieve last modified time for `{path}`",
673 path = path.display()
674 )
675 })
676}
677
678pub fn load_metadata(manifest_path: Option<&Path>) -> Result<Metadata> {
680 let mut command = MetadataCommand::new();
681 command.no_deps();
682
683 if let Some(path) = manifest_path {
684 log::debug!(
685 "loading metadata from manifest `{path}`",
686 path = path.display()
687 );
688 command.manifest_path(path);
689 } else {
690 log::debug!("loading metadata from current directory");
691 }
692
693 command.exec().context("failed to load cargo metadata")
694}
695
696pub fn load_component_metadata<'a>(
700 metadata: &'a Metadata,
701 specs: impl ExactSizeIterator<Item = &'a CargoPackageSpec>,
702 workspace: bool,
703) -> Result<Vec<PackageComponentMetadata<'a>>> {
704 let pkgs = if workspace {
705 metadata.workspace_packages()
706 } else if specs.len() > 0 {
707 let mut pkgs = Vec::with_capacity(specs.len());
708 for spec in specs {
709 let pkg = metadata
710 .packages
711 .iter()
712 .find(|p| {
713 p.name == spec.name
714 && match spec.version.as_ref() {
715 Some(v) => &p.version == v,
716 None => true,
717 }
718 })
719 .with_context(|| {
720 format!("package ID specification `{spec}` did not match any packages")
721 })?;
722 pkgs.push(pkg);
723 }
724
725 pkgs
726 } else {
727 metadata.workspace_default_packages()
728 };
729
730 pkgs.into_iter()
731 .map(PackageComponentMetadata::new)
732 .collect::<Result<_>>()
733}
734
735async fn generate_bindings(
736 client: Arc<CachingClient<FileCache>>,
737 config: &Config,
738 metadata: &Metadata,
739 packages: &[PackageComponentMetadata<'_>],
740 cargo_args: &CargoArguments,
741) -> Result<HashMap<String, HashMap<String, String>>> {
742 let file_lock = acquire_lock_file_ro(config.terminal(), metadata)?;
743 let lock_file = file_lock
744 .as_ref()
745 .map(|f| {
746 LockFile::read(f.file()).with_context(|| {
747 format!(
748 "failed to read lock file `{path}`",
749 path = f.path().display()
750 )
751 })
752 })
753 .transpose()?;
754
755 let cwd =
756 env::current_dir().with_context(|| "couldn't get the current directory of the process")?;
757
758 let resolver = lock_file.as_ref().map(LockFileResolver::new);
759 let resolution_map = create_resolution_map(client, packages, resolver).await?;
760 let mut import_name_map = HashMap::new();
761 for PackageComponentMetadata { package, .. } in packages {
762 let resolution = resolution_map.get(&package.id).expect("missing resolution");
763 import_name_map.insert(
764 package.name.clone(),
765 generate_package_bindings(config, resolution, &cwd).await?,
766 );
767 }
768
769 let new_lock_file = resolution_map.to_lock_file();
771 if (lock_file.is_some() || !new_lock_file.packages.is_empty())
772 && Some(&new_lock_file) != lock_file.as_ref()
773 {
774 drop(file_lock);
775 let file_lock = acquire_lock_file_rw(
776 config.terminal(),
777 metadata,
778 cargo_args.lock_update_allowed(),
779 cargo_args.locked,
780 )?;
781 new_lock_file
782 .write(file_lock.file(), "cargo-component")
783 .with_context(|| {
784 format!(
785 "failed to write lock file `{path}`",
786 path = file_lock.path().display()
787 )
788 })?;
789 }
790
791 Ok(import_name_map)
792}
793
794async fn create_resolution_map<'a>(
795 client: Arc<CachingClient<FileCache>>,
796 packages: &'a [PackageComponentMetadata<'_>],
797 lock_file: Option<LockFileResolver<'_>>,
798) -> Result<PackageResolutionMap<'a>> {
799 let mut map = PackageResolutionMap::default();
800
801 for PackageComponentMetadata { package, metadata } in packages {
802 let resolution =
803 PackageDependencyResolution::new(client.clone(), metadata, lock_file).await?;
804
805 map.insert(package.id.clone(), resolution);
806 }
807
808 Ok(map)
809}
810
811async fn generate_package_bindings(
812 config: &Config,
813 resolution: &PackageDependencyResolution<'_>,
814 cwd: &Path,
815) -> Result<HashMap<String, String>> {
816 if !resolution.metadata.section_present && resolution.metadata.target_path().is_none() {
817 log::debug!(
818 "skipping generating bindings for package `{name}`",
819 name = resolution.metadata.name
820 );
821 return Ok(HashMap::new());
822 }
823
824 let (generator, import_name_map) = match BindingsGenerator::new(resolution).await? {
826 Some(v) => v,
827 None => return Ok(HashMap::new()),
828 };
829
830 let output_dir = resolution
832 .metadata
833 .manifest_path
834 .parent()
835 .unwrap()
836 .join("src");
837 let bindings_path = output_dir.join("bindings.rs");
838
839 config.terminal().status(
840 "Generating",
841 format!(
842 "bindings for {name} ({path})",
843 name = resolution.metadata.name,
844 path = bindings_path
845 .strip_prefix(cwd)
846 .unwrap_or(&bindings_path)
847 .display()
848 ),
849 )?;
850
851 let bindings = generator.generate()?;
852 fs::create_dir_all(&output_dir).with_context(|| {
853 format!(
854 "failed to create output directory `{path}`",
855 path = output_dir.display()
856 )
857 })?;
858 if fs::read_to_string(&bindings_path).unwrap_or_default() != bindings {
859 fs::write(&bindings_path, bindings).with_context(|| {
860 format!(
861 "failed to write bindings file `{path}`",
862 path = bindings_path.display()
863 )
864 })?;
865 }
866
867 Ok(import_name_map)
868}
869
870fn adapter_bytes(
871 config: &Config,
872 metadata: &ComponentMetadata,
873 is_command: bool,
874) -> Result<Cow<'static, [u8]>> {
875 if let Some(adapter) = &metadata.section.adapter {
876 if metadata.section.proxy {
877 config.terminal().warn(
878 "ignoring `proxy` setting due to `adapter` setting being present in `Cargo.toml`",
879 )?;
880 }
881
882 return Ok(fs::read(adapter)
883 .with_context(|| {
884 format!(
885 "failed to read module adapter `{path}`",
886 path = adapter.display()
887 )
888 })?
889 .into());
890 }
891
892 if is_command {
893 if metadata.section.proxy {
894 config
895 .terminal()
896 .warn("ignoring `proxy` setting in `Cargo.toml` for command component")?;
897 }
898
899 Ok(Cow::Borrowed(
900 wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_COMMAND_ADAPTER,
901 ))
902 } else if metadata.section.proxy {
903 Ok(Cow::Borrowed(
904 wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_PROXY_ADAPTER,
905 ))
906 } else {
907 Ok(Cow::Borrowed(
908 wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER,
909 ))
910 }
911}
912
913fn componentize(
914 config: &Config,
915 (cargo_metadata, metadata): (&Metadata, &ComponentMetadata),
916 import_name_map: &HashMap<String, String>,
917 artifact: &Artifact,
918 path: &Path,
919 cwd: &Path,
920 bytes: &[u8],
921) -> Result<()> {
922 let is_command = artifact.profile.test
923 || artifact
924 .target
925 .crate_types
926 .iter()
927 .any(|t| *t == CrateType::Bin);
928
929 log::debug!(
930 "componentizing WebAssembly module `{path}` as a {kind} component (fresh = {fresh})",
931 path = path.display(),
932 kind = if is_command { "command" } else { "reactor" },
933 fresh = artifact.fresh,
934 );
935
936 if !artifact.fresh {
945 config.terminal().status(
946 "Creating",
947 format!(
948 "component {path}",
949 path = path.strip_prefix(cwd).unwrap_or(path).display()
950 ),
951 )?;
952 }
953
954 let mut encoder = ComponentEncoder::default()
955 .module(bytes)?
956 .import_name_map(import_name_map.clone())
957 .adapter(
958 "wasi_snapshot_preview1",
959 &adapter_bytes(config, metadata, is_command)?,
960 )
961 .with_context(|| {
962 format!(
963 "failed to load adapter module `{path}`",
964 path = metadata
965 .section
966 .adapter
967 .as_deref()
968 .unwrap_or_else(|| Path::new("<built-in>"))
969 .display()
970 )
971 })?
972 .validate(true);
973
974 let mut producers = wasm_metadata::Producers::empty();
975 producers.add(
976 "processed-by",
977 env!("CARGO_PKG_NAME"),
978 option_env!("CARGO_VERSION_INFO").unwrap_or(env!("CARGO_PKG_VERSION")),
979 );
980
981 let component = producers.add_to_wasm(&encoder.encode()?).with_context(|| {
982 format!(
983 "failed to add metadata to output component `{path}`",
984 path = path.display()
985 )
986 })?;
987
988 let temp_dir = cargo_metadata.target_directory.join("tmp");
990 fs::create_dir_all(&temp_dir)
991 .with_context(|| format!("failed to create directory `{temp_dir}`"))?;
992
993 let mut file = NamedTempFile::new_in(&temp_dir)
994 .with_context(|| format!("failed to create temp file in `{temp_dir}`"))?;
995
996 use std::io::Write;
997 file.write_all(&component).with_context(|| {
998 format!(
999 "failed to write output component `{path}`",
1000 path = file.path().display()
1001 )
1002 })?;
1003
1004 file.into_temp_path().persist(path).with_context(|| {
1005 format!(
1006 "failed to persist output component `{path}`",
1007 path = path.display()
1008 )
1009 })?;
1010
1011 Ok(())
1012}
1013
1014pub struct PublishOptions<'a> {
1016 pub package: &'a Package,
1018 pub registry: Option<&'a Registry>,
1020 pub name: &'a PackageRef,
1022 pub version: &'a Version,
1024 pub path: &'a Path,
1026 pub dry_run: bool,
1028}
1029
1030fn add_registry_metadata(package: &Package, bytes: &[u8], path: &Path) -> Result<Vec<u8>> {
1031 let mut metadata = RegistryMetadata::default();
1032 if !package.authors.is_empty() {
1033 metadata.set_authors(Some(package.authors.clone()));
1034 }
1035
1036 if !package.categories.is_empty() {
1037 metadata.set_categories(Some(package.categories.clone()));
1038 }
1039
1040 metadata.set_description(package.description.clone());
1041
1042 metadata.set_license(package.license.clone());
1048
1049 let mut links = Vec::new();
1050 if let Some(docs) = &package.documentation {
1051 links.push(Link {
1052 ty: LinkType::Documentation,
1053 value: docs.clone(),
1054 });
1055 }
1056
1057 if let Some(homepage) = &package.homepage {
1058 links.push(Link {
1059 ty: LinkType::Homepage,
1060 value: homepage.clone(),
1061 });
1062 }
1063
1064 if let Some(repo) = &package.repository {
1065 links.push(Link {
1066 ty: LinkType::Repository,
1067 value: repo.clone(),
1068 });
1069 }
1070
1071 if !links.is_empty() {
1072 metadata.set_links(Some(links));
1073 }
1074
1075 metadata.add_to_wasm(bytes).with_context(|| {
1076 format!(
1077 "failed to add registry metadata to component `{path}`",
1078 path = path.display()
1079 )
1080 })
1081}
1082
1083pub async fn publish(
1085 config: &Config,
1086 client: Arc<CachingClient<FileCache>>,
1087 options: &PublishOptions<'_>,
1088) -> Result<()> {
1089 if options.dry_run {
1090 config
1091 .terminal()
1092 .warn("not publishing component to the registry due to the --dry-run option")?;
1093 return Ok(());
1094 }
1095
1096 let bytes = fs::read(options.path).with_context(|| {
1097 format!(
1098 "failed to read component `{path}`",
1099 path = options.path.display()
1100 )
1101 })?;
1102
1103 let bytes = add_registry_metadata(options.package, &bytes, options.path)?;
1104
1105 config.terminal().status(
1106 "Publishing",
1107 format!("component {path}", path = options.path.display()),
1108 )?;
1109
1110 let (name, version) = client
1111 .client()?
1112 .publish_release_data(
1113 Box::pin(std::io::Cursor::new(bytes)),
1114 PublishOpts {
1115 package: Some((options.name.to_owned(), options.version.to_owned())),
1116 registry: options.registry.cloned(),
1117 },
1118 )
1119 .await?;
1120
1121 config
1122 .terminal()
1123 .status("Published", format!("package `{name}` v{version}"))?;
1124
1125 Ok(())
1126}
1127
1128pub async fn update_lockfile(
1132 client: Arc<CachingClient<FileCache>>,
1133 config: &Config,
1134 metadata: &Metadata,
1135 packages: &[PackageComponentMetadata<'_>],
1136 lock_update_allowed: bool,
1137 locked: bool,
1138 dry_run: bool,
1139) -> Result<()> {
1140 let map = create_resolution_map(client, packages, None).await?;
1142
1143 let file_lock = acquire_lock_file_ro(config.terminal(), metadata)?;
1144 let orig_lock_file = file_lock
1145 .as_ref()
1146 .map(|f| {
1147 LockFile::read(f.file()).with_context(|| {
1148 format!(
1149 "failed to read lock file `{path}`",
1150 path = f.path().display()
1151 )
1152 })
1153 })
1154 .transpose()?
1155 .unwrap_or_default();
1156
1157 let new_lock_file = map.to_lock_file();
1158
1159 for old_pkg in &orig_lock_file.packages {
1160 let new_pkg = match new_lock_file
1161 .packages
1162 .binary_search_by_key(&old_pkg.key(), LockedPackage::key)
1163 .map(|index| &new_lock_file.packages[index])
1164 {
1165 Ok(pkg) => pkg,
1166 Err(_) => {
1167 for old_ver in &old_pkg.versions {
1169 config.terminal().status_with_color(
1170 if dry_run { "Would remove" } else { "Removing" },
1171 format!(
1172 "dependency `{name}` v{version}",
1173 name = old_pkg.name,
1174 version = old_ver.version,
1175 ),
1176 Colors::Red,
1177 )?;
1178 }
1179 continue;
1180 }
1181 };
1182
1183 for old_ver in &old_pkg.versions {
1184 let new_ver = match new_pkg
1185 .versions
1186 .binary_search_by_key(&old_ver.key(), LockedPackageVersion::key)
1187 .map(|index| &new_pkg.versions[index])
1188 {
1189 Ok(ver) => ver,
1190 Err(_) => {
1191 config.terminal().status_with_color(
1193 if dry_run { "Would remove" } else { "Removing" },
1194 format!(
1195 "dependency `{name}` v{version}",
1196 name = old_pkg.name,
1197 version = old_ver.version,
1198 ),
1199 Colors::Red,
1200 )?;
1201 continue;
1202 }
1203 };
1204
1205 if old_ver.version != new_ver.version {
1207 config.terminal().status_with_color(
1208 if dry_run { "Would update" } else { "Updating" },
1209 format!(
1210 "dependency `{name}` v{old} -> v{new}",
1211 name = old_pkg.name,
1212 old = old_ver.version,
1213 new = new_ver.version
1214 ),
1215 Colors::Cyan,
1216 )?;
1217 }
1218 }
1219 }
1220
1221 for new_pkg in &new_lock_file.packages {
1222 let old_pkg = match orig_lock_file
1223 .packages
1224 .binary_search_by_key(&new_pkg.key(), LockedPackage::key)
1225 .map(|index| &orig_lock_file.packages[index])
1226 {
1227 Ok(pkg) => pkg,
1228 Err(_) => {
1229 for new_ver in &new_pkg.versions {
1231 config.terminal().status_with_color(
1232 if dry_run { "Would add" } else { "Adding" },
1233 format!(
1234 "dependency `{name}` v{version}",
1235 name = new_pkg.name,
1236 version = new_ver.version,
1237 ),
1238 Colors::Green,
1239 )?;
1240 }
1241 continue;
1242 }
1243 };
1244
1245 for new_ver in &new_pkg.versions {
1246 if old_pkg
1247 .versions
1248 .binary_search_by_key(&new_ver.key(), LockedPackageVersion::key)
1249 .map(|index| &old_pkg.versions[index])
1250 .is_err()
1251 {
1252 config.terminal().status_with_color(
1254 if dry_run { "Would add" } else { "Adding" },
1255 format!(
1256 "dependency `{name}` v{version}",
1257 name = new_pkg.name,
1258 version = new_ver.version,
1259 ),
1260 Colors::Green,
1261 )?;
1262 }
1263 }
1264 }
1265
1266 if dry_run {
1267 config
1268 .terminal()
1269 .warn("not updating component lock file due to --dry-run option")?;
1270 } else {
1271 if new_lock_file != orig_lock_file {
1273 drop(file_lock);
1274 let file_lock =
1275 acquire_lock_file_rw(config.terminal(), metadata, lock_update_allowed, locked)?;
1276 new_lock_file
1277 .write(file_lock.file(), "cargo-component")
1278 .with_context(|| {
1279 format!(
1280 "failed to write lock file `{path}`",
1281 path = file_lock.path().display()
1282 )
1283 })?;
1284 }
1285 }
1286
1287 Ok(())
1288}