Skip to main content

cargo/ops/
cargo_new.rs

1use crate::core::{Edition, Shell, Workspace};
2use crate::util::errors::CargoResult;
3use crate::util::important_paths::find_root_manifest_for_wd;
4use crate::util::{FossilRepo, GitRepo, HgRepo, PijulRepo, existing_vcs_repo};
5use crate::util::{GlobalContext, restricted_names};
6use anyhow::{Context as _, anyhow};
7use cargo_util::paths::{self, write_atomic};
8use cargo_util_schemas::manifest::PackageName;
9use home::home_dir;
10use serde::Deserialize;
11use serde::de;
12use std::collections::BTreeMap;
13use std::ffi::OsStr;
14use std::io::{BufRead, BufReader, ErrorKind};
15use std::path::{Path, PathBuf};
16use std::str::FromStr;
17use std::{fmt, slice};
18use toml_edit::{Array, Value};
19
20#[derive(Clone, Copy, Debug, PartialEq)]
21pub enum VersionControl {
22    Git,
23    Hg,
24    Pijul,
25    Fossil,
26    NoVcs,
27}
28
29impl FromStr for VersionControl {
30    type Err = anyhow::Error;
31
32    fn from_str(s: &str) -> Result<Self, anyhow::Error> {
33        match s {
34            "git" => Ok(VersionControl::Git),
35            "hg" => Ok(VersionControl::Hg),
36            "pijul" => Ok(VersionControl::Pijul),
37            "fossil" => Ok(VersionControl::Fossil),
38            "none" => Ok(VersionControl::NoVcs),
39            other => anyhow::bail!("unknown vcs specification: `{}`", other),
40        }
41    }
42}
43
44impl<'de> de::Deserialize<'de> for VersionControl {
45    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
46    where
47        D: de::Deserializer<'de>,
48    {
49        let s = String::deserialize(deserializer)?;
50        FromStr::from_str(&s).map_err(de::Error::custom)
51    }
52}
53
54#[derive(Debug)]
55pub struct NewOptions {
56    pub version_control: Option<VersionControl>,
57    pub kind: NewProjectKind,
58    pub auto_detect_kind: bool,
59    /// Absolute path to the directory for the new package
60    pub path: PathBuf,
61    pub name: Option<String>,
62    pub edition: Option<String>,
63    pub registry: Option<String>,
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub enum NewProjectKind {
68    Bin,
69    Lib,
70}
71
72impl NewProjectKind {
73    fn is_bin(self) -> bool {
74        self == NewProjectKind::Bin
75    }
76}
77
78impl fmt::Display for NewProjectKind {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match *self {
81            NewProjectKind::Bin => "binary (application)",
82            NewProjectKind::Lib => "library",
83        }
84        .fmt(f)
85    }
86}
87
88struct SourceFileInformation {
89    relative_path: String,
90    bin: bool,
91}
92
93struct MkOptions<'a> {
94    version_control: Option<VersionControl>,
95    path: &'a Path,
96    name: &'a str,
97    source_files: Vec<SourceFileInformation>,
98    edition: Option<&'a str>,
99    registry: Option<&'a str>,
100}
101
102impl NewOptions {
103    pub fn new(
104        version_control: Option<VersionControl>,
105        bin: bool,
106        lib: bool,
107        path: PathBuf,
108        name: Option<String>,
109        edition: Option<String>,
110        registry: Option<String>,
111    ) -> CargoResult<NewOptions> {
112        let auto_detect_kind = !bin && !lib;
113
114        let kind = match (bin, lib) {
115            (true, true) => anyhow::bail!("can't specify both lib and binary outputs"),
116            (false, true) => NewProjectKind::Lib,
117            (_, false) => NewProjectKind::Bin,
118        };
119
120        let opts = NewOptions {
121            version_control,
122            kind,
123            auto_detect_kind,
124            path,
125            name,
126            edition,
127            registry,
128        };
129        Ok(opts)
130    }
131}
132
133#[derive(Deserialize)]
134#[serde(rename_all = "kebab-case")]
135struct CargoNewConfig {
136    #[deprecated = "cargo-new no longer supports adding the authors field"]
137    #[expect(dead_code, reason = "deprecated")]
138    name: Option<String>,
139
140    #[deprecated = "cargo-new no longer supports adding the authors field"]
141    #[expect(dead_code, reason = "deprecated")]
142    email: Option<String>,
143
144    #[serde(rename = "vcs")]
145    version_control: Option<VersionControl>,
146}
147
148fn get_name<'a>(path: &'a Path, opts: &'a NewOptions) -> CargoResult<&'a str> {
149    if let Some(ref name) = opts.name {
150        return Ok(name);
151    }
152
153    let file_name = path.file_name().ok_or_else(|| {
154        anyhow::format_err!(
155            "cannot auto-detect package name from path {:?} ; use --name to override",
156            path.as_os_str()
157        )
158    })?;
159
160    file_name.to_str().ok_or_else(|| {
161        anyhow::format_err!(
162            "cannot create package with a non-unicode name: {:?}",
163            file_name
164        )
165    })
166}
167
168/// See also `util::toml::embedded::sanitize_name`
169fn check_name(
170    name: &str,
171    show_name_help: bool,
172    has_bin: bool,
173    shell: &mut Shell,
174) -> CargoResult<()> {
175    // If --name is already used to override, no point in suggesting it
176    // again as a fix.
177    let name_help = if show_name_help {
178        "\nnote: the directory name is used as the package name\
179        \nhelp: to override the package name, pass `--name <pkgname>`"
180    } else {
181        ""
182    };
183    let bin_help = || {
184        let mut help = String::from(name_help);
185        // Only suggest `bin.name` for valid crate names because it is used for `--crate`
186        if has_bin && validate_crate_name(name) {
187            help.push_str(&format!(
188                "\n\
189                help: to name the binary \"{name}\", use a valid package \
190                name, and set the binary name to be different from the package. \
191                This can be done by setting the binary filename to `src/bin/{name}.rs` \
192                or change the name in Cargo.toml with:\n\
193                \n    \
194                [[bin]]\n    \
195                name = \"{name}\"\n    \
196                path = \"src/main.rs\"\n\
197            ",
198                name = name
199            ));
200        }
201        help
202    };
203    PackageName::new(name).map_err(|err| {
204        let help = bin_help();
205        anyhow::anyhow!("{err}{help}")
206    })?;
207
208    if restricted_names::is_keyword(name) {
209        anyhow::bail!(
210            "invalid package name `{}`: it is a Rust keyword{}",
211            name,
212            bin_help()
213        );
214    }
215    if restricted_names::is_conflicting_artifact_name(name) {
216        if has_bin {
217            anyhow::bail!(
218                "invalid package name `{}`: \
219                it conflicts with cargo's build directory names{}",
220                name,
221                name_help
222            );
223        } else {
224            shell.warn(format!(
225                "package `{}` will not support binary \
226                executables with that name, \
227                it conflicts with cargo's build directory names",
228                name
229            ))?;
230        }
231    }
232    if name == "test" {
233        anyhow::bail!(
234            "invalid package name `test`: \
235            it conflicts with Rust's built-in test library{}",
236            bin_help()
237        );
238    }
239    if ["core", "std", "alloc", "proc_macro", "proc-macro"].contains(&name) {
240        shell.warn(format!(
241            "package name `{}` may be confused with the package with that name in Rust's standard library\n\
242            It is recommended to use a different name to avoid problems.{}",
243            name,
244            bin_help()
245        ))?;
246    }
247    if restricted_names::is_windows_reserved(name) {
248        if cfg!(windows) {
249            anyhow::bail!(
250                "invalid package name `{}`: it is a reserved Windows filename{}",
251                name,
252                name_help
253            );
254        } else {
255            shell.warn(format!(
256                "package name `{}` is a reserved Windows filename\n\
257                This package will not work on Windows platforms.",
258                name
259            ))?;
260        }
261    }
262    if restricted_names::is_non_ascii_name(name) {
263        shell.warn(format!(
264            "invalid package name `{}`: contains non-ASCII characters\n\
265            Non-ASCII crate names are not supported by Rust.",
266            name
267        ))?;
268    }
269    let name_in_lowercase = name.to_lowercase();
270    if name != name_in_lowercase {
271        shell.warn(format!(
272            "package name `{name}` is not snake_case or kebab-case which is recommended for package names, consider `{name_in_lowercase}`"
273        ))?;
274    }
275
276    Ok(())
277}
278
279// Taken from <https://github.com/rust-lang/rust/blob/693f365667a97b24cb40173bc2801eb66ea53020/compiler/rustc_session/src/output.rs#L49-L79>
280fn validate_crate_name(name: &str) -> bool {
281    if name.is_empty() {
282        return false;
283    }
284
285    for c in name.chars() {
286        if c.is_alphanumeric() || c == '-' || c == '_' {
287            continue;
288        } else {
289            return false;
290        }
291    }
292
293    true
294}
295
296/// Checks if the path contains any invalid PATH env characters.
297fn check_path(path: &Path, shell: &mut Shell) -> CargoResult<()> {
298    // warn if the path contains characters that will break `env::join_paths`
299    if let Err(_) = paths::join_paths(slice::from_ref(&OsStr::new(path)), "") {
300        let path = path.to_string_lossy();
301        shell.warn(format!(
302            "the path `{path}` contains invalid PATH characters (usually `:`, `;`, or `\"`)\n\
303            It is recommended to use a different name to avoid problems."
304        ))?;
305    }
306    Ok(())
307}
308
309fn detect_source_paths_and_types(
310    package_path: &Path,
311    package_name: &str,
312    detected_files: &mut Vec<SourceFileInformation>,
313) -> CargoResult<()> {
314    let path = package_path;
315    let name = package_name;
316
317    enum H {
318        Bin,
319        Lib,
320        Detect,
321    }
322
323    struct Test {
324        proposed_path: String,
325        handling: H,
326    }
327
328    let tests = vec![
329        Test {
330            proposed_path: "src/main.rs".to_string(),
331            handling: H::Bin,
332        },
333        Test {
334            proposed_path: "main.rs".to_string(),
335            handling: H::Bin,
336        },
337        Test {
338            proposed_path: format!("src/{}.rs", name),
339            handling: H::Detect,
340        },
341        Test {
342            proposed_path: format!("{}.rs", name),
343            handling: H::Detect,
344        },
345        Test {
346            proposed_path: "src/lib.rs".to_string(),
347            handling: H::Lib,
348        },
349        Test {
350            proposed_path: "lib.rs".to_string(),
351            handling: H::Lib,
352        },
353    ];
354
355    for i in tests {
356        let pp = i.proposed_path;
357
358        // path/pp does not exist or is not a file
359        if !path.join(&pp).is_file() {
360            continue;
361        }
362
363        let sfi = match i.handling {
364            H::Bin => SourceFileInformation {
365                relative_path: pp,
366                bin: true,
367            },
368            H::Lib => SourceFileInformation {
369                relative_path: pp,
370                bin: false,
371            },
372            H::Detect => {
373                let content = paths::read(&path.join(pp.clone()))?;
374                let isbin = content.contains("fn main");
375                SourceFileInformation {
376                    relative_path: pp,
377                    bin: isbin,
378                }
379            }
380        };
381        detected_files.push(sfi);
382    }
383
384    // Check for duplicate lib attempt
385
386    let mut previous_lib_relpath: Option<&str> = None;
387    let mut duplicates_checker: BTreeMap<&str, &SourceFileInformation> = BTreeMap::new();
388
389    for i in detected_files {
390        if i.bin {
391            if let Some(x) = BTreeMap::get::<str>(&duplicates_checker, &name) {
392                anyhow::bail!(
393                    "\
394multiple possible binary sources found:
395  {}
396  {}
397cannot automatically generate Cargo.toml as the main target would be ambiguous",
398                    &x.relative_path,
399                    &i.relative_path
400                );
401            }
402            duplicates_checker.insert(name, i);
403        } else {
404            if let Some(plp) = previous_lib_relpath {
405                anyhow::bail!(
406                    "cannot have a package with \
407                     multiple libraries, \
408                     found both `{}` and `{}`",
409                    plp,
410                    i.relative_path
411                )
412            }
413            previous_lib_relpath = Some(&i.relative_path);
414        }
415    }
416
417    Ok(())
418}
419
420fn plan_new_source_file(bin: bool) -> SourceFileInformation {
421    if bin {
422        SourceFileInformation {
423            relative_path: "src/main.rs".to_string(),
424            bin: true,
425        }
426    } else {
427        SourceFileInformation {
428            relative_path: "src/lib.rs".to_string(),
429            bin: false,
430        }
431    }
432}
433
434fn calculate_new_project_kind(
435    requested_kind: NewProjectKind,
436    auto_detect_kind: bool,
437    found_files: &Vec<SourceFileInformation>,
438) -> NewProjectKind {
439    let bin_file = found_files.iter().find(|x| x.bin);
440
441    let kind_from_files = if !found_files.is_empty() && bin_file.is_none() {
442        NewProjectKind::Lib
443    } else {
444        NewProjectKind::Bin
445    };
446
447    if auto_detect_kind {
448        return kind_from_files;
449    }
450
451    requested_kind
452}
453
454pub fn new(opts: &NewOptions, gctx: &GlobalContext) -> CargoResult<()> {
455    let path = &opts.path;
456    let name = get_name(path, opts)?;
457    gctx.shell()
458        .status("Creating", format!("{} `{}` package", opts.kind, name))?;
459
460    if path.exists() {
461        anyhow::bail!(
462            "destination `{}` already exists\n\n\
463             Use `cargo init` to initialize the directory",
464            path.display()
465        )
466    }
467    check_path(path, &mut gctx.shell())?;
468
469    let is_bin = opts.kind.is_bin();
470
471    check_name(name, opts.name.is_none(), is_bin, &mut gctx.shell())?;
472
473    let mkopts = MkOptions {
474        version_control: opts.version_control,
475        path,
476        name,
477        source_files: vec![plan_new_source_file(opts.kind.is_bin())],
478        edition: opts.edition.as_deref(),
479        registry: opts.registry.as_deref(),
480    };
481
482    mk(gctx, &mkopts).with_context(|| {
483        format!(
484            "failed to create package `{}` at `{}`",
485            name,
486            path.display()
487        )
488    })?;
489    Ok(())
490}
491
492pub fn init(opts: &NewOptions, gctx: &GlobalContext) -> CargoResult<NewProjectKind> {
493    // This is here just as a random location to exercise the internal error handling.
494    if gctx.get_env_os("__CARGO_TEST_INTERNAL_ERROR").is_some() {
495        return Err(crate::util::internal("internal error test"));
496    }
497
498    let path = &opts.path;
499
500    if let Some(home) = home_dir() {
501        if path == &home {
502            anyhow::bail!(
503                "cannot create package in the home directory\n\n\
504                 help: use `cargo init <path>` to create a package in a different directory"
505            )
506        }
507    }
508    let name = get_name(path, opts)?;
509    let mut src_paths_types = vec![];
510    detect_source_paths_and_types(path, name, &mut src_paths_types)?;
511    let kind = calculate_new_project_kind(opts.kind, opts.auto_detect_kind, &src_paths_types);
512    gctx.shell()
513        .status("Creating", format!("{} package", opts.kind))?;
514
515    if path.join("Cargo.toml").exists() {
516        anyhow::bail!(
517            "`cargo init` cannot be run on existing Cargo packages\n\
518             help: use `cargo new` to create a package in a new subdirectory"
519        )
520    }
521    check_path(path, &mut gctx.shell())?;
522
523    let has_bin = kind.is_bin();
524
525    if src_paths_types.is_empty() {
526        src_paths_types.push(plan_new_source_file(has_bin));
527    } else if src_paths_types.len() == 1 && !src_paths_types.iter().any(|x| x.bin == has_bin) {
528        // we've found the only file and it's not the type user wants. Change the type and warn
529        let file_type = if src_paths_types[0].bin {
530            NewProjectKind::Bin
531        } else {
532            NewProjectKind::Lib
533        };
534        gctx.shell().warn(format!(
535            "file `{}` seems to be a {} file",
536            src_paths_types[0].relative_path, file_type
537        ))?;
538        src_paths_types[0].bin = has_bin
539    } else if src_paths_types.len() > 1 && !has_bin {
540        // We have found both lib and bin files and the user would like us to treat both as libs
541        anyhow::bail!(
542            "cannot have a package with \
543             multiple libraries, \
544             found both `{}` and `{}`",
545            src_paths_types[0].relative_path,
546            src_paths_types[1].relative_path
547        )
548    }
549
550    check_name(name, opts.name.is_none(), has_bin, &mut gctx.shell())?;
551
552    let mut version_control = opts.version_control;
553
554    if version_control == None {
555        let mut num_detected_vcses = 0;
556
557        if path.join(".git").exists() {
558            version_control = Some(VersionControl::Git);
559            num_detected_vcses += 1;
560        }
561
562        if path.join(".hg").exists() {
563            version_control = Some(VersionControl::Hg);
564            num_detected_vcses += 1;
565        }
566
567        if path.join(".pijul").exists() {
568            version_control = Some(VersionControl::Pijul);
569            num_detected_vcses += 1;
570        }
571
572        if path.join(".fossil").exists() {
573            version_control = Some(VersionControl::Fossil);
574            num_detected_vcses += 1;
575        }
576
577        // if none exists, maybe create git, like in `cargo new`
578
579        if num_detected_vcses > 1 {
580            anyhow::bail!(
581                "more than one of .hg, .git, .pijul, .fossil configurations \
582                 found and the ignore file can't be filled in as \
583                 a result. specify --vcs to override detection"
584            );
585        }
586    }
587
588    let mkopts = MkOptions {
589        version_control,
590        path,
591        name,
592        source_files: src_paths_types,
593        edition: opts.edition.as_deref(),
594        registry: opts.registry.as_deref(),
595    };
596
597    mk(gctx, &mkopts).with_context(|| {
598        format!(
599            "failed to create package `{}` at `{}`",
600            name,
601            path.display()
602        )
603    })?;
604    Ok(kind)
605}
606
607/// `IgnoreList`
608struct IgnoreList {
609    /// git like formatted entries
610    ignore: Vec<String>,
611    /// mercurial formatted entries
612    hg_ignore: Vec<String>,
613    /// Fossil-formatted entries.
614    fossil_ignore: Vec<String>,
615}
616
617impl IgnoreList {
618    /// constructor to build a new ignore file
619    fn new() -> IgnoreList {
620        IgnoreList {
621            ignore: Vec::new(),
622            hg_ignore: Vec::new(),
623            fossil_ignore: Vec::new(),
624        }
625    }
626
627    /// Add a new entry to the ignore list. Requires three arguments with the
628    /// entry in possibly three different formats. One for "git style" entries,
629    /// one for "mercurial style" entries and one for "fossil style" entries.
630    fn push(&mut self, ignore: &str, hg_ignore: &str, fossil_ignore: &str) {
631        self.ignore.push(ignore.to_string());
632        self.hg_ignore.push(hg_ignore.to_string());
633        self.fossil_ignore.push(fossil_ignore.to_string());
634    }
635
636    /// Return the correctly formatted content of the ignore file for the given
637    /// version control system as `String`.
638    fn format_new(&self, vcs: VersionControl) -> String {
639        let ignore_items = match vcs {
640            VersionControl::Hg => &self.hg_ignore,
641            VersionControl::Fossil => &self.fossil_ignore,
642            _ => &self.ignore,
643        };
644
645        ignore_items.join("\n") + "\n"
646    }
647
648    /// `format_existing` is used to format the `IgnoreList` when the ignore file
649    /// already exists. It reads the contents of the given `BufRead` and
650    /// checks if the contents of the ignore list are already existing in the
651    /// file.
652    fn format_existing<T: BufRead>(&self, existing: T, vcs: VersionControl) -> CargoResult<String> {
653        let mut existing_items = Vec::new();
654        for (i, item) in existing.lines().enumerate() {
655            match item {
656                Ok(s) => existing_items.push(s),
657                Err(err) => match err.kind() {
658                    ErrorKind::InvalidData => {
659                        return Err(anyhow!(
660                            "Character at line {} is invalid. Cargo only supports UTF-8.",
661                            i
662                        ));
663                    }
664                    _ => return Err(anyhow!(err)),
665                },
666            }
667        }
668
669        let ignore_items = match vcs {
670            VersionControl::Hg => &self.hg_ignore,
671            VersionControl::Fossil => &self.fossil_ignore,
672            _ => &self.ignore,
673        };
674
675        let mut out = String::new();
676
677        // Fossil does not support `#` comments.
678        if vcs != VersionControl::Fossil {
679            out.push_str("\n\n# Added by cargo\n");
680            if ignore_items
681                .iter()
682                .any(|item| existing_items.contains(item))
683            {
684                out.push_str("#\n# already existing elements were commented out\n");
685            }
686            out.push('\n');
687        }
688
689        for item in ignore_items {
690            if existing_items.contains(item) {
691                if vcs == VersionControl::Fossil {
692                    // Just merge for Fossil.
693                    continue;
694                }
695                out.push('#');
696            }
697            out.push_str(item);
698            out.push('\n');
699        }
700
701        Ok(out)
702    }
703}
704
705/// Writes the ignore file to the given directory. If the ignore file for the
706/// given vcs system already exists, its content is read and duplicate ignore
707/// file entries are filtered out.
708fn write_ignore_file(base_path: &Path, list: &IgnoreList, vcs: VersionControl) -> CargoResult<()> {
709    // Fossil only supports project-level settings in a dedicated subdirectory.
710    if vcs == VersionControl::Fossil {
711        paths::create_dir_all(base_path.join(".fossil-settings"))?;
712    }
713
714    for fp_ignore in match vcs {
715        VersionControl::Git => vec![base_path.join(".gitignore")],
716        VersionControl::Hg => vec![base_path.join(".hgignore")],
717        VersionControl::Pijul => vec![base_path.join(".ignore")],
718        // Fossil has a cleaning functionality configured in a separate file.
719        VersionControl::Fossil => vec![
720            base_path.join(".fossil-settings/ignore-glob"),
721            base_path.join(".fossil-settings/clean-glob"),
722        ],
723        VersionControl::NoVcs => return Ok(()),
724    } {
725        let ignore: String = match paths::open(&fp_ignore) {
726            Err(err) => match err.downcast_ref::<std::io::Error>() {
727                Some(io_err) if io_err.kind() == ErrorKind::NotFound => list.format_new(vcs),
728                _ => return Err(err),
729            },
730            Ok(file) => list.format_existing(BufReader::new(file), vcs)?,
731        };
732
733        paths::append(&fp_ignore, ignore.as_bytes())?;
734    }
735
736    Ok(())
737}
738
739/// Initializes the correct VCS system based on the provided config.
740fn init_vcs(path: &Path, vcs: VersionControl, gctx: &GlobalContext) -> CargoResult<()> {
741    match vcs {
742        VersionControl::Git => {
743            if !path.join(".git").exists() {
744                // Temporary fix to work around bug in libgit2 when creating a
745                // directory in the root of a posix filesystem.
746                // See: https://github.com/libgit2/libgit2/issues/5130
747                paths::create_dir_all(path)?;
748                GitRepo::init(path, gctx.cwd())?;
749            }
750        }
751        VersionControl::Hg => {
752            if !path.join(".hg").exists() {
753                HgRepo::init(path, gctx.cwd())?;
754            }
755        }
756        VersionControl::Pijul => {
757            if !path.join(".pijul").exists() {
758                PijulRepo::init(path, gctx.cwd())?;
759            }
760        }
761        VersionControl::Fossil => {
762            if !path.join(".fossil").exists() {
763                FossilRepo::init(path, gctx.cwd())?;
764            }
765        }
766        VersionControl::NoVcs => {
767            paths::create_dir_all(path)?;
768        }
769    };
770
771    Ok(())
772}
773
774fn mk(gctx: &GlobalContext, opts: &MkOptions<'_>) -> CargoResult<()> {
775    let path = opts.path;
776    let name = opts.name;
777    let cfg = gctx.get::<CargoNewConfig>("cargo-new")?;
778
779    // Using the push method with multiple arguments ensures that the entries
780    // for all mutually-incompatible VCS in terms of syntax are in sync.
781    let mut ignore = IgnoreList::new();
782    ignore.push("/target", "^target$", "target");
783
784    let vcs = opts.version_control.unwrap_or_else(|| {
785        let in_existing_vcs = existing_vcs_repo(path.parent().unwrap_or(path), gctx.cwd());
786        match (cfg.version_control, in_existing_vcs) {
787            (None, false) => VersionControl::Git,
788            (Some(opt), false) => opt,
789            (_, true) => VersionControl::NoVcs,
790        }
791    });
792
793    init_vcs(path, vcs, gctx)?;
794    write_ignore_file(path, &ignore, vcs)?;
795
796    // Create `Cargo.toml` file with necessary `[lib]` and `[[bin]]` sections, if needed.
797    let mut manifest = toml_edit::DocumentMut::new();
798    manifest["package"] = toml_edit::Item::Table(toml_edit::Table::new());
799    manifest["package"]["name"] = toml_edit::value(name);
800    manifest["package"]["version"] = toml_edit::value("0.1.0");
801    let edition = match opts.edition {
802        Some(edition) => edition.to_string(),
803        None => Edition::LATEST_STABLE.to_string(),
804    };
805    manifest["package"]["edition"] = toml_edit::value(edition);
806    if let Some(registry) = opts.registry {
807        let mut array = toml_edit::Array::default();
808        array.push(registry);
809        manifest["package"]["publish"] = toml_edit::value(array);
810    }
811    let dep_table = toml_edit::Table::default();
812    manifest["dependencies"] = toml_edit::Item::Table(dep_table);
813
814    // Calculate what `[lib]` and `[[bin]]`s we need to append to `Cargo.toml`.
815    for i in &opts.source_files {
816        if i.bin {
817            if i.relative_path != "src/main.rs" {
818                let mut bin = toml_edit::Table::new();
819                bin["name"] = toml_edit::value(name);
820                bin["path"] = toml_edit::value(i.relative_path.clone());
821                manifest["bin"]
822                    .or_insert(toml_edit::Item::ArrayOfTables(
823                        toml_edit::ArrayOfTables::new(),
824                    ))
825                    .as_array_of_tables_mut()
826                    .expect("bin is an array of tables")
827                    .push(bin);
828            }
829        } else if i.relative_path != "src/lib.rs" {
830            let mut lib = toml_edit::Table::new();
831            lib["path"] = toml_edit::value(i.relative_path.clone());
832            manifest["lib"] = toml_edit::Item::Table(lib);
833        }
834    }
835
836    let manifest_path = paths::normalize_path(&path.join("Cargo.toml"));
837    if let Ok(root_manifest_path) = find_root_manifest_for_wd(&manifest_path) {
838        let root_manifest = paths::read(&root_manifest_path)?;
839        // Sometimes the root manifest is not a valid manifest, so we only try to parse it if it is.
840        // This should not block the creation of the new project. It is only a best effort to
841        // inherit the workspace package keys.
842        if let Ok(mut workspace_document) = root_manifest.parse::<toml_edit::DocumentMut>() {
843            let display_path = get_display_path(&root_manifest_path, &path)?;
844            let can_be_a_member = can_be_workspace_member(&display_path, &workspace_document)?;
845            // Only try to inherit the workspace stuff if the new package can be a member of the workspace.
846            if can_be_a_member {
847                if let Some(workspace_package_keys) = workspace_document
848                    .get("workspace")
849                    .and_then(|workspace| workspace.get("package"))
850                    .and_then(|package| package.as_table())
851                {
852                    update_manifest_with_inherited_workspace_package_keys(
853                        opts,
854                        &mut manifest,
855                        workspace_package_keys,
856                    )
857                }
858                // Try to inherit the workspace lints key if it exists.
859                if workspace_document
860                    .get("workspace")
861                    .and_then(|workspace| workspace.get("lints"))
862                    .is_some()
863                {
864                    let mut table = toml_edit::Table::new();
865                    table["workspace"] = toml_edit::value(true);
866                    manifest["lints"] = toml_edit::Item::Table(table);
867                }
868
869                // Try to add the new package to the workspace members.
870                if update_manifest_with_new_member(
871                    &root_manifest_path,
872                    &mut workspace_document,
873                    &display_path,
874                )? {
875                    gctx.shell().status(
876                        "Adding",
877                        format!(
878                            "`{}` as member of workspace at `{}`",
879                            PathBuf::from(&display_path)
880                                .file_name()
881                                .unwrap()
882                                .to_str()
883                                .unwrap(),
884                            root_manifest_path.parent().unwrap().display()
885                        ),
886                    )?
887                }
888            }
889        }
890    }
891
892    paths::write(&manifest_path, manifest.to_string())?;
893
894    // Create all specified source files (with respective parent directories) if they don't exist.
895    for i in &opts.source_files {
896        let path_of_source_file = path.join(i.relative_path.clone());
897
898        if let Some(src_dir) = path_of_source_file.parent() {
899            paths::create_dir_all(src_dir)?;
900        }
901
902        let default_file_content: &[u8] = if i.bin {
903            b"\
904fn main() {
905    println!(\"Hello, world!\");
906}
907"
908        } else {
909            b"\
910pub fn add(left: u64, right: u64) -> u64 {
911    left + right
912}
913
914#[cfg(test)]
915mod tests {
916    use super::*;
917
918    #[test]
919    fn it_works() {
920        let result = add(2, 2);
921        assert_eq!(result, 4);
922    }
923}
924"
925        };
926
927        if !path_of_source_file.is_file() {
928            paths::write(&path_of_source_file, default_file_content)?;
929
930            // Format the newly created source file
931            if let Err(e) = cargo_util::ProcessBuilder::new("rustfmt")
932                .arg(&path_of_source_file)
933                .exec_with_output()
934            {
935                tracing::warn!("failed to call rustfmt: {:#}", e);
936            }
937        }
938    }
939
940    if let Err(e) = Workspace::new(&manifest_path, gctx) {
941        crate::display_warning_with_error(
942            "compiling this new package may not work due to invalid \
943             workspace configuration",
944            &e,
945            &mut gctx.shell(),
946        );
947    }
948
949    gctx.shell().note(
950        "see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html",
951    )?;
952
953    Ok(())
954}
955
956// Update the manifest with the inherited workspace package keys.
957// If the option is not set, the key is removed from the manifest.
958// If the option is set, keep the value from the manifest.
959fn update_manifest_with_inherited_workspace_package_keys(
960    opts: &MkOptions<'_>,
961    manifest: &mut toml_edit::DocumentMut,
962    workspace_package_keys: &toml_edit::Table,
963) {
964    if workspace_package_keys.is_empty() {
965        return;
966    }
967
968    let try_remove_and_inherit_package_key = |key: &str, manifest: &mut toml_edit::DocumentMut| {
969        let package = manifest["package"]
970            .as_table_mut()
971            .expect("package is a table");
972        package.remove(key);
973        let mut table = toml_edit::Table::new();
974        table.set_dotted(true);
975        table["workspace"] = toml_edit::value(true);
976        package.insert(key, toml_edit::Item::Table(table));
977    };
978
979    // Inherit keys from the workspace.
980    // Only keep the value from the manifest if the option is set.
981    for (key, _) in workspace_package_keys {
982        if key == "edition" && opts.edition.is_some() {
983            continue;
984        }
985        if key == "publish" && opts.registry.is_some() {
986            continue;
987        }
988
989        try_remove_and_inherit_package_key(key, manifest);
990    }
991}
992
993/// Adds the new package member to the [workspace.members] array.
994/// - It first checks if the name matches any element in [workspace.exclude],
995///  and it ignores the name if there is a match.
996/// - Then it check if the name matches any element already in [workspace.members],
997/// and it ignores the name if there is a match.
998/// - If [workspace.members] doesn't exist in the manifest, it will add a new section
999/// with the new package in it.
1000fn update_manifest_with_new_member(
1001    root_manifest_path: &Path,
1002    workspace_document: &mut toml_edit::DocumentMut,
1003    display_path: &str,
1004) -> CargoResult<bool> {
1005    let Some(workspace) = workspace_document.get_mut("workspace") else {
1006        return Ok(false);
1007    };
1008
1009    // If the members element already exist, check if one of the patterns
1010    // in the array already includes the new package's relative path.
1011    // - Add the relative path if the members don't match the new package's path.
1012    // - Create a new members array if there are no members element in the workspace yet.
1013    if let Some(members) = workspace
1014        .get_mut("members")
1015        .and_then(|members| members.as_array_mut())
1016    {
1017        for member in members.iter() {
1018            let pat = member
1019                .as_str()
1020                .with_context(|| format!("invalid non-string member `{}`", member))?;
1021            let pattern = glob::Pattern::new(pat)
1022                .with_context(|| format!("cannot build glob pattern from `{}`", pat))?;
1023
1024            if pattern.matches(&display_path) {
1025                return Ok(false);
1026            }
1027        }
1028
1029        let was_sorted = members.iter().map(Value::as_str).is_sorted();
1030        members.push(display_path);
1031        if was_sorted {
1032            members.sort_by(|lhs, rhs| lhs.as_str().cmp(&rhs.as_str()));
1033        }
1034    } else {
1035        let mut array = Array::new();
1036        array.push(display_path);
1037
1038        workspace["members"] = toml_edit::value(array);
1039    }
1040
1041    write_atomic(
1042        &root_manifest_path,
1043        workspace_document.to_string().to_string().as_bytes(),
1044    )?;
1045    Ok(true)
1046}
1047
1048fn get_display_path(root_manifest_path: &Path, package_path: &Path) -> CargoResult<String> {
1049    // Find the relative path for the package from the workspace root directory.
1050    let workspace_root = root_manifest_path.parent().with_context(|| {
1051        format!(
1052            "workspace root manifest doesn't have a parent directory `{}`",
1053            root_manifest_path.display()
1054        )
1055    })?;
1056    let relpath = pathdiff::diff_paths(package_path, workspace_root).with_context(|| {
1057        format!(
1058            "path comparison requires two absolute paths; package_path: `{}`, workspace_path: `{}`",
1059            package_path.display(),
1060            workspace_root.display()
1061        )
1062    })?;
1063
1064    let mut components = Vec::new();
1065    for comp in relpath.iter() {
1066        let comp = comp.to_str().with_context(|| {
1067            format!("invalid unicode component in path `{}`", relpath.display())
1068        })?;
1069        components.push(comp);
1070    }
1071    let display_path = components.join("/");
1072    Ok(display_path)
1073}
1074
1075// Check if the package can be a member of the workspace.
1076fn can_be_workspace_member(
1077    display_path: &str,
1078    workspace_document: &toml_edit::DocumentMut,
1079) -> CargoResult<bool> {
1080    if let Some(exclude) = workspace_document
1081        .get("workspace")
1082        .and_then(|workspace| workspace.get("exclude"))
1083        .and_then(|exclude| exclude.as_array())
1084    {
1085        for member in exclude {
1086            let pat = member
1087                .as_str()
1088                .with_context(|| format!("invalid non-string exclude path `{}`", member))?;
1089            if pat == display_path {
1090                return Ok(false);
1091            }
1092        }
1093    }
1094    Ok(true)
1095}