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