]> git.proxmox.com Git - cargo.git/blame - src/cargo/ops/cargo_new.rs
More lint cleaning
[cargo.git] / src / cargo / ops / cargo_new.rs
CommitLineData
f8fb0a02 1use std::collections::BTreeMap;
ee5e24ff 2use std::env;
7e66058a
EH
3use std::fs;
4use std::path::Path;
7e66058a 5
10373f40
AO
6use serde::{Deserialize, Deserializer};
7use serde::de;
16d3e72a 8
5d0cb3f2 9use git2::Config as GitConfig;
4ebeb1f7 10use git2::Repository as GitRepository;
a4272ef2 11
58ddb28a 12use core::Workspace;
6633e290 13use ops::is_bad_artifact_name;
7eca7f27 14use util::{GitRepo, HgRepo, PijulRepo, FossilRepo, internal};
7e66058a 15use util::{Config, paths};
c7de4859 16use util::errors::{CargoError, CargoResult, CargoResultExt};
7e66058a
EH
17
18use toml;
284c0ef9 19
abe56727 20#[derive(Clone, Copy, Debug, PartialEq)]
7eca7f27 21pub enum VersionControl { Git, Hg, Pijul, Fossil, NoVcs }
16d3e72a 22
a4272ef2 23pub struct NewOptions<'a> {
16d3e72a 24 pub version_control: Option<VersionControl>,
a4272ef2 25 pub bin: bool,
a882abe7 26 pub lib: bool,
a4272ef2 27 pub path: &'a str,
99736d84 28 pub name: Option<&'a str>,
a4272ef2
AC
29}
30
800172fb
VS
31struct SourceFileInformation {
32 relative_path: String,
33 target_name: String,
34 bin: bool,
35}
36
37struct MkOptions<'a> {
38 version_control: Option<VersionControl>,
39 path: &'a Path,
40 name: &'a str,
7e66058a 41 source_files: Vec<SourceFileInformation>,
0e4fa582 42 bin: bool,
800172fb
VS
43}
44
10373f40
AO
45impl<'de> Deserialize<'de> for VersionControl {
46 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<VersionControl, D::Error> {
47 Ok(match &String::deserialize(d)?[..] {
16d3e72a
WW
48 "git" => VersionControl::Git,
49 "hg" => VersionControl::Hg,
2b31978c 50 "pijul" => VersionControl::Pijul,
7eca7f27 51 "fossil" => VersionControl::Fossil,
16d3e72a
WW
52 "none" => VersionControl::NoVcs,
53 n => {
10373f40
AO
54 let value = de::Unexpected::Str(n);
55 let msg = "unsupported version control system";
56 return Err(de::Error::invalid_value(value, &msg));
16d3e72a
WW
57 }
58 })
59 }
60}
61
a882abe7
JT
62impl<'a> NewOptions<'a> {
63 pub fn new(version_control: Option<VersionControl>,
64 bin: bool,
65 lib: bool,
66 path: &'a str,
7e66058a 67 name: Option<&'a str>) -> NewOptions<'a> {
a882abe7
JT
68
69 // default to lib
70 let is_lib = if !bin {
71 true
72 }
73 else {
74 lib
75 };
76
77 NewOptions {
78 version_control: version_control,
79 bin: bin,
80 lib: is_lib,
81 path: path,
82 name: name,
83 }
84 }
85}
86
de0f6041
AC
87struct CargoNewConfig {
88 name: Option<String>,
89 email: Option<String>,
16d3e72a 90 version_control: Option<VersionControl>,
de0f6041
AC
91}
92
800172fb
VS
93fn get_name<'a>(path: &'a Path, opts: &'a NewOptions, config: &Config) -> CargoResult<&'a str> {
94 if let Some(name) = opts.name {
95 return Ok(name);
96 }
455f800c 97
800172fb
VS
98 if path.file_name().is_none() {
99 bail!("cannot auto-detect project name from path {:?} ; use --name to override",
100 path.as_os_str());
101 }
455f800c 102
e95044e3 103 let dir_name = path.file_name().and_then(|s| s.to_str()).ok_or_else(|| {
c7de4859 104 CargoError::from(format!("cannot create a project with a non-unicode name: {:?}",
105 path.file_name().unwrap()))
82655b46 106 })?;
455f800c 107
800172fb
VS
108 if opts.bin {
109 Ok(dir_name)
110 } else {
111 let new_name = strip_rust_affixes(dir_name);
112 if new_name != dir_name {
f8fb0a02
AC
113 writeln!(config.shell().err(),
114 "note: package will be named `{}`; use --name to override",
115 new_name)?;
99736d84 116 }
800172fb
VS
117 Ok(new_name)
118 }
119}
120
6633e290 121fn check_name(name: &str, is_bin: bool) -> CargoResult<()> {
466c74a9
SBI
122
123 // Ban keywords + test list found at
124 // https://doc.rust-lang.org/grammar.html#keywords
125 let blacklist = ["abstract", "alignof", "as", "become", "box",
126 "break", "const", "continue", "crate", "do",
127 "else", "enum", "extern", "false", "final",
128 "fn", "for", "if", "impl", "in",
129 "let", "loop", "macro", "match", "mod",
130 "move", "mut", "offsetof", "override", "priv",
131 "proc", "pub", "pure", "ref", "return",
132 "self", "sizeof", "static", "struct",
133 "super", "test", "trait", "true", "type", "typeof",
134 "unsafe", "unsized", "use", "virtual", "where",
135 "while", "yield"];
6633e290 136 if blacklist.contains(&name) || (is_bin && is_bad_artifact_name(name)) {
d30e963c
TE
137 bail!("The name `{}` cannot be used as a crate name\n\
138 use --name to override crate name",
139 name)
140 }
141
73aa1174
RS
142 if let Some(ref c) = name.chars().nth(0) {
143 if c.is_digit(10) {
144 bail!("Package names starting with a digit cannot be used as a crate name\n\
145 use --name to override crate name")
146 }
147 }
148
9a387087
AK
149 for c in name.chars() {
150 if c.is_alphanumeric() { continue }
151 if c == '_' || c == '-' { continue }
800172fb
VS
152 bail!("Invalid character `{}` in crate name: `{}`\n\
153 use --name to override crate name",
154 c, name)
155 }
156 Ok(())
157}
158
455f800c
AC
159fn detect_source_paths_and_types(project_path : &Path,
160 project_name: &str,
800172fb
VS
161 detected_files: &mut Vec<SourceFileInformation>,
162 ) -> CargoResult<()> {
163 let path = project_path;
164 let name = project_name;
455f800c 165
800172fb
VS
166 enum H {
167 Bin,
168 Lib,
169 Detect,
170 }
455f800c 171
800172fb
VS
172 struct Test {
173 proposed_path: String,
174 handling: H,
175 }
455f800c 176
800172fb 177 let tests = vec![
23591fe5
LL
178 Test { proposed_path: String::from("src/main.rs"), handling: H::Bin },
179 Test { proposed_path: String::from("main.rs"), handling: H::Bin },
800172fb 180 Test { proposed_path: format!("src/{}.rs", name), handling: H::Detect },
23591fe5
LL
181 Test { proposed_path: format!("{}.rs", name), handling: H::Detect },
182 Test { proposed_path: String::from("src/lib.rs"), handling: H::Lib },
183 Test { proposed_path: String::from("lib.rs"), handling: H::Lib },
800172fb 184 ];
455f800c 185
800172fb
VS
186 for i in tests {
187 let pp = i.proposed_path;
455f800c 188
800172fb
VS
189 // path/pp does not exist or is not a file
190 if !fs::metadata(&path.join(&pp)).map(|x| x.is_file()).unwrap_or(false) {
191 continue;
192 }
455f800c 193
800172fb
VS
194 let sfi = match i.handling {
195 H::Bin => {
455f800c
AC
196 SourceFileInformation {
197 relative_path: pp,
198 target_name: project_name.to_string(),
199 bin: true
800172fb
VS
200 }
201 }
202 H::Lib => {
455f800c
AC
203 SourceFileInformation {
204 relative_path: pp,
205 target_name: project_name.to_string(),
800172fb
VS
206 bin: false
207 }
208 }
209 H::Detect => {
82655b46 210 let content = paths::read(&path.join(pp.clone()))?;
800172fb 211 let isbin = content.contains("fn main");
455f800c
AC
212 SourceFileInformation {
213 relative_path: pp,
214 target_name: project_name.to_string(),
215 bin: isbin
800172fb
VS
216 }
217 }
218 };
219 detected_files.push(sfi);
220 }
455f800c 221
800172fb 222 // Check for duplicate lib attempt
455f800c 223
800172fb
VS
224 let mut previous_lib_relpath : Option<&str> = None;
225 let mut duplicates_checker : BTreeMap<&str, &SourceFileInformation> = BTreeMap::new();
455f800c 226
800172fb
VS
227 for i in detected_files {
228 if i.bin {
229 if let Some(x) = BTreeMap::get::<str>(&duplicates_checker, i.target_name.as_ref()) {
230 bail!("\
231multiple possible binary sources found:
232 {}
233 {}
234cannot automatically generate Cargo.toml as the main target would be ambiguous",
235 &x.relative_path, &i.relative_path);
236 }
237 duplicates_checker.insert(i.target_name.as_ref(), i);
238 } else {
239 if let Some(plp) = previous_lib_relpath {
c7de4859 240 return Err(format!("cannot have a project with \
241 multiple libraries, \
242 found both `{}` and `{}`",
243 plp, i.relative_path).into());
800172fb
VS
244 }
245 previous_lib_relpath = Some(&i.relative_path);
246 }
9a387087 247 }
455f800c 248
800172fb
VS
249 Ok(())
250}
251
252fn plan_new_source_file(bin: bool, project_name: String) -> SourceFileInformation {
253 if bin {
455f800c 254 SourceFileInformation {
800172fb
VS
255 relative_path: "src/main.rs".to_string(),
256 target_name: project_name,
257 bin: true,
258 }
259 } else {
260 SourceFileInformation {
261 relative_path: "src/lib.rs".to_string(),
262 target_name: project_name,
263 bin: false,
264 }
265 }
266}
267
23591fe5 268pub fn new(opts: &NewOptions, config: &Config) -> CargoResult<()> {
800172fb
VS
269 let path = config.cwd().join(opts.path);
270 if fs::metadata(&path).is_ok() {
484a33af
BE
271 bail!("destination `{}` already exists\n\n\
272 Use `cargo init` to initialize the directory\
273 ", path.display()
274 )
800172fb 275 }
455f800c 276
a882abe7 277 if opts.lib && opts.bin {
484a33af 278 bail!("can't specify both lib and binary outputs")
a882abe7
JT
279 }
280
23591fe5 281 let name = get_name(&path, opts, config)?;
6633e290 282 check_name(name, opts.bin)?;
800172fb
VS
283
284 let mkopts = MkOptions {
285 version_control: opts.version_control,
286 path: &path,
287 name: name,
7e66058a 288 source_files: vec![plan_new_source_file(opts.bin, name.to_string())],
0e4fa582 289 bin: opts.bin,
800172fb 290 };
455f800c 291
e95044e3 292 mk(config, &mkopts).chain_err(|| {
c7de4859 293 format!("Failed to create project `{}` at `{}`",
294 name, path.display())
800172fb
VS
295 })
296}
297
23591fe5 298pub fn init(opts: &NewOptions, config: &Config) -> CargoResult<()> {
800172fb 299 let path = config.cwd().join(opts.path);
455f800c 300
800172fb
VS
301 let cargotoml_path = path.join("Cargo.toml");
302 if fs::metadata(&cargotoml_path).is_ok() {
303 bail!("`cargo init` cannot be run on existing Cargo projects")
304 }
455f800c 305
a882abe7
JT
306 if opts.lib && opts.bin {
307 bail!("can't specify both lib and binary outputs");
308 }
309
23591fe5 310 let name = get_name(&path, opts, config)?;
6633e290 311 check_name(name, opts.bin)?;
455f800c 312
800172fb 313 let mut src_paths_types = vec![];
455f800c 314
82655b46 315 detect_source_paths_and_types(&path, name, &mut src_paths_types)?;
455f800c 316
23591fe5 317 if src_paths_types.is_empty() {
800172fb
VS
318 src_paths_types.push(plan_new_source_file(opts.bin, name.to_string()));
319 } else {
320 // --bin option may be ignored if lib.rs or src/lib.rs present
321 // Maybe when doing `cargo init --bin` inside a library project stub,
322 // user may mean "initialize for library, but also add binary target"
323 }
455f800c 324
800172fb 325 let mut version_control = opts.version_control;
455f800c 326
800172fb
VS
327 if version_control == None {
328 let mut num_detected_vsces = 0;
455f800c 329
800172fb
VS
330 if fs::metadata(&path.join(".git")).is_ok() {
331 version_control = Some(VersionControl::Git);
332 num_detected_vsces += 1;
333 }
455f800c 334
800172fb
VS
335 if fs::metadata(&path.join(".hg")).is_ok() {
336 version_control = Some(VersionControl::Hg);
337 num_detected_vsces += 1;
338 }
455f800c 339
2b31978c
PW
340 if fs::metadata(&path.join(".pijul")).is_ok() {
341 version_control = Some(VersionControl::Pijul);
342 num_detected_vsces += 1;
343 }
344
7eca7f27
DK
345 if fs::metadata(&path.join(".fossil")).is_ok() {
346 version_control = Some(VersionControl::Fossil);
347 num_detected_vsces += 1;
348 }
349
800172fb 350 // if none exists, maybe create git, like in `cargo new`
455f800c 351
800172fb 352 if num_detected_vsces > 1 {
7eca7f27
DK
353 bail!("more than one of .hg, .git, .pijul, .fossil configurations \
354 found and the ignore file can't be filled in as \
355 a result. specify --vcs to override detection");
800172fb
VS
356 }
357 }
455f800c 358
800172fb
VS
359 let mkopts = MkOptions {
360 version_control: version_control,
361 path: &path,
362 name: name,
0e4fa582 363 bin: src_paths_types.iter().any(|x|x.bin),
7e66058a 364 source_files: src_paths_types,
800172fb 365 };
455f800c 366
e95044e3 367 mk(config, &mkopts).chain_err(|| {
c7de4859 368 format!("Failed to create project `{}` at `{}`",
369 name, path.display())
a4272ef2
AC
370 })
371}
372
3f298bca
CW
373fn strip_rust_affixes(name: &str) -> &str {
374 for &prefix in &["rust-", "rust_", "rs-", "rs_"] {
375 if name.starts_with(prefix) {
376 return &name[prefix.len()..];
377 }
378 }
379 for &suffix in &["-rust", "_rust", "-rs", "_rs"] {
380 if name.ends_with(suffix) {
381 return &name[..name.len()-suffix.len()];
382 }
383 }
384 name
385}
386
09d62a65
ML
387fn existing_vcs_repo(path: &Path, cwd: &Path) -> bool {
388 GitRepo::discover(path, cwd).is_ok() || HgRepo::discover(path, cwd).is_ok()
6c200854
PW
389}
390
800172fb
VS
391fn mk(config: &Config, opts: &MkOptions) -> CargoResult<()> {
392 let path = opts.path;
393 let name = opts.name;
82655b46 394 let cfg = global_config(config)?;
bef1f477 395 // Please ensure that ignore and hgignore are in sync.
06267a77 396 let ignore = ["/target/\n", "**/*.rs.bk\n",
78dce2cc
SM
397 if !opts.bin { "Cargo.lock\n" } else { "" }]
398 .concat();
bef1f477
SA
399 // Mercurial glob ignores can't be rooted, so just sticking a 'syntax: glob' at the top of the
400 // file will exclude too much. Instead, use regexp-based ignores. See 'hg help ignore' for
401 // more.
402 let hgignore = ["^target/\n", "glob:*.rs.bk\n",
403 if !opts.bin { "glob:Cargo.lock\n" } else { "" }]
404 .concat();
021f70af 405
50879f33 406 let in_existing_vcs_repo = existing_vcs_repo(path.parent().unwrap_or(path), config.cwd());
16d3e72a
WW
407 let vcs = match (opts.version_control, cfg.version_control, in_existing_vcs_repo) {
408 (None, None, false) => VersionControl::Git,
23591fe5 409 (None, Some(option), false) | (Some(option), _, _) => option,
16d3e72a
WW
410 (_, _, true) => VersionControl::NoVcs,
411 };
16d3e72a
WW
412 match vcs {
413 VersionControl::Git => {
800172fb 414 if !fs::metadata(&path.join(".git")).is_ok() {
82655b46 415 GitRepo::init(path, config.cwd())?;
800172fb 416 }
82655b46 417 paths::append(&path.join(".gitignore"), ignore.as_bytes())?;
16d3e72a
WW
418 },
419 VersionControl::Hg => {
800172fb 420 if !fs::metadata(&path.join(".hg")).is_ok() {
82655b46 421 HgRepo::init(path, config.cwd())?;
800172fb 422 }
bef1f477 423 paths::append(&path.join(".hgignore"), hgignore.as_bytes())?;
16d3e72a 424 },
2b31978c
PW
425 VersionControl::Pijul => {
426 if !fs::metadata(&path.join(".pijul")).is_ok() {
427 PijulRepo::init(path, config.cwd())?;
428 }
429 },
7eca7f27
DK
430 VersionControl::Fossil => {
431 if !fs::metadata(&path.join(".fossil")).is_ok() {
432 FossilRepo::init(path, config.cwd())?;
433 }
434 },
16d3e72a 435 VersionControl::NoVcs => {
82655b46 436 fs::create_dir_all(path)?;
16d3e72a
WW
437 },
438 };
a4272ef2 439
82655b46 440 let (author_name, email) = discover_author()?;
18293f4a 441 // Hoo boy, sure glad we've got exhaustiveness checking behind us.
7e66058a 442 let author = match (cfg.name, cfg.email, author_name, email) {
de0f6041
AC
443 (Some(name), Some(email), _, _) |
444 (Some(name), None, _, Some(email)) |
445 (None, Some(email), name, _) |
446 (None, None, name, Some(email)) => format!("{} <{}>", name, email),
447 (Some(name), None, _, None) |
448 (None, None, name, None) => name,
449 };
455f800c 450
7e66058a
EH
451 let mut cargotoml_path_specifier = String::new();
452
18293f4a 453 // Calculate what [lib] and [[bin]]s do we need to append to Cargo.toml
7e66058a
EH
454
455 for i in &opts.source_files {
456 if i.bin {
457 if i.relative_path != "src/main.rs" {
458 cargotoml_path_specifier.push_str(&format!(r#"
459[[bin]]
460name = "{}"
461path = {}
462"#, i.target_name, toml::Value::String(i.relative_path.clone())));
463 }
23591fe5
LL
464 } else if i.relative_path != "src/lib.rs" {
465 cargotoml_path_specifier.push_str(&format!(r#"
7e66058a
EH
466[lib]
467name = "{}"
468path = {}
469"#, i.target_name, toml::Value::String(i.relative_path.clone())));
7e66058a
EH
470 }
471 }
472
473 // Create Cargo.toml file with necessary [lib] and [[bin]] sections, if needed
474
475 paths::write(&path.join("Cargo.toml"), format!(
476r#"[package]
477name = "{}"
478version = "0.1.0"
479authors = [{}]
480
481[dependencies]
482{}"#, name, toml::Value::String(author), cargotoml_path_specifier).as_bytes())?;
483
484
485 // Create all specified source files
486 // (with respective parent directories)
487 // if they are don't exist
488
489 for i in &opts.source_files {
490 let path_of_source_file = path.join(i.relative_path.clone());
491
492 if let Some(src_dir) = path_of_source_file.parent() {
493 fs::create_dir_all(src_dir)?;
800172fb 494 }
455f800c 495
7e66058a
EH
496 let default_file_content : &[u8] = if i.bin {
497 b"\
498fn main() {
499 println!(\"Hello, world!\");
500}
501"
502 } else {
503 b"\
504#[cfg(test)]
505mod tests {
506 #[test]
7016687b
GB
507 fn it_works() {
508 assert_eq!(2 + 2, 4);
509 }
7e66058a
EH
510}
511"
512 };
513
514 if !fs::metadata(&path_of_source_file).map(|x| x.is_file()).unwrap_or(false) {
515 paths::write(&path_of_source_file, default_file_content)?;
516 }
a4272ef2
AC
517 }
518
58ddb28a
AC
519 if let Err(e) = Workspace::new(&path.join("Cargo.toml"), config) {
520 let msg = format!("compiling this new crate may not work due to invalid \
521 workspace configuration\n\n{}", e);
82655b46 522 config.shell().warn(msg)?;
58ddb28a 523 }
875a8aba 524
7e66058a 525 Ok(())
875a8aba
EH
526}
527
91e40657
SBI
528fn get_environment_variable(variables: &[&str] ) -> Option<String>{
529 variables.iter()
530 .filter_map(|var| env::var(var).ok())
531 .next()
532}
533
de0f6041 534fn discover_author() -> CargoResult<(String, Option<String>)> {
4ebeb1f7
HP
535 let cwd = env::current_dir()?;
536 let git_config = if let Ok(repo) = GitRepository::discover(&cwd) {
fe5a875e 537 repo.config().ok().or_else(|| GitConfig::open_default().ok())
4ebeb1f7
HP
538 } else {
539 GitConfig::open_default().ok()
540 };
c64fd71e 541 let git_config = git_config.as_ref();
c29be695
SBI
542 let name_variables = ["CARGO_NAME", "GIT_AUTHOR_NAME", "GIT_COMMITTER_NAME",
543 "USER", "USERNAME", "NAME"];
544 let name = get_environment_variable(&name_variables[0..3])
545 .or_else(|| git_config.and_then(|g| g.get_string("user.name").ok()))
546 .or_else(|| get_environment_variable(&name_variables[3..]));
91e40657 547
c64fd71e
AC
548 let name = match name {
549 Some(name) => name,
333af136
AC
550 None => {
551 let username_var = if cfg!(windows) {"USERNAME"} else {"USER"};
7ab18e3a
AC
552 bail!("could not determine the current user, please set ${}",
553 username_var)
333af136 554 }
a4272ef2 555 };
c29be695
SBI
556 let email_variables = ["CARGO_EMAIL", "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_EMAIL",
557 "EMAIL"];
558 let email = get_environment_variable(&email_variables[0..3])
559 .or_else(|| git_config.and_then(|g| g.get_string("user.email").ok()))
560 .or_else(|| get_environment_variable(&email_variables[3..]));
a4272ef2 561
25e537aa
AC
562 let name = name.trim().to_string();
563 let email = email.map(|s| s.trim().to_string());
a4272ef2 564
de0f6041
AC
565 Ok((name, email))
566}
567
5d0cb3f2 568fn global_config(config: &Config) -> CargoResult<CargoNewConfig> {
82655b46
SG
569 let name = config.get_string("cargo-new.name")?.map(|s| s.val);
570 let email = config.get_string("cargo-new.email")?.map(|s| s.val);
571 let vcs = config.get_string("cargo-new.vcs")?;
a468236a 572
455f800c 573 let vcs = match vcs.as_ref().map(|p| (&p.val[..], &p.definition)) {
a468236a
AC
574 Some(("git", _)) => Some(VersionControl::Git),
575 Some(("hg", _)) => Some(VersionControl::Hg),
576 Some(("none", _)) => Some(VersionControl::NoVcs),
577 Some((s, p)) => {
578 return Err(internal(format!("invalid configuration for key \
579 `cargo-new.vcs`, unknown vcs `{}` \
455f800c 580 (found in {})", s, p)))
de0f6041 581 }
a468236a 582 None => None
de0f6041 583 };
a468236a
AC
584 Ok(CargoNewConfig {
585 name: name,
586 email: email,
587 version_control: vcs,
588 })
a4272ef2 589}
3f298bca
CW
590
591#[cfg(test)]
592mod tests {
593 use super::strip_rust_affixes;
594
595 #[test]
596 fn affixes_stripped() {
597 assert_eq!(strip_rust_affixes("rust-foo"), "foo");
598 assert_eq!(strip_rust_affixes("foo-rs"), "foo");
599 assert_eq!(strip_rust_affixes("rs_foo"), "foo");
600 // Only one affix is stripped
601 assert_eq!(strip_rust_affixes("rs-foo-rs"), "foo-rs");
602 assert_eq!(strip_rust_affixes("foo-rs-rs"), "foo-rs");
603 // It shouldn't touch the middle
604 assert_eq!(strip_rust_affixes("some-rust-crate"), "some-rust-crate");
605 }
606}