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!("`cargo init` cannot be run on existing Cargo packages")
517    }
518    check_path(path, &mut gctx.shell())?;
519
520    let has_bin = kind.is_bin();
521
522    if src_paths_types.is_empty() {
523        src_paths_types.push(plan_new_source_file(has_bin));
524    } else if src_paths_types.len() == 1 && !src_paths_types.iter().any(|x| x.bin == has_bin) {
525        // we've found the only file and it's not the type user wants. Change the type and warn
526        let file_type = if src_paths_types[0].bin {
527            NewProjectKind::Bin
528        } else {
529            NewProjectKind::Lib
530        };
531        gctx.shell().warn(format!(
532            "file `{}` seems to be a {} file",
533            src_paths_types[0].relative_path, file_type
534        ))?;
535        src_paths_types[0].bin = has_bin
536    } else if src_paths_types.len() > 1 && !has_bin {
537        // We have found both lib and bin files and the user would like us to treat both as libs
538        anyhow::bail!(
539            "cannot have a package with \
540             multiple libraries, \
541             found both `{}` and `{}`",
542            src_paths_types[0].relative_path,
543            src_paths_types[1].relative_path
544        )
545    }
546
547    check_name(name, opts.name.is_none(), has_bin, &mut gctx.shell())?;
548
549    let mut version_control = opts.version_control;
550
551    if version_control == None {
552        let mut num_detected_vcses = 0;
553
554        if path.join(".git").exists() {
555            version_control = Some(VersionControl::Git);
556            num_detected_vcses += 1;
557        }
558
559        if path.join(".hg").exists() {
560            version_control = Some(VersionControl::Hg);
561            num_detected_vcses += 1;
562        }
563
564        if path.join(".pijul").exists() {
565            version_control = Some(VersionControl::Pijul);
566            num_detected_vcses += 1;
567        }
568
569        if path.join(".fossil").exists() {
570            version_control = Some(VersionControl::Fossil);
571            num_detected_vcses += 1;
572        }
573
574        // if none exists, maybe create git, like in `cargo new`
575
576        if num_detected_vcses > 1 {
577            anyhow::bail!(
578                "more than one of .hg, .git, .pijul, .fossil configurations \
579                 found and the ignore file can't be filled in as \
580                 a result. specify --vcs to override detection"
581            );
582        }
583    }
584
585    let mkopts = MkOptions {
586        version_control,
587        path,
588        name,
589        source_files: src_paths_types,
590        edition: opts.edition.as_deref(),
591        registry: opts.registry.as_deref(),
592    };
593
594    mk(gctx, &mkopts).with_context(|| {
595        format!(
596            "Failed to create package `{}` at `{}`",
597            name,
598            path.display()
599        )
600    })?;
601    Ok(kind)
602}
603
604/// `IgnoreList`
605struct IgnoreList {
606    /// git like formatted entries
607    ignore: Vec<String>,
608    /// mercurial formatted entries
609    hg_ignore: Vec<String>,
610    /// Fossil-formatted entries.
611    fossil_ignore: Vec<String>,
612}
613
614impl IgnoreList {
615    /// constructor to build a new ignore file
616    fn new() -> IgnoreList {
617        IgnoreList {
618            ignore: Vec::new(),
619            hg_ignore: Vec::new(),
620            fossil_ignore: Vec::new(),
621        }
622    }
623
624    /// Add a new entry to the ignore list. Requires three arguments with the
625    /// entry in possibly three different formats. One for "git style" entries,
626    /// one for "mercurial style" entries and one for "fossil style" entries.
627    fn push(&mut self, ignore: &str, hg_ignore: &str, fossil_ignore: &str) {
628        self.ignore.push(ignore.to_string());
629        self.hg_ignore.push(hg_ignore.to_string());
630        self.fossil_ignore.push(fossil_ignore.to_string());
631    }
632
633    /// Return the correctly formatted content of the ignore file for the given
634    /// version control system as `String`.
635    fn format_new(&self, vcs: VersionControl) -> String {
636        let ignore_items = match vcs {
637            VersionControl::Hg => &self.hg_ignore,
638            VersionControl::Fossil => &self.fossil_ignore,
639            _ => &self.ignore,
640        };
641
642        ignore_items.join("\n") + "\n"
643    }
644
645    /// `format_existing` is used to format the `IgnoreList` when the ignore file
646    /// already exists. It reads the contents of the given `BufRead` and
647    /// checks if the contents of the ignore list are already existing in the
648    /// file.
649    fn format_existing<T: BufRead>(&self, existing: T, vcs: VersionControl) -> CargoResult<String> {
650        let mut existing_items = Vec::new();
651        for (i, item) in existing.lines().enumerate() {
652            match item {
653                Ok(s) => existing_items.push(s),
654                Err(err) => match err.kind() {
655                    ErrorKind::InvalidData => {
656                        return Err(anyhow!(
657                            "Character at line {} is invalid. Cargo only supports UTF-8.",
658                            i
659                        ));
660                    }
661                    _ => return Err(anyhow!(err)),
662                },
663            }
664        }
665
666        let ignore_items = match vcs {
667            VersionControl::Hg => &self.hg_ignore,
668            VersionControl::Fossil => &self.fossil_ignore,
669            _ => &self.ignore,
670        };
671
672        let mut out = String::new();
673
674        // Fossil does not support `#` comments.
675        if vcs != VersionControl::Fossil {
676            out.push_str("\n\n# Added by cargo\n");
677            if ignore_items
678                .iter()
679                .any(|item| existing_items.contains(item))
680            {
681                out.push_str("#\n# already existing elements were commented out\n");
682            }
683            out.push('\n');
684        }
685
686        for item in ignore_items {
687            if existing_items.contains(item) {
688                if vcs == VersionControl::Fossil {
689                    // Just merge for Fossil.
690                    continue;
691                }
692                out.push('#');
693            }
694            out.push_str(item);
695            out.push('\n');
696        }
697
698        Ok(out)
699    }
700}
701
702/// Writes the ignore file to the given directory. If the ignore file for the
703/// given vcs system already exists, its content is read and duplicate ignore
704/// file entries are filtered out.
705fn write_ignore_file(base_path: &Path, list: &IgnoreList, vcs: VersionControl) -> CargoResult<()> {
706    // Fossil only supports project-level settings in a dedicated subdirectory.
707    if vcs == VersionControl::Fossil {
708        paths::create_dir_all(base_path.join(".fossil-settings"))?;
709    }
710
711    for fp_ignore in match vcs {
712        VersionControl::Git => vec![base_path.join(".gitignore")],
713        VersionControl::Hg => vec![base_path.join(".hgignore")],
714        VersionControl::Pijul => vec![base_path.join(".ignore")],
715        // Fossil has a cleaning functionality configured in a separate file.
716        VersionControl::Fossil => vec![
717            base_path.join(".fossil-settings/ignore-glob"),
718            base_path.join(".fossil-settings/clean-glob"),
719        ],
720        VersionControl::NoVcs => return Ok(()),
721    } {
722        let ignore: String = match paths::open(&fp_ignore) {
723            Err(err) => match err.downcast_ref::<std::io::Error>() {
724                Some(io_err) if io_err.kind() == ErrorKind::NotFound => list.format_new(vcs),
725                _ => return Err(err),
726            },
727            Ok(file) => list.format_existing(BufReader::new(file), vcs)?,
728        };
729
730        paths::append(&fp_ignore, ignore.as_bytes())?;
731    }
732
733    Ok(())
734}
735
736/// Initializes the correct VCS system based on the provided config.
737fn init_vcs(path: &Path, vcs: VersionControl, gctx: &GlobalContext) -> CargoResult<()> {
738    match vcs {
739        VersionControl::Git => {
740            if !path.join(".git").exists() {
741                // Temporary fix to work around bug in libgit2 when creating a
742                // directory in the root of a posix filesystem.
743                // See: https://github.com/libgit2/libgit2/issues/5130
744                paths::create_dir_all(path)?;
745                GitRepo::init(path, gctx.cwd())?;
746            }
747        }
748        VersionControl::Hg => {
749            if !path.join(".hg").exists() {
750                HgRepo::init(path, gctx.cwd())?;
751            }
752        }
753        VersionControl::Pijul => {
754            if !path.join(".pijul").exists() {
755                PijulRepo::init(path, gctx.cwd())?;
756            }
757        }
758        VersionControl::Fossil => {
759            if !path.join(".fossil").exists() {
760                FossilRepo::init(path, gctx.cwd())?;
761            }
762        }
763        VersionControl::NoVcs => {
764            paths::create_dir_all(path)?;
765        }
766    };
767
768    Ok(())
769}
770
771fn mk(gctx: &GlobalContext, opts: &MkOptions<'_>) -> CargoResult<()> {
772    let path = opts.path;
773    let name = opts.name;
774    let cfg = gctx.get::<CargoNewConfig>("cargo-new")?;
775
776    // Using the push method with multiple arguments ensures that the entries
777    // for all mutually-incompatible VCS in terms of syntax are in sync.
778    let mut ignore = IgnoreList::new();
779    ignore.push("/target", "^target$", "target");
780
781    let vcs = opts.version_control.unwrap_or_else(|| {
782        let in_existing_vcs = existing_vcs_repo(path.parent().unwrap_or(path), gctx.cwd());
783        match (cfg.version_control, in_existing_vcs) {
784            (None, false) => VersionControl::Git,
785            (Some(opt), false) => opt,
786            (_, true) => VersionControl::NoVcs,
787        }
788    });
789
790    init_vcs(path, vcs, gctx)?;
791    write_ignore_file(path, &ignore, vcs)?;
792
793    // Create `Cargo.toml` file with necessary `[lib]` and `[[bin]]` sections, if needed.
794    let mut manifest = toml_edit::DocumentMut::new();
795    manifest["package"] = toml_edit::Item::Table(toml_edit::Table::new());
796    manifest["package"]["name"] = toml_edit::value(name);
797    manifest["package"]["version"] = toml_edit::value("0.1.0");
798    let edition = match opts.edition {
799        Some(edition) => edition.to_string(),
800        None => Edition::LATEST_STABLE.to_string(),
801    };
802    manifest["package"]["edition"] = toml_edit::value(edition);
803    if let Some(registry) = opts.registry {
804        let mut array = toml_edit::Array::default();
805        array.push(registry);
806        manifest["package"]["publish"] = toml_edit::value(array);
807    }
808    let dep_table = toml_edit::Table::default();
809    manifest["dependencies"] = toml_edit::Item::Table(dep_table);
810
811    // Calculate what `[lib]` and `[[bin]]`s we need to append to `Cargo.toml`.
812    for i in &opts.source_files {
813        if i.bin {
814            if i.relative_path != "src/main.rs" {
815                let mut bin = toml_edit::Table::new();
816                bin["name"] = toml_edit::value(name);
817                bin["path"] = toml_edit::value(i.relative_path.clone());
818                manifest["bin"]
819                    .or_insert(toml_edit::Item::ArrayOfTables(
820                        toml_edit::ArrayOfTables::new(),
821                    ))
822                    .as_array_of_tables_mut()
823                    .expect("bin is an array of tables")
824                    .push(bin);
825            }
826        } else if i.relative_path != "src/lib.rs" {
827            let mut lib = toml_edit::Table::new();
828            lib["path"] = toml_edit::value(i.relative_path.clone());
829            manifest["lib"] = toml_edit::Item::Table(lib);
830        }
831    }
832
833    let manifest_path = paths::normalize_path(&path.join("Cargo.toml"));
834    if let Ok(root_manifest_path) = find_root_manifest_for_wd(&manifest_path) {
835        let root_manifest = paths::read(&root_manifest_path)?;
836        // Sometimes the root manifest is not a valid manifest, so we only try to parse it if it is.
837        // This should not block the creation of the new project. It is only a best effort to
838        // inherit the workspace package keys.
839        if let Ok(mut workspace_document) = root_manifest.parse::<toml_edit::DocumentMut>() {
840            let display_path = get_display_path(&root_manifest_path, &path)?;
841            let can_be_a_member = can_be_workspace_member(&display_path, &workspace_document)?;
842            // Only try to inherit the workspace stuff if the new package can be a member of the workspace.
843            if can_be_a_member {
844                if let Some(workspace_package_keys) = workspace_document
845                    .get("workspace")
846                    .and_then(|workspace| workspace.get("package"))
847                    .and_then(|package| package.as_table())
848                {
849                    update_manifest_with_inherited_workspace_package_keys(
850                        opts,
851                        &mut manifest,
852                        workspace_package_keys,
853                    )
854                }
855                // Try to inherit the workspace lints key if it exists.
856                if workspace_document
857                    .get("workspace")
858                    .and_then(|workspace| workspace.get("lints"))
859                    .is_some()
860                {
861                    let mut table = toml_edit::Table::new();
862                    table["workspace"] = toml_edit::value(true);
863                    manifest["lints"] = toml_edit::Item::Table(table);
864                }
865
866                // Try to add the new package to the workspace members.
867                if update_manifest_with_new_member(
868                    &root_manifest_path,
869                    &mut workspace_document,
870                    &display_path,
871                )? {
872                    gctx.shell().status(
873                        "Adding",
874                        format!(
875                            "`{}` as member of workspace at `{}`",
876                            PathBuf::from(&display_path)
877                                .file_name()
878                                .unwrap()
879                                .to_str()
880                                .unwrap(),
881                            root_manifest_path.parent().unwrap().display()
882                        ),
883                    )?
884                }
885            }
886        }
887    }
888
889    paths::write(&manifest_path, manifest.to_string())?;
890
891    // Create all specified source files (with respective parent directories) if they don't exist.
892    for i in &opts.source_files {
893        let path_of_source_file = path.join(i.relative_path.clone());
894
895        if let Some(src_dir) = path_of_source_file.parent() {
896            paths::create_dir_all(src_dir)?;
897        }
898
899        let default_file_content: &[u8] = if i.bin {
900            b"\
901fn main() {
902    println!(\"Hello, world!\");
903}
904"
905        } else {
906            b"\
907pub fn add(left: u64, right: u64) -> u64 {
908    left + right
909}
910
911#[cfg(test)]
912mod tests {
913    use super::*;
914
915    #[test]
916    fn it_works() {
917        let result = add(2, 2);
918        assert_eq!(result, 4);
919    }
920}
921"
922        };
923
924        if !path_of_source_file.is_file() {
925            paths::write(&path_of_source_file, default_file_content)?;
926
927            // Format the newly created source file
928            if let Err(e) = cargo_util::ProcessBuilder::new("rustfmt")
929                .arg(&path_of_source_file)
930                .exec_with_output()
931            {
932                tracing::warn!("failed to call rustfmt: {:#}", e);
933            }
934        }
935    }
936
937    if let Err(e) = Workspace::new(&manifest_path, gctx) {
938        crate::display_warning_with_error(
939            "compiling this new package may not work due to invalid \
940             workspace configuration",
941            &e,
942            &mut gctx.shell(),
943        );
944    }
945
946    gctx.shell().note(
947        "see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html",
948    )?;
949
950    Ok(())
951}
952
953// Update the manifest with the inherited workspace package keys.
954// If the option is not set, the key is removed from the manifest.
955// If the option is set, keep the value from the manifest.
956fn update_manifest_with_inherited_workspace_package_keys(
957    opts: &MkOptions<'_>,
958    manifest: &mut toml_edit::DocumentMut,
959    workspace_package_keys: &toml_edit::Table,
960) {
961    if workspace_package_keys.is_empty() {
962        return;
963    }
964
965    let try_remove_and_inherit_package_key = |key: &str, manifest: &mut toml_edit::DocumentMut| {
966        let package = manifest["package"]
967            .as_table_mut()
968            .expect("package is a table");
969        package.remove(key);
970        let mut table = toml_edit::Table::new();
971        table.set_dotted(true);
972        table["workspace"] = toml_edit::value(true);
973        package.insert(key, toml_edit::Item::Table(table));
974    };
975
976    // Inherit keys from the workspace.
977    // Only keep the value from the manifest if the option is set.
978    for (key, _) in workspace_package_keys {
979        if key == "edition" && opts.edition.is_some() {
980            continue;
981        }
982        if key == "publish" && opts.registry.is_some() {
983            continue;
984        }
985
986        try_remove_and_inherit_package_key(key, manifest);
987    }
988}
989
990/// Adds the new package member to the [workspace.members] array.
991/// - It first checks if the name matches any element in [workspace.exclude],
992///  and it ignores the name if there is a match.
993/// - Then it check if the name matches any element already in [workspace.members],
994/// and it ignores the name if there is a match.
995/// - If [workspace.members] doesn't exist in the manifest, it will add a new section
996/// with the new package in it.
997fn update_manifest_with_new_member(
998    root_manifest_path: &Path,
999    workspace_document: &mut toml_edit::DocumentMut,
1000    display_path: &str,
1001) -> CargoResult<bool> {
1002    let Some(workspace) = workspace_document.get_mut("workspace") else {
1003        return Ok(false);
1004    };
1005
1006    // If the members element already exist, check if one of the patterns
1007    // in the array already includes the new package's relative path.
1008    // - Add the relative path if the members don't match the new package's path.
1009    // - Create a new members array if there are no members element in the workspace yet.
1010    if let Some(members) = workspace
1011        .get_mut("members")
1012        .and_then(|members| members.as_array_mut())
1013    {
1014        for member in members.iter() {
1015            let pat = member
1016                .as_str()
1017                .with_context(|| format!("invalid non-string member `{}`", member))?;
1018            let pattern = glob::Pattern::new(pat)
1019                .with_context(|| format!("cannot build glob pattern from `{}`", pat))?;
1020
1021            if pattern.matches(&display_path) {
1022                return Ok(false);
1023            }
1024        }
1025
1026        let was_sorted = members.iter().map(Value::as_str).is_sorted();
1027        members.push(display_path);
1028        if was_sorted {
1029            members.sort_by(|lhs, rhs| lhs.as_str().cmp(&rhs.as_str()));
1030        }
1031    } else {
1032        let mut array = Array::new();
1033        array.push(display_path);
1034
1035        workspace["members"] = toml_edit::value(array);
1036    }
1037
1038    write_atomic(
1039        &root_manifest_path,
1040        workspace_document.to_string().to_string().as_bytes(),
1041    )?;
1042    Ok(true)
1043}
1044
1045fn get_display_path(root_manifest_path: &Path, package_path: &Path) -> CargoResult<String> {
1046    // Find the relative path for the package from the workspace root directory.
1047    let workspace_root = root_manifest_path.parent().with_context(|| {
1048        format!(
1049            "workspace root manifest doesn't have a parent directory `{}`",
1050            root_manifest_path.display()
1051        )
1052    })?;
1053    let relpath = pathdiff::diff_paths(package_path, workspace_root).with_context(|| {
1054        format!(
1055            "path comparison requires two absolute paths; package_path: `{}`, workspace_path: `{}`",
1056            package_path.display(),
1057            workspace_root.display()
1058        )
1059    })?;
1060
1061    let mut components = Vec::new();
1062    for comp in relpath.iter() {
1063        let comp = comp.to_str().with_context(|| {
1064            format!("invalid unicode component in path `{}`", relpath.display())
1065        })?;
1066        components.push(comp);
1067    }
1068    let display_path = components.join("/");
1069    Ok(display_path)
1070}
1071
1072// Check if the package can be a member of the workspace.
1073fn can_be_workspace_member(
1074    display_path: &str,
1075    workspace_document: &toml_edit::DocumentMut,
1076) -> CargoResult<bool> {
1077    if let Some(exclude) = workspace_document
1078        .get("workspace")
1079        .and_then(|workspace| workspace.get("exclude"))
1080        .and_then(|exclude| exclude.as_array())
1081    {
1082        for member in exclude {
1083            let pat = member
1084                .as_str()
1085                .with_context(|| format!("invalid non-string exclude path `{}`", member))?;
1086            if pat == display_path {
1087                return Ok(false);
1088            }
1089        }
1090    }
1091    Ok(true)
1092}