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 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
118fn check_name(
120 name: &str,
121 show_name_help: bool,
122 has_bin: bool,
123 shell: &mut Shell,
124) -> CargoResult<()> {
125 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 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
229fn 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
246fn check_path(path: &Path, shell: &mut Shell) -> CargoResult<()> {
248 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 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 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 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 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 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 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
557struct IgnoreList {
559 ignore: Vec<String>,
561 hg_ignore: Vec<String>,
563 fossil_ignore: Vec<String>,
565}
566
567impl IgnoreList {
568 fn new() -> IgnoreList {
570 IgnoreList {
571 ignore: Vec::new(),
572 hg_ignore: Vec::new(),
573 fossil_ignore: Vec::new(),
574 }
575 }
576
577 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 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 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 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 continue;
644 }
645 out.push('#');
646 }
647 out.push_str(item);
648 out.push('\n');
649 }
650
651 Ok(out)
652 }
653}
654
655fn write_ignore_file(base_path: &Path, list: &IgnoreList, vcs: VersionControl) -> CargoResult<()> {
659 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 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
689fn init_vcs(path: &Path, vcs: VersionControl, gctx: &GlobalContext) -> CargoResult<()> {
691 match vcs {
692 VersionControl::Git => {
693 if !path.join(".git").exists() {
694 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 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 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 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 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 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 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 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 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 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
906fn 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 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
943fn 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 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 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
1025fn 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}