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