cargo_component/commands/
add.rs

1use std::{
2    fs,
3    path::{Path, PathBuf},
4    sync::Arc,
5};
6
7use anyhow::{bail, Context, Result};
8use cargo_component_core::{
9    command::CommonOptions,
10    registry::{Dependency, DependencyResolution, DependencyResolver, RegistryPackage},
11    VersionedPackageName,
12};
13use cargo_metadata::Package;
14use clap::Args;
15use semver::VersionReq;
16use toml_edit::{value, DocumentMut, InlineTable, Item, Table, Value};
17use wasm_pkg_client::{
18    caching::{CachingClient, FileCache},
19    PackageRef,
20};
21
22use crate::{
23    config::CargoPackageSpec,
24    load_component_metadata, load_metadata,
25    metadata::{ComponentMetadata, Target},
26    Config, PackageComponentMetadata,
27};
28
29/// Add a dependency for a WebAssembly component
30#[derive(Args)]
31#[clap(disable_version_flag = true)]
32pub struct AddCommand {
33    /// The common command options.
34    #[clap(flatten)]
35    pub common: CommonOptions,
36
37    /// Path to the manifest to add a dependency to
38    #[clap(long = "manifest-path", value_name = "PATH")]
39    pub manifest_path: Option<PathBuf>,
40
41    /// Don't actually write the manifest
42    #[clap(long = "dry-run")]
43    pub dry_run: bool,
44
45    /// Cargo package to add the dependency to (see `cargo help pkgid`)
46    #[clap(long = "package", short = 'p', value_name = "SPEC")]
47    pub spec: Option<CargoPackageSpec>,
48
49    /// The name of the registry to use.
50    #[clap(long = "registry", short = 'r', value_name = "REGISTRY")]
51    pub registry: Option<String>,
52
53    /// The name of the dependency to use; defaults to the package name.
54    #[clap(long, value_name = "NAME")]
55    pub name: Option<PackageRef>,
56
57    /// The name of the package to add a dependency to.
58    #[clap(value_name = "PACKAGE")]
59    pub package: VersionedPackageName,
60
61    /// Add the dependency to the list of target dependencies
62    #[clap(long = "target")]
63    pub target: bool,
64
65    /// Add a package dependency to a file or directory.
66    #[clap(long = "path", value_name = "PATH")]
67    pub path: Option<PathBuf>,
68}
69
70impl AddCommand {
71    /// Executes the command
72    pub async fn exec(self) -> Result<()> {
73        let config = Config::new(self.common.new_terminal(), self.common.config.clone()).await?;
74        let metadata = load_metadata(self.manifest_path.as_deref())?;
75
76        let client = config.client(self.common.cache_dir.clone(), false).await?;
77
78        let spec = match &self.spec {
79            Some(spec) => Some(spec.clone()),
80            None => CargoPackageSpec::find_current_package_spec(&metadata),
81        };
82
83        let PackageComponentMetadata { package, metadata }: PackageComponentMetadata<'_> =
84            match &spec {
85                Some(spec) => {
86                    let pkgs = load_component_metadata(&metadata, std::iter::once(spec), false)?;
87                    assert!(pkgs.len() == 1, "one package should be present");
88                    pkgs.into_iter().next().unwrap()
89                }
90                None => PackageComponentMetadata::new(
91                    metadata
92                        .root_package()
93                        .context("no root package found in metadata")?,
94                )?,
95            };
96
97        let name = match &self.name {
98            Some(name) => name,
99            None => &self.package.name,
100        };
101
102        self.validate(&metadata, name)?;
103
104        if let Some(path) = self.path.as_ref() {
105            self.add_from_path(package, path)?;
106
107            config.terminal().status(
108                "Added",
109                format!(
110                    "dependency `{name}` from path `{path}`",
111                    path = path.to_str().unwrap()
112                ),
113            )?;
114        } else {
115            let version = self.resolve_version(client, name).await?;
116            let version = version.trim_start_matches('^');
117            self.add(package, version)?;
118
119            config.terminal().status(
120                "Added",
121                format!("dependency `{name}` with version `{version}`"),
122            )?;
123        }
124
125        Ok(())
126    }
127
128    async fn resolve_version(
129        &self,
130        client: Arc<CachingClient<FileCache>>,
131        name: &PackageRef,
132    ) -> Result<String> {
133        let mut resolver = DependencyResolver::new_with_client(client, None)?;
134        let dependency = Dependency::Package(RegistryPackage {
135            name: Some(self.package.name.clone()),
136            version: self
137                .package
138                .version
139                .as_ref()
140                .unwrap_or(&VersionReq::STAR)
141                .clone(),
142            registry: self.registry.clone(),
143        });
144
145        resolver.add_dependency(name, &dependency).await?;
146
147        let dependencies = resolver.resolve().await?;
148        assert_eq!(dependencies.len(), 1);
149
150        match dependencies.values().next().expect("expected a resolution") {
151            DependencyResolution::Registry(resolution) => Ok(self
152                .package
153                .version
154                .as_ref()
155                .map(ToString::to_string)
156                .unwrap_or_else(|| resolution.version.to_string())),
157            _ => unreachable!(),
158        }
159    }
160
161    fn with_dependencies<F>(&self, pkg: &Package, body: F) -> Result<()>
162    where
163        F: FnOnce(&mut Table) -> Result<()>,
164    {
165        let manifest = fs::read_to_string(&pkg.manifest_path).with_context(|| {
166            format!(
167                "failed to read manifest file `{path}`",
168                path = pkg.manifest_path
169            )
170        })?;
171
172        let mut document: DocumentMut = manifest.parse().with_context(|| {
173            format!(
174                "failed to parse manifest file `{path}`",
175                path = pkg.manifest_path
176            )
177        })?;
178
179        let metadata = document["package"]["metadata"]
180            .or_insert(Item::Table(Table::new()))
181            .as_table_mut()
182            .context("section `package.metadata` is not a table")?;
183
184        metadata.set_implicit(true);
185
186        let component = metadata["component"]
187            .or_insert(Item::Table(Table::new()))
188            .as_table_mut()
189            .context("section `package.metadata.component` is not a table")?;
190
191        component.set_implicit(true);
192
193        let dependencies = if self.target {
194            let target = component["target"]
195                .or_insert(Item::Table(Table::new()))
196                .as_table_mut()
197                .context("section `package.metadata.component.target` is not a table")?;
198
199            target.set_implicit(true);
200
201            target["dependencies"]
202                .or_insert(Item::Table(Table::new()))
203                .as_table_mut()
204                .context(
205                    "section `package.metadata.component.target.dependencies` is not a table",
206                )?
207        } else {
208            component["dependencies"]
209                .or_insert(Item::Table(Table::new()))
210                .as_table_mut()
211                .context("section `package.metadata.component.dependencies` is not a table")?
212        };
213
214        body(dependencies)?;
215
216        if self.dry_run {
217            println!("{document}");
218        } else {
219            fs::write(&pkg.manifest_path, document.to_string()).with_context(|| {
220                format!(
221                    "failed to write manifest file `{path}`",
222                    path = pkg.manifest_path
223                )
224            })?;
225        }
226
227        Ok(())
228    }
229
230    fn add(&self, pkg: &Package, version: &str) -> Result<()> {
231        self.with_dependencies(pkg, |dependencies| {
232            match self.name.as_ref() {
233                Some(name) => {
234                    let str_name = name.to_string();
235                    dependencies[&str_name] = value(InlineTable::from_iter([
236                        ("package", Value::from(self.package.name.to_string())),
237                        ("version", Value::from(version)),
238                    ]));
239                }
240                _ => {
241                    let str_name = self.package.name.to_string();
242                    dependencies[&str_name] = value(version);
243                }
244            }
245            Ok(())
246        })
247    }
248
249    fn add_from_path(&self, pkg: &Package, path: &Path) -> Result<()> {
250        self.with_dependencies(pkg, |dependencies| {
251            let key = match self.name.as_ref() {
252                Some(name) => name.to_string(),
253                None => self.package.name.to_string(),
254            };
255
256            dependencies[&key] = value(InlineTable::from_iter([(
257                "path",
258                Value::from(path.to_str().unwrap()),
259            )]));
260
261            Ok(())
262        })
263    }
264
265    fn validate(&self, metadata: &ComponentMetadata, name: &PackageRef) -> Result<()> {
266        if self.target {
267            match &metadata.section.target {
268                Target::Package { .. } => {
269                    bail!("cannot add dependency `{name}` to a registry package target")
270                }
271                Target::Local { dependencies, .. } => {
272                    if dependencies.contains_key(name) {
273                        bail!("cannot add dependency `{name}` as it conflicts with an existing dependency");
274                    }
275                }
276            }
277        } else if metadata.section.dependencies.contains_key(name) {
278            bail!("cannot add dependency `{name}` as it conflicts with an existing dependency");
279        }
280
281        Ok(())
282    }
283}