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