Skip to main content

cargo/ops/
cargo_new.rs

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