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