Skip to main content

cargo/ops/
cargo_new.rs

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