cargo_component/
lib.rs

1//! Cargo support for WebAssembly components.
2
3#![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/// Represents a cargo package paired with its component metadata.
57#[derive(Debug)]
58pub struct PackageComponentMetadata<'a> {
59    /// The cargo package.
60    pub package: &'a Package,
61    /// The associated component metadata.
62    pub metadata: ComponentMetadata,
63}
64
65impl<'a> PackageComponentMetadata<'a> {
66    /// Creates a new package metadata from the given package.
67    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
132/// Runs the cargo command as specified in the configuration.
133///
134/// Note: if the command returns a non-zero status, or if the
135/// `--help` option was given on the command line, this
136/// function will exit the process.
137///
138/// Returns any relevant output components.
139pub 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        // Treat `--help` as the help command
157        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    // Spawn the actual cargo command
176    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        // Treat run and serve as build commands as we need to componentize the output
185        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    // Handle the target for buildable commands
197    if command.buildable() {
198        install_wasm32_wasip1(config)?;
199
200        // Add an implicit wasm32-wasip1 target if there isn't a wasm target present
201        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        // It will output the message as json so we can extract the wasm files
218        // that will be componentized
219        cargo.arg("--message-format").arg("json-render-diagnostics");
220        cargo.stdout(Stdio::piped());
221    } else {
222        cargo.stdout(Stdio::inherit());
223    }
224
225    // At this point, spawn the command for help and terminate
226    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        // Only build for the test target; running will be handled
242        // after the componentization
243        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    // We check here before we actually build that a runtime is present.
273    // We first check the runner for `wasm32-wasip1` in the order from
274    // cargo's convention for a user-supplied runtime (path or executable)
275    // and use the default, namely `wasmtime`, if it is not set.
276    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    // Treat the runner object as an executable with list of arguments it
294    // that was extracted by splitting each whitespace. This allows the user
295    // to provide arguments which are passed to wasmtime without having to
296    // add more command-line argument parsing to this crate.
297    let wasi_runner = runner.path.to_string_lossy().into_owned();
298
299    if !using_default {
300        // check if the override runner exists
301        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 the command line arguments also had `--message-format`, echo the line
349            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    /// The path to the output.
389    path: PathBuf,
390    /// The display name if the output is an executable.
391    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    // Acquire the lock file to ensure any other cargo-component process waits for this to complete
408    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    // The format of the display name is intentionally the same
488    // as what `cargo` formats for running executables.
489    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    /// A WebAssembly module that will not be componentized.
593    Module,
594    /// A WebAssembly module that will be componentized.
595    Componentizable(Vec<u8>),
596    /// A WebAssembly component.
597    Component,
598    /// An artifact that is not a WebAssembly module or component.
599    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
677/// Loads the workspace metadata based on the given manifest path.
678pub 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
695/// Loads the component metadata for the given package specs.
696///
697/// If `workspace` is true, all workspace packages are loaded.
698pub 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    // Update the lock file if it exists or if the new lock file is non-empty
769    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    // If there is no wit files and no dependencies, stop generating the bindings file for it.
824    let (generator, import_name_map) = match BindingsGenerator::new(resolution).await? {
825        Some(v) => v,
826        None => return Ok(HashMap::new()),
827    };
828
829    // TODO: make the output path configurable
830    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    // Only print the message if the artifact was not fresh
936    // Due to the way cargo currently works on macOS, it will overwrite
937    // a previously generated component on an up-to-date build.
938    //
939    // Therefore, we always componentize the artifact on macOS, but we
940    // only print the status message if the artifact was not fresh.
941    //
942    // See: https://github.com/rust-lang/cargo/blob/99ad42deb4b0be0cdb062d333d5e63460a94c33c/crates/cargo-util/src/paths.rs#L542-L550
943    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    // To make the write atomic, first write to a temp file and then rename the file
982    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
1007/// Represents options for a publish operation.
1008pub struct PublishOptions<'a> {
1009    /// The package to publish.
1010    pub package: &'a Package,
1011    /// The registry URL to publish to.
1012    pub registry: Option<&'a Registry>,
1013    /// The name of the package being published.
1014    pub name: &'a PackageRef,
1015    /// The version of the package being published.
1016    pub version: &'a Version,
1017    /// The path to the package being published.
1018    pub path: &'a Path,
1019    /// Whether to perform a dry run or not.
1020    pub dry_run: bool,
1021}
1022
1023/// Read metadata from `Cargo.toml` and add it to the component
1024fn 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        // TODO: get the git commit hash
1059        revision: None,
1060        version: Some(wasm_metadata::Version::new(package.version.to_string())),
1061    };
1062    metadata.to_wasm(wasm)
1063}
1064
1065/// Publish a component for the given workspace and publish options.
1066pub 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
1108/// Update the dependencies in the lock file.
1109///
1110/// This updates only `Cargo-component.lock`.
1111pub 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    // Read the current lock file and generate a new one
1121    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                // The package is no longer a dependency
1148                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                    // The version of the package is no longer a dependency
1172                    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            // The version has changed
1186            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                // The package is new
1210                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                // The version is new
1233                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        // Update the lock file
1252        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}