cargo_component/commands/
add.rs1use 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#[derive(Args)]
31#[clap(disable_version_flag = true)]
32pub struct AddCommand {
33 #[clap(flatten)]
35 pub common: CommonOptions,
36
37 #[clap(long = "manifest-path", value_name = "PATH")]
39 pub manifest_path: Option<PathBuf>,
40
41 #[clap(long = "dry-run")]
43 pub dry_run: bool,
44
45 #[clap(long = "package", short = 'p', value_name = "SPEC")]
47 pub spec: Option<CargoPackageSpec>,
48
49 #[clap(long = "registry", short = 'r', value_name = "REGISTRY")]
51 pub registry: Option<String>,
52
53 #[clap(long, value_name = "NAME")]
55 pub name: Option<PackageRef>,
56
57 #[clap(value_name = "PACKAGE")]
59 pub package: VersionedPackageName,
60
61 #[clap(long = "target")]
63 pub target: bool,
64
65 #[clap(long = "path", value_name = "PATH")]
67 pub path: Option<PathBuf>,
68}
69
70impl AddCommand {
71 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}