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