1 //! Handle `cargo add` arguments
2
3 use cargo_edit::{find, registry_url, Dependency};
4 use cargo_edit::{get_latest_dependency, CrateName};
5 use std::path::PathBuf;
6 use structopt::StructOpt;
7
8 use crate::errors::*;
9
10 #[derive(Debug, StructOpt)]
11 #[structopt(bin_name = "cargo")]
12 pub enum Command {
13 /// Add dependency to a Cargo.toml manifest file.
14 #[structopt(name = "add")]
15 #[structopt(
16 after_help = "This command allows you to add a dependency to a Cargo.toml manifest file. If <crate> is a github
17 or gitlab repository URL, or a local path, `cargo add` will try to automatically get the crate name
18 and set the appropriate `--git` or `--path` value.
19
20 Please note that Cargo treats versions like '1.2.3' as '^1.2.3' (and that '^1.2.3' is specified
21 as '>=1.2.3 and <2.0.0'). By default, `cargo add` will use this format, as it is the one that the
22 crates.io registry suggests. One goal of `cargo add` is to prevent you from using wildcard
23 dependencies (version set to '*')."
24 )]
25 Add(Args),
26 }
27
28 #[derive(Debug, StructOpt)]
29 pub struct Args {
30 /// Crates to be added.
31 #[structopt(name = "crate", required = true)]
32 pub crates: Vec<String>,
33
34 /// Rename a dependency in Cargo.toml,
35 /// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#renaming-dependencies-in-cargotoml
36 /// Only works when specifying a single dependency.
37 #[structopt(long = "rename", short = "r")]
38 pub rename: Option<String>,
39
40 /// Add crate as development dependency.
41 #[structopt(long = "dev", short = "D", conflicts_with = "build")]
42 pub dev: bool,
43
44 /// Add crate as build dependency.
45 #[structopt(long = "build", short = "B", conflicts_with = "dev")]
46 pub build: bool,
47
48 /// Specify the version to grab from the registry(crates.io).
49 /// You can also specify version as part of name, e.g
50 /// `cargo add bitflags@0.3.2`.
51 #[structopt(long = "vers", value_name = "uri", conflicts_with = "git")]
52 pub vers: Option<String>,
53
54 /// Specify a git repository to download the crate from.
55 #[structopt(
56 long = "git",
57 value_name = "uri",
58 conflicts_with = "vers",
59 conflicts_with = "path"
60 )]
61 pub git: Option<String>,
62
63 /// Specify a git branch to download the crate from.
64 #[structopt(
65 long = "branch",
66 value_name = "branch",
67 conflicts_with = "vers",
68 conflicts_with = "path"
69 )]
70 pub branch: Option<String>,
71
72 /// Specify the path the crate should be loaded from.
73 #[structopt(long = "path", conflicts_with = "git")]
74 pub path: Option<PathBuf>,
75
76 /// Add as dependency to the given target platform.
77 #[structopt(long = "target", conflicts_with = "dev", conflicts_with = "build")]
78 pub target: Option<String>,
79
80 /// Add as an optional dependency (for use in features).
81 #[structopt(long = "optional", conflicts_with = "dev", conflicts_with = "build")]
82 pub optional: bool,
83
84 /// Path to the manifest to add a dependency to.
85 #[structopt(long = "manifest-path", value_name = "path", conflicts_with = "pkgid")]
86 pub manifest_path: Option<PathBuf>,
87
88 /// Package id of the crate to add this dependency to.
89 #[structopt(
90 long = "package",
91 short = "p",
92 value_name = "pkgid",
93 conflicts_with = "path"
94 )]
95 pub pkgid: Option<String>,
96
97 /// Choose method of semantic version upgrade. Must be one of "none" (exact version, `=`
98 /// modifier), "patch" (`~` modifier), "minor" (`^` modifier), "all" (`>=`), or "default" (no
99 /// modifier).
100 #[structopt(
101 long = "upgrade",
102 value_name = "method",
103 possible_value = "none",
104 possible_value = "patch",
105 possible_value = "minor",
106 possible_value = "all",
107 possible_value = "default",
108 default_value = "default"
109 )]
110 pub upgrade: String,
111
112 /// Include prerelease versions when fetching from crates.io (e.g.
113 /// '0.6.0-alpha').
114 #[structopt(long = "allow-prerelease")]
115 pub allow_prerelease: bool,
116
117 /// Space-separated list of features to add. For an alternative approach to
118 /// enabling features, consider installing the `cargo-feature` utility.
119 #[structopt(long = "features", number_of_values = 1)]
120 pub features: Option<Vec<String>>,
121
122 /// Set `default-features = false` for the added dependency.
123 #[structopt(long = "no-default-features")]
124 pub no_default_features: bool,
125
126 /// Do not print any output in case of success.
127 #[structopt(long = "quiet", short = "q")]
128 pub quiet: bool,
129
130 /// Run without accessing the network
131 #[structopt(long = "offline")]
132 pub offline: bool,
133
134 /// Sort dependencies even if currently unsorted
135 #[structopt(long = "sort", short = "s")]
136 pub sort: bool,
137
138 /// Registry to use
139 #[structopt(long = "registry", conflicts_with = "git", conflicts_with = "path")]
140 pub registry: Option<String>,
141 }
142
parse_version_req(s: &str) -> Result<&str>143 fn parse_version_req(s: &str) -> Result<&str> {
144 semver::VersionReq::parse(s).chain_err(|| "Invalid dependency version requirement")?;
145 Ok(s)
146 }
147
148 impl Args {
149 /// Get dependency section
get_section(&self) -> Vec<String>150 pub fn get_section(&self) -> Vec<String> {
151 if self.dev {
152 vec!["dev-dependencies".to_owned()]
153 } else if self.build {
154 vec!["build-dependencies".to_owned()]
155 } else if let Some(ref target) = self.target {
156 if target.is_empty() {
157 panic!("Target specification may not be empty");
158 }
159 vec![
160 "target".to_owned(),
161 target.clone(),
162 "dependencies".to_owned(),
163 ]
164 } else {
165 vec!["dependencies".to_owned()]
166 }
167 }
168
parse_single_dependency(&self, crate_name: &str) -> Result<Dependency>169 fn parse_single_dependency(&self, crate_name: &str) -> Result<Dependency> {
170 let crate_name = CrateName::new(crate_name);
171
172 if let Some(mut dependency) = crate_name.parse_as_version()? {
173 // crate specifier includes a version (e.g. `docopt@0.8`)
174 if let Some(ref url) = self.git {
175 let url = url.clone();
176 let version = dependency.version().unwrap().to_string();
177 return Err(ErrorKind::GitUrlWithVersion(url, version).into());
178 }
179
180 if let Some(ref path) = self.path {
181 dependency = dependency.set_path(path.to_str().unwrap());
182 }
183
184 Ok(dependency)
185 } else if crate_name.is_url_or_path() {
186 Ok(crate_name.parse_crate_name_from_uri()?)
187 } else {
188 assert_eq!(self.git.is_some() && self.vers.is_some(), false);
189 assert_eq!(self.git.is_some() && self.path.is_some(), false);
190 assert_eq!(self.git.is_some() && self.registry.is_some(), false);
191 assert_eq!(self.path.is_some() && self.registry.is_some(), false);
192
193 let mut dependency = Dependency::new(crate_name.name());
194
195 if let Some(repo) = &self.git {
196 dependency = dependency.set_git(repo, self.branch.clone());
197 }
198 if let Some(path) = &self.path {
199 dependency = dependency.set_path(path.to_str().unwrap());
200 }
201 if let Some(version) = &self.vers {
202 dependency = dependency.set_version(parse_version_req(version)?);
203 }
204 let registry_url = if let Some(registry) = &self.registry {
205 Some(registry_url(&find(&self.manifest_path)?, Some(registry))?)
206 } else {
207 None
208 };
209
210 if self.git.is_none() && self.path.is_none() && self.vers.is_none() {
211 let dep = get_latest_dependency(
212 crate_name.name(),
213 self.allow_prerelease,
214 &find(&self.manifest_path)?,
215 ®istry_url,
216 )?;
217 let v = format!(
218 "{prefix}{version}",
219 prefix = self.get_upgrade_prefix(),
220 // If version is unavailable `get_latest_dependency` must have
221 // returned `Err(FetchVersionError::GetVersion)`
222 version = dep.version().unwrap_or_else(|| unreachable!())
223 );
224 dependency = dep.set_version(&v);
225 }
226
227 // Set the registry after getting the latest version as
228 // get_latest_dependency returns a registry-less Dependency
229 if let Some(registry) = &self.registry {
230 dependency = dependency.set_registry(registry);
231 }
232 Ok(dependency)
233 }
234 }
235
236 /// Build dependencies from arguments
parse_dependencies(&self) -> Result<Vec<Dependency>>237 pub fn parse_dependencies(&self) -> Result<Vec<Dependency>> {
238 if self.crates.len() > 1
239 && (self.git.is_some() || self.path.is_some() || self.vers.is_some())
240 {
241 return Err(ErrorKind::MultipleCratesWithGitOrPathOrVers.into());
242 }
243
244 if self.crates.len() > 1 && self.rename.is_some() {
245 return Err(ErrorKind::MultipleCratesWithRename.into());
246 }
247
248 if self.crates.len() > 1 && self.features.is_some() {
249 return Err(ErrorKind::MultipleCratesWithFeatures.into());
250 }
251
252 self.crates
253 .iter()
254 .map(|crate_name| {
255 self.parse_single_dependency(crate_name).map(|x| {
256 let mut x = x
257 .set_optional(self.optional)
258 .set_features(self.features.clone())
259 .set_default_features(!self.no_default_features);
260 if let Some(ref rename) = self.rename {
261 x = x.set_rename(rename);
262 }
263 x
264 })
265 })
266 .collect()
267 }
268
get_upgrade_prefix(&self) -> &'static str269 fn get_upgrade_prefix(&self) -> &'static str {
270 match self.upgrade.as_ref() {
271 "default" => "",
272 "none" => "=",
273 "patch" => "~",
274 "minor" => "^",
275 "all" => ">=",
276 _ => unreachable!(),
277 }
278 }
279 }
280
281 #[cfg(test)]
282 impl Default for Args {
default() -> Args283 fn default() -> Args {
284 Args {
285 crates: vec!["demo".to_owned()],
286 rename: None,
287 dev: false,
288 build: false,
289 vers: None,
290 git: None,
291 branch: None,
292 path: None,
293 target: None,
294 optional: false,
295 manifest_path: None,
296 pkgid: None,
297 upgrade: "minor".to_string(),
298 allow_prerelease: false,
299 features: None,
300 no_default_features: false,
301 quiet: false,
302 offline: true,
303 sort: false,
304 registry: None,
305 }
306 }
307 }
308
309 #[cfg(test)]
310 mod tests {
311 use super::*;
312 use cargo_edit::Dependency;
313
314 #[test]
test_dependency_parsing()315 fn test_dependency_parsing() {
316 let args = Args {
317 vers: Some("0.4.2".to_owned()),
318 ..Args::default()
319 };
320
321 assert_eq!(
322 args.parse_dependencies().unwrap(),
323 vec![Dependency::new("demo").set_version("0.4.2")]
324 );
325 }
326
327 #[test]
328 #[cfg(feature = "test-external-apis")]
test_repo_as_arg_parsing()329 fn test_repo_as_arg_parsing() {
330 let github_url = "https://github.com/killercup/cargo-edit/";
331 let args_github = Args {
332 crates: vec![github_url.to_owned()],
333 ..Args::default()
334 };
335 assert_eq!(
336 args_github.parse_dependencies().unwrap(),
337 vec![Dependency::new("cargo-edit").set_git(github_url, None)]
338 );
339
340 let gitlab_url = "https://gitlab.com/Polly-lang/Polly.git";
341 let args_gitlab = Args {
342 crates: vec![gitlab_url.to_owned()],
343 ..Args::default()
344 };
345 assert_eq!(
346 args_gitlab.parse_dependencies().unwrap(),
347 vec![Dependency::new("polly").set_git(gitlab_url, None)]
348 );
349 }
350
351 #[test]
test_path_as_arg_parsing()352 fn test_path_as_arg_parsing() {
353 let self_path = ".";
354 let args_path = Args {
355 crates: vec![self_path.to_owned()],
356 ..Args::default()
357 };
358 assert_eq!(
359 args_path.parse_dependencies().unwrap(),
360 vec![Dependency::new("cargo-edit").set_path(self_path)]
361 );
362 }
363 }
364