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_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/// Represents a cargo package paired with its component metadata.
58#[derive(Debug)]
59pub struct PackageComponentMetadata<'a> {
60    /// The cargo package.
61    pub package: &'a Package,
62    /// The associated component metadata.
63    pub metadata: ComponentMetadata,
64}
65
66impl<'a> PackageComponentMetadata<'a> {
67    /// Creates a new package metadata from the given package.
68    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
133/// Runs the cargo command as specified in the configuration.
134///
135/// Note: if the command returns a non-zero status, or if the
136/// `--help` option was given on the command line, this
137/// function will exit the process.
138///
139/// Returns any relevant output components.
140pub 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        // Treat `--help` as the help command
158        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    // Spawn the actual cargo command
177    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        // Treat run and serve as build commands as we need to componentize the output
186        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    // Handle the target for buildable commands
198    if command.buildable() {
199        install_wasm32_wasip1(config)?;
200
201        // Add an implicit wasm32-wasip1 target if there isn't a wasm target present
202        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        // It will output the message as json so we can extract the wasm files
219        // that will be componentized
220        cargo.arg("--message-format").arg("json-render-diagnostics");
221        cargo.stdout(Stdio::piped());
222    } else {
223        cargo.stdout(Stdio::inherit());
224    }
225
226    // At this point, spawn the command for help and terminate
227    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        // Only build for the test target; running will be handled
243        // after the componentization
244        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    // We check here before we actually build that a runtime is present.
274    // We first check the runner for `wasm32-wasip1` in the order from
275    // cargo's convention for a user-supplied runtime (path or executable)
276    // and use the default, namely `wasmtime`, if it is not set.
277    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    // Treat the runner object as an executable with list of arguments it
295    // that was extracted by splitting each whitespace. This allows the user
296    // to provide arguments which are passed to wasmtime without having to
297    // add more command-line argument parsing to this crate.
298    let wasi_runner = runner.path.to_string_lossy().into_owned();
299
300    if !using_default {
301        // check if the override runner exists
302        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 the command line arguments also had `--message-format`, echo the line
350            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    /// The path to the output.
390    path: PathBuf,
391    /// The display name if the output is an executable.
392    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    // Acquire the lock file to ensure any other cargo-component process waits for this to complete
409    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    // The format of the display name is intentionally the same
489    // as what `cargo` formats for running executables.
490    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    /// A WebAssembly module that will not be componentized.
594    Module,
595    /// A WebAssembly module that will be componentized.
596    Componentizable(Vec<u8>),
597    /// A WebAssembly component.
598    Component,
599    /// An artifact that is not a WebAssembly module or component.
600    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
678/// Loads the workspace metadata based on the given manifest path.
679pub 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
696/// Loads the component metadata for the given package specs.
697///
698/// If `workspace` is true, all workspace packages are loaded.
699pub 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    // Update the lock file if it exists or if the new lock file is non-empty
770    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    // If there is no wit files and no dependencies, stop generating the bindings file for it.
825    let (generator, import_name_map) = match BindingsGenerator::new(resolution).await? {
826        Some(v) => v,
827        None => return Ok(HashMap::new()),
828    };
829
830    // TODO: make the output path configurable
831    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    // Only print the message if the artifact was not fresh
937    // Due to the way cargo currently works on macOS, it will overwrite
938    // a previously generated component on an up-to-date build.
939    //
940    // Therefore, we always componentize the artifact on macOS, but we
941    // only print the status message if the artifact was not fresh.
942    //
943    // See: https://github.com/rust-lang/cargo/blob/99ad42deb4b0be0cdb062d333d5e63460a94c33c/crates/cargo-util/src/paths.rs#L542-L550
944    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    // To make the write atomic, first write to a temp file and then rename the file
989    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
1014/// Represents options for a publish operation.
1015pub struct PublishOptions<'a> {
1016    /// The package to publish.
1017    pub package: &'a Package,
1018    /// The registry URL to publish to.
1019    pub registry: Option<&'a Registry>,
1020    /// The name of the package being published.
1021    pub name: &'a PackageRef,
1022    /// The version of the package being published.
1023    pub version: &'a Version,
1024    /// The path to the package being published.
1025    pub path: &'a Path,
1026    /// Whether to perform a dry run or not.
1027    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    // TODO: registry metadata should have keywords
1043    // if !package.keywords.is_empty() {
1044    //     metadata.set_keywords(Some(package.keywords.clone()));
1045    // }
1046
1047    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
1083/// Publish a component for the given workspace and publish options.
1084pub 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
1128/// Update the dependencies in the lock file.
1129///
1130/// This updates only `Cargo-component.lock`.
1131pub 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    // Read the current lock file and generate a new one
1141    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                // The package is no longer a dependency
1168                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                    // The version of the package is no longer a dependency
1192                    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            // The version has changed
1206            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                // The package is new
1230                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                // The version is new
1253                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        // Update the lock file
1272        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}