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 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
169fn check_name(
171 name: &str,
172 show_name_help: bool,
173 has_bin: bool,
174 shell: &mut Shell,
175) -> CargoResult<()> {
176 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 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
280fn 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
297fn check_path(path: &Path, shell: &mut Shell) -> CargoResult<()> {
299 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 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 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 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 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 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 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
608struct IgnoreList {
610 ignore: Vec<String>,
612 hg_ignore: Vec<String>,
614 fossil_ignore: Vec<String>,
616}
617
618impl IgnoreList {
619 fn new() -> IgnoreList {
621 IgnoreList {
622 ignore: Vec::new(),
623 hg_ignore: Vec::new(),
624 fossil_ignore: Vec::new(),
625 }
626 }
627
628 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 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 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 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 continue;
695 }
696 out.push('#');
697 }
698 out.push_str(item);
699 out.push('\n');
700 }
701
702 Ok(out)
703 }
704}
705
706fn write_ignore_file(base_path: &Path, list: &IgnoreList, vcs: VersionControl) -> CargoResult<()> {
710 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 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
740fn init_vcs(path: &Path, vcs: VersionControl, gctx: &GlobalContext) -> CargoResult<()> {
742 match vcs {
743 VersionControl::Git => {
744 if !path.join(".git").exists() {
745 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 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 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 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 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 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 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 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 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 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
957fn 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 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
994fn 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 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 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
1076fn 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}