1use crate::core::{Shell, Workspace};
2use crate::util::errors::{CargoResult, CargoResultExt};
3use crate::util::{existing_vcs_repo, FossilRepo, GitRepo, HgRepo, PijulRepo};
4use crate::util::{paths, restricted_names, Config};
5use git2::Config as GitConfig;
6use git2::Repository as GitRepository;
7use serde::de;
8use serde::Deserialize;
9use std::collections::BTreeMap;
10use std::env;
11use std::fmt;
12use std::fs;
13use std::io::{BufRead, BufReader, ErrorKind};
14use std::path::{Path, PathBuf};
15use std::process::Command;
16use std::str::{from_utf8, FromStr};
17
18#[derive(Clone, Copy, Debug, PartialEq)]
19pub enum VersionControl {
20 Git,
21 Hg,
22 Pijul,
23 Fossil,
24 NoVcs,
25}
26
27impl FromStr for VersionControl {
28 type Err = anyhow::Error;
29
30 fn from_str(s: &str) -> Result<Self, anyhow::Error> {
31 match s {
32 "git" => Ok(VersionControl::Git),
33 "hg" => Ok(VersionControl::Hg),
34 "pijul" => Ok(VersionControl::Pijul),
35 "fossil" => Ok(VersionControl::Fossil),
36 "none" => Ok(VersionControl::NoVcs),
37 other => anyhow::bail!("unknown vcs specification: `{}`", other),
38 }
39 }
40}
41
42impl<'de> de::Deserialize<'de> for VersionControl {
43 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
44 where
45 D: de::Deserializer<'de>,
46 {
47 let s = String::deserialize(deserializer)?;
48 FromStr::from_str(&s).map_err(de::Error::custom)
49 }
50}
51
52#[derive(Debug)]
53pub struct NewOptions {
54 pub version_control: Option<VersionControl>,
55 pub kind: NewProjectKind,
56 pub path: PathBuf,
58 pub name: Option<String>,
59 pub edition: Option<String>,
60 pub registry: Option<String>,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum NewProjectKind {
65 Bin,
66 Lib,
67}
68
69impl NewProjectKind {
70 fn is_bin(self) -> bool {
71 self == NewProjectKind::Bin
72 }
73}
74
75impl fmt::Display for NewProjectKind {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 match *self {
78 NewProjectKind::Bin => "binary (application)",
79 NewProjectKind::Lib => "library",
80 }
81 .fmt(f)
82 }
83}
84
85struct SourceFileInformation {
86 relative_path: String,
87 target_name: String,
88 bin: bool,
89}
90
91struct MkOptions<'a> {
92 version_control: Option<VersionControl>,
93 path: &'a Path,
94 name: &'a str,
95 source_files: Vec<SourceFileInformation>,
96 bin: bool,
97 edition: Option<&'a str>,
98 registry: Option<&'a str>,
99}
100
101impl NewOptions {
102 pub fn new(
103 version_control: Option<VersionControl>,
104 bin: bool,
105 lib: bool,
106 path: PathBuf,
107 name: Option<String>,
108 edition: Option<String>,
109 registry: Option<String>,
110 ) -> CargoResult<NewOptions> {
111 let kind = match (bin, lib) {
112 (true, true) => anyhow::bail!("can't specify both lib and binary outputs"),
113 (false, true) => NewProjectKind::Lib,
114 (_, false) => NewProjectKind::Bin,
116 };
117
118 let opts = NewOptions {
119 version_control,
120 kind,
121 path,
122 name,
123 edition,
124 registry,
125 };
126 Ok(opts)
127 }
128}
129
130#[derive(Deserialize)]
131struct CargoNewConfig {
132 name: Option<String>,
133 email: Option<String>,
134 #[serde(rename = "vcs")]
135 version_control: Option<VersionControl>,
136}
137
138fn get_name<'a>(path: &'a Path, opts: &'a NewOptions) -> CargoResult<&'a str> {
139 if let Some(ref name) = opts.name {
140 return Ok(name);
141 }
142
143 let file_name = path.file_name().ok_or_else(|| {
144 anyhow::format_err!(
145 "cannot auto-detect package name from path {:?} ; use --name to override",
146 path.as_os_str()
147 )
148 })?;
149
150 file_name.to_str().ok_or_else(|| {
151 anyhow::format_err!(
152 "cannot create package with a non-unicode name: {:?}",
153 file_name
154 )
155 })
156}
157
158fn check_name(name: &str, name_help: &str, has_bin: bool, shell: &mut Shell) -> CargoResult<()> {
159 restricted_names::validate_package_name(name, "crate name", name_help)?;
160
161 if restricted_names::is_keyword(name) {
162 anyhow::bail!(
163 "the name `{}` cannot be used as a crate name, it is a Rust keyword{}",
164 name,
165 name_help
166 );
167 }
168 if restricted_names::is_conflicting_artifact_name(name) {
169 if has_bin {
170 anyhow::bail!(
171 "the name `{}` cannot be used as a crate name, \
172 it conflicts with cargo's build directory names{}",
173 name,
174 name_help
175 );
176 } else {
177 shell.warn(format!(
178 "the name `{}` will not support binary \
179 executables with that name, \
180 it conflicts with cargo's build directory names",
181 name
182 ))?;
183 }
184 }
185 if name == "test" {
186 anyhow::bail!(
187 "the name `test` cannot be used as a crate name, \
188 it conflicts with Rust's built-in test library{}",
189 name_help
190 );
191 }
192 if ["core", "std", "alloc", "proc_macro", "proc-macro"].contains(&name) {
193 shell.warn(format!(
194 "the name `{}` is part of Rust's standard library\n\
195 It is recommended to use a different name to avoid problems.",
196 name
197 ))?;
198 }
199 if restricted_names::is_windows_reserved(name) {
200 if cfg!(windows) {
201 anyhow::bail!(
202 "cannot use name `{}`, it is a reserved Windows filename{}",
203 name,
204 name_help
205 );
206 } else {
207 shell.warn(format!(
208 "the name `{}` is a reserved Windows filename\n\
209 This package will not work on Windows platforms.",
210 name
211 ))?;
212 }
213 }
214 if restricted_names::is_non_ascii_name(name) {
215 shell.warn(format!(
216 "the name `{}` contains non-ASCII characters\n\
217 Support for non-ASCII crate names is experimental and only valid \
218 on the nightly toolchain.",
219 name
220 ))?;
221 }
222
223 Ok(())
224}
225
226fn detect_source_paths_and_types(
227 package_path: &Path,
228 package_name: &str,
229 detected_files: &mut Vec<SourceFileInformation>,
230) -> CargoResult<()> {
231 let path = package_path;
232 let name = package_name;
233
234 enum H {
235 Bin,
236 Lib,
237 Detect,
238 }
239
240 struct Test {
241 proposed_path: String,
242 handling: H,
243 }
244
245 let tests = vec![
246 Test {
247 proposed_path: "src/main.rs".to_string(),
248 handling: H::Bin,
249 },
250 Test {
251 proposed_path: "main.rs".to_string(),
252 handling: H::Bin,
253 },
254 Test {
255 proposed_path: format!("src/{}.rs", name),
256 handling: H::Detect,
257 },
258 Test {
259 proposed_path: format!("{}.rs", name),
260 handling: H::Detect,
261 },
262 Test {
263 proposed_path: "src/lib.rs".to_string(),
264 handling: H::Lib,
265 },
266 Test {
267 proposed_path: "lib.rs".to_string(),
268 handling: H::Lib,
269 },
270 ];
271
272 for i in tests {
273 let pp = i.proposed_path;
274
275 if !fs::metadata(&path.join(&pp))
277 .map(|x| x.is_file())
278 .unwrap_or(false)
279 {
280 continue;
281 }
282
283 let sfi = match i.handling {
284 H::Bin => SourceFileInformation {
285 relative_path: pp,
286 target_name: package_name.to_string(),
287 bin: true,
288 },
289 H::Lib => SourceFileInformation {
290 relative_path: pp,
291 target_name: package_name.to_string(),
292 bin: false,
293 },
294 H::Detect => {
295 let content = paths::read(&path.join(pp.clone()))?;
296 let isbin = content.contains("fn main");
297 SourceFileInformation {
298 relative_path: pp,
299 target_name: package_name.to_string(),
300 bin: isbin,
301 }
302 }
303 };
304 detected_files.push(sfi);
305 }
306
307 let mut previous_lib_relpath: Option<&str> = None;
310 let mut duplicates_checker: BTreeMap<&str, &SourceFileInformation> = BTreeMap::new();
311
312 for i in detected_files {
313 if i.bin {
314 if let Some(x) = BTreeMap::get::<str>(&duplicates_checker, i.target_name.as_ref()) {
315 anyhow::bail!(
316 "\
317multiple possible binary sources found:
318 {}
319 {}
320cannot automatically generate Cargo.toml as the main target would be ambiguous",
321 &x.relative_path,
322 &i.relative_path
323 );
324 }
325 duplicates_checker.insert(i.target_name.as_ref(), i);
326 } else {
327 if let Some(plp) = previous_lib_relpath {
328 anyhow::bail!(
329 "cannot have a package with \
330 multiple libraries, \
331 found both `{}` and `{}`",
332 plp,
333 i.relative_path
334 )
335 }
336 previous_lib_relpath = Some(&i.relative_path);
337 }
338 }
339
340 Ok(())
341}
342
343fn plan_new_source_file(bin: bool, package_name: String) -> SourceFileInformation {
344 if bin {
345 SourceFileInformation {
346 relative_path: "src/main.rs".to_string(),
347 target_name: package_name,
348 bin: true,
349 }
350 } else {
351 SourceFileInformation {
352 relative_path: "src/lib.rs".to_string(),
353 target_name: package_name,
354 bin: false,
355 }
356 }
357}
358
359pub fn new(opts: &NewOptions, config: &Config) -> CargoResult<()> {
360 let path = &opts.path;
361 if fs::metadata(path).is_ok() {
362 anyhow::bail!(
363 "destination `{}` already exists\n\n\
364 Use `cargo init` to initialize the directory",
365 path.display()
366 )
367 }
368
369 let name = get_name(path, opts)?;
370 check_name(name, "", opts.kind.is_bin(), &mut config.shell())?;
371
372 let mkopts = MkOptions {
373 version_control: opts.version_control,
374 path,
375 name,
376 source_files: vec![plan_new_source_file(opts.kind.is_bin(), name.to_string())],
377 bin: opts.kind.is_bin(),
378 edition: opts.edition.as_deref(),
379 registry: opts.registry.as_deref(),
380 };
381
382 mk(config, &mkopts).chain_err(|| {
383 anyhow::format_err!(
384 "Failed to create package `{}` at `{}`",
385 name,
386 path.display()
387 )
388 })?;
389 Ok(())
390}
391
392pub fn init(opts: &NewOptions, config: &Config) -> CargoResult<()> {
393 if std::env::var_os("__CARGO_TEST_INTERNAL_ERROR").is_some() {
395 return Err(crate::util::internal("internal error test"));
396 }
397
398 let path = &opts.path;
399
400 if fs::metadata(&path.join("Cargo.toml")).is_ok() {
401 anyhow::bail!("`cargo init` cannot be run on existing Cargo packages")
402 }
403
404 let name = get_name(path, opts)?;
405
406 let mut src_paths_types = vec![];
407
408 detect_source_paths_and_types(path, name, &mut src_paths_types)?;
409
410 if src_paths_types.is_empty() {
411 src_paths_types.push(plan_new_source_file(opts.kind.is_bin(), name.to_string()));
412 } else {
413 }
417 let has_bin = src_paths_types.iter().any(|x| x.bin);
418 let name_help = match opts.name {
421 Some(_) => "",
422 None => "\nuse --name to override crate name",
423 };
424 check_name(name, name_help, has_bin, &mut config.shell())?;
425
426 let mut version_control = opts.version_control;
427
428 if version_control == None {
429 let mut num_detected_vsces = 0;
430
431 if fs::metadata(&path.join(".git")).is_ok() {
432 version_control = Some(VersionControl::Git);
433 num_detected_vsces += 1;
434 }
435
436 if fs::metadata(&path.join(".hg")).is_ok() {
437 version_control = Some(VersionControl::Hg);
438 num_detected_vsces += 1;
439 }
440
441 if fs::metadata(&path.join(".pijul")).is_ok() {
442 version_control = Some(VersionControl::Pijul);
443 num_detected_vsces += 1;
444 }
445
446 if fs::metadata(&path.join(".fossil")).is_ok() {
447 version_control = Some(VersionControl::Fossil);
448 num_detected_vsces += 1;
449 }
450
451 if num_detected_vsces > 1 {
454 anyhow::bail!(
455 "more than one of .hg, .git, .pijul, .fossil configurations \
456 found and the ignore file can't be filled in as \
457 a result. specify --vcs to override detection"
458 );
459 }
460 }
461
462 let mkopts = MkOptions {
463 version_control,
464 path,
465 name,
466 bin: has_bin,
467 source_files: src_paths_types,
468 edition: opts.edition.as_deref(),
469 registry: opts.registry.as_deref(),
470 };
471
472 mk(config, &mkopts).chain_err(|| {
473 anyhow::format_err!(
474 "Failed to create package `{}` at `{}`",
475 name,
476 path.display()
477 )
478 })?;
479 Ok(())
480}
481
482struct IgnoreList {
484 ignore: Vec<String>,
486 hg_ignore: Vec<String>,
488}
489
490impl IgnoreList {
491 fn new() -> IgnoreList {
493 IgnoreList {
494 ignore: Vec::new(),
495 hg_ignore: Vec::new(),
496 }
497 }
498
499 fn push(&mut self, ignore: &str, hg_ignore: &str) {
503 self.ignore.push(ignore.to_string());
504 self.hg_ignore.push(hg_ignore.to_string());
505 }
506
507 fn format_new(&self, vcs: VersionControl) -> String {
510 let ignore_items = match vcs {
511 VersionControl::Hg => &self.hg_ignore,
512 _ => &self.ignore,
513 };
514
515 ignore_items.join("\n") + "\n"
516 }
517
518 fn format_existing<T: BufRead>(&self, existing: T, vcs: VersionControl) -> String {
523 let existing_items = existing.lines().collect::<Result<Vec<_>, _>>().unwrap();
525
526 let ignore_items = match vcs {
527 VersionControl::Hg => &self.hg_ignore,
528 _ => &self.ignore,
529 };
530
531 let mut out = "\n\n#Added by cargo\n".to_string();
532 if ignore_items
533 .iter()
534 .any(|item| existing_items.contains(item))
535 {
536 out.push_str("#\n#already existing elements were commented out\n");
537 }
538 out.push('\n');
539
540 for item in ignore_items {
541 if existing_items.contains(item) {
542 out.push('#');
543 }
544 out.push_str(item);
545 out.push('\n');
546 }
547
548 out
549 }
550}
551
552fn write_ignore_file(
556 base_path: &Path,
557 list: &IgnoreList,
558 vcs: VersionControl,
559) -> CargoResult<String> {
560 let fp_ignore = match vcs {
561 VersionControl::Git => base_path.join(".gitignore"),
562 VersionControl::Hg => base_path.join(".hgignore"),
563 VersionControl::Pijul => base_path.join(".ignore"),
564 VersionControl::Fossil => return Ok("".to_string()),
565 VersionControl::NoVcs => return Ok("".to_string()),
566 };
567
568 let ignore: String = match fs::File::open(&fp_ignore) {
569 Err(why) => match why.kind() {
570 ErrorKind::NotFound => list.format_new(vcs),
571 _ => return Err(anyhow::format_err!("{}", why)),
572 },
573 Ok(file) => list.format_existing(BufReader::new(file), vcs),
574 };
575
576 paths::append(&fp_ignore, ignore.as_bytes())?;
577
578 Ok(ignore)
579}
580
581fn init_vcs(path: &Path, vcs: VersionControl, config: &Config) -> CargoResult<()> {
583 match vcs {
584 VersionControl::Git => {
585 if !path.join(".git").exists() {
586 paths::create_dir_all(path)?;
590 GitRepo::init(path, config.cwd())?;
591 }
592 }
593 VersionControl::Hg => {
594 if !path.join(".hg").exists() {
595 HgRepo::init(path, config.cwd())?;
596 }
597 }
598 VersionControl::Pijul => {
599 if !path.join(".pijul").exists() {
600 PijulRepo::init(path, config.cwd())?;
601 }
602 }
603 VersionControl::Fossil => {
604 if !path.join(".fossil").exists() {
605 FossilRepo::init(path, config.cwd())?;
606 }
607 }
608 VersionControl::NoVcs => {
609 paths::create_dir_all(path)?;
610 }
611 };
612
613 Ok(())
614}
615
616fn mk(config: &Config, opts: &MkOptions<'_>) -> CargoResult<()> {
617 let path = opts.path;
618 let name = opts.name;
619 let cfg = config.get::<CargoNewConfig>("cargo-new")?;
620
621 let mut ignore = IgnoreList::new();
624 ignore.push("/target", "^target/");
625 if !opts.bin {
626 ignore.push("Cargo.lock", "glob:Cargo.lock");
627 }
628
629 let vcs = opts.version_control.unwrap_or_else(|| {
630 let in_existing_vcs = existing_vcs_repo(path.parent().unwrap_or(path), config.cwd());
631 match (cfg.version_control, in_existing_vcs) {
632 (None, false) => VersionControl::Git,
633 (Some(opt), false) => opt,
634 (_, true) => VersionControl::NoVcs,
635 }
636 });
637
638 init_vcs(path, vcs, config)?;
639 write_ignore_file(path, &ignore, vcs)?;
640
641 let (author_name, email) = discover_author()?;
642 let author = match (cfg.name, cfg.email, author_name, email) {
643 (Some(name), Some(email), _, _)
644 | (Some(name), None, _, Some(email))
645 | (None, Some(email), name, _)
646 | (None, None, name, Some(email)) => {
647 if email.is_empty() {
648 name
649 } else {
650 format!("{} <{}>", name, email)
651 }
652 }
653 (Some(name), None, _, None) | (None, None, name, None) => name,
654 };
655
656 let mut cargotoml_path_specifier = String::new();
657
658 for i in &opts.source_files {
661 if i.bin {
662 if i.relative_path != "src/main.rs" {
663 cargotoml_path_specifier.push_str(&format!(
664 r#"
665[[bin]]
666name = "{}"
667path = {}
668"#,
669 i.target_name,
670 toml::Value::String(i.relative_path.clone())
671 ));
672 }
673 } else if i.relative_path != "src/lib.rs" {
674 cargotoml_path_specifier.push_str(&format!(
675 r#"
676[lib]
677name = "{}"
678path = {}
679"#,
680 i.target_name,
681 toml::Value::String(i.relative_path.clone())
682 ));
683 }
684 }
685
686 paths::write(
689 &path.join("Cargo.toml"),
690 format!(
691 r#"[package]
692name = "{}"
693version = "0.1.0"
694authors = [{}]
695edition = {}
696{}
697# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
698
699[dependencies]
700{}"#,
701 name,
702 toml::Value::String(author),
703 match opts.edition {
704 Some(edition) => toml::Value::String(edition.to_string()),
705 None => toml::Value::String("2018".to_string()),
706 },
707 match opts.registry {
708 Some(registry) => format!(
709 "publish = {}\n",
710 toml::Value::Array(vec!(toml::Value::String(registry.to_string())))
711 ),
712 None => "".to_string(),
713 },
714 cargotoml_path_specifier
715 )
716 .as_bytes(),
717 )?;
718
719 for i in &opts.source_files {
722 let path_of_source_file = path.join(i.relative_path.clone());
723
724 if let Some(src_dir) = path_of_source_file.parent() {
725 paths::create_dir_all(src_dir)?;
726 }
727
728 let default_file_content: &[u8] = if i.bin {
729 b"\
730fn main() {
731 println!(\"Hello, world!\");
732}
733"
734 } else {
735 b"\
736#[cfg(test)]
737mod tests {
738 #[test]
739 fn it_works() {
740 assert_eq!(2 + 2, 4);
741 }
742}
743"
744 };
745
746 if !fs::metadata(&path_of_source_file)
747 .map(|x| x.is_file())
748 .unwrap_or(false)
749 {
750 paths::write(&path_of_source_file, default_file_content)?;
751
752 match Command::new("rustfmt").arg(&path_of_source_file).output() {
754 Err(e) => log::warn!("failed to call rustfmt: {}", e),
755 Ok(output) => {
756 if !output.status.success() {
757 log::warn!("rustfmt failed: {:?}", from_utf8(&output.stdout));
758 }
759 }
760 };
761 }
762 }
763
764 if let Err(e) = Workspace::new(&path.join("Cargo.toml"), config) {
765 let msg = format!(
766 "compiling this new crate may not work due to invalid \
767 workspace configuration\n\n{:?}",
768 e,
769 );
770 config.shell().warn(msg)?;
771 }
772
773 Ok(())
774}
775
776fn get_environment_variable(variables: &[&str]) -> Option<String> {
777 variables.iter().filter_map(|var| env::var(var).ok()).next()
778}
779
780fn discover_author() -> CargoResult<(String, Option<String>)> {
781 let git_config = find_git_config();
782 let git_config = git_config.as_ref();
783
784 let name_variables = [
785 "CARGO_NAME",
786 "GIT_AUTHOR_NAME",
787 "GIT_COMMITTER_NAME",
788 "USER",
789 "USERNAME",
790 "NAME",
791 ];
792 let name = get_environment_variable(&name_variables[0..3])
793 .or_else(|| git_config.and_then(|g| g.get_string("user.name").ok()))
794 .or_else(|| get_environment_variable(&name_variables[3..]));
795
796 let name = match name {
797 Some(name) => name,
798 None => {
799 let username_var = if cfg!(windows) { "USERNAME" } else { "USER" };
800 anyhow::bail!(
801 "could not determine the current user, please set ${}",
802 username_var
803 )
804 }
805 };
806 let email_variables = [
807 "CARGO_EMAIL",
808 "GIT_AUTHOR_EMAIL",
809 "GIT_COMMITTER_EMAIL",
810 "EMAIL",
811 ];
812 let email = get_environment_variable(&email_variables[0..3])
813 .or_else(|| git_config.and_then(|g| g.get_string("user.email").ok()))
814 .or_else(|| get_environment_variable(&email_variables[3..]));
815
816 let name = name.trim().to_string();
817 let email = email.map(|s| {
818 let mut s = s.trim();
819
820 if s.starts_with('<') && s.ends_with('>') {
823 s = &s[1..s.len() - 1];
824 }
825
826 s.to_string()
827 });
828
829 Ok((name, email))
830}
831
832fn find_git_config() -> Option<GitConfig> {
833 match env::var("__CARGO_TEST_ROOT") {
834 Ok(test_root) => find_tests_git_config(test_root),
835 Err(_) => find_real_git_config(),
836 }
837}
838
839fn find_tests_git_config(cargo_test_root: String) -> Option<GitConfig> {
840 let test_git_config = PathBuf::from(cargo_test_root).join(".git").join("config");
842
843 if test_git_config.exists() {
844 GitConfig::open(&test_git_config).ok()
845 } else {
846 GitConfig::open_default().ok()
847 }
848}
849
850fn find_real_git_config() -> Option<GitConfig> {
851 match env::current_dir() {
852 Ok(cwd) => GitRepository::discover(cwd)
853 .and_then(|repo| repo.config())
854 .or_else(|_| GitConfig::open_default())
855 .ok(),
856 Err(_) => GitConfig::open_default().ok(),
857 }
858}