#
cargo/ops/
cargo_new.rs

    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    /// Absolute path to the directory for the new package
    59    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    #[expect(dead_code, reason = "deprecated")]
    137    name: Option<String>,
    138
    139    #[deprecated = "cargo-new no longer supports adding the authors field"]
    140    #[expect(dead_code, reason = "deprecated")]
    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
    167/// See also `util::toml::embedded::sanitize_name`
    168fn check_name(
    169    name: &str,
    170    show_name_help: bool,
    171    has_bin: bool,
    172    shell: &mut Shell,
    173) -> CargoResult<()> {
    174    // If --name is already used to override, no point in suggesting it
    175    // again as a fix.
    176    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        // Only suggest `bin.name` for valid crate names because it is used for `--crate`
    185        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
    278// Taken from <https://github.com/rust-lang/rust/blob/693f365667a97b24cb40173bc2801eb66ea53020/compiler/rustc_session/src/output.rs#L49-L79>
    279fn 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
    295/// Checks if the path contains any invalid PATH env characters.
    296fn check_path(path: &Path, shell: &mut Shell) -> CargoResult<()> {
    297    // warn if the path contains characters that will break `env::join_paths`
    298    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        // path/pp does not exist or is not a file
    358        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    // Check for duplicate lib attempt
    384
    385    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    // This is here just as a random location to exercise the internal error handling.
    493    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        // we've found the only file and it's not the type user wants. Change the type and warn
    516        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        // We have found both lib and bin files and the user would like us to treat both as libs
    528        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 none exists, maybe create git, like in `cargo new`
    565
    566        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
    594/// `IgnoreList`
    595struct IgnoreList {
    596    /// git like formatted entries
    597    ignore: Vec<String>,
    598    /// mercurial formatted entries
    599    hg_ignore: Vec<String>,
    600    /// Fossil-formatted entries.
    601    fossil_ignore: Vec<String>,
    602}
    603
    604impl IgnoreList {
    605    /// constructor to build a new ignore file
    606    fn new() -> IgnoreList {
    607        IgnoreList {
    608            ignore: Vec::new(),
    609            hg_ignore: Vec::new(),
    610            fossil_ignore: Vec::new(),
    611        }
    612    }
    613
    614    /// Add a new entry to the ignore list. Requires three arguments with the
    615    /// entry in possibly three different formats. One for "git style" entries,
    616    /// one for "mercurial style" entries and one for "fossil style" entries.
    617    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    /// Return the correctly formatted content of the ignore file for the given
    624    /// version control system as `String`.
    625    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    /// `format_existing` is used to format the `IgnoreList` when the ignore file
    636    /// already exists. It reads the contents of the given `BufRead` and
    637    /// checks if the contents of the ignore list are already existing in the
    638    /// file.
    639    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        // Fossil does not support `#` comments.
    665        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                    // Just merge for Fossil.
    680                    continue;
    681                }
    682                out.push('#');
    683            }
    684            out.push_str(item);
    685            out.push('\n');
    686        }
    687
    688        Ok(out)
    689    }
    690}
    691
    692/// Writes the ignore file to the given directory. If the ignore file for the
    693/// given vcs system already exists, its content is read and duplicate ignore
    694/// file entries are filtered out.
    695fn write_ignore_file(base_path: &Path, list: &IgnoreList, vcs: VersionControl) -> CargoResult<()> {
    696    // Fossil only supports project-level settings in a dedicated subdirectory.
    697    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        // Fossil has a cleaning functionality configured in a separate file.
    706        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
    726/// Initializes the correct VCS system based on the provided config.
    727fn init_vcs(path: &Path, vcs: VersionControl, gctx: &GlobalContext) -> CargoResult<()> {
    728    match vcs {
    729        VersionControl::Git => {
    730            if !path.join(".git").exists() {
    731                // Temporary fix to work around bug in libgit2 when creating a
    732                // directory in the root of a posix filesystem.
    733                // See: https://github.com/libgit2/libgit2/issues/5130
    734                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    // Using the push method with multiple arguments ensures that the entries
    767    // for all mutually-incompatible VCS in terms of syntax are in sync.
    768    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    // Create `Cargo.toml` file with necessary `[lib]` and `[[bin]]` sections, if needed.
    784    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    // Calculate what `[lib]` and `[[bin]]`s we need to append to `Cargo.toml`.
    802    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        // Sometimes the root manifest is not a valid manifest, so we only try to parse it if it is.
    827        // This should not block the creation of the new project. It is only a best effort to
    828        // inherit the workspace package keys.
    829        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            // Only try to inherit the workspace stuff if the new package can be a member of the workspace.
    833            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                // Try to inherit the workspace lints key if it exists.
    846                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                // Try to add the new package to the workspace members.
    857                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    // Create all specified source files (with respective parent directories) if they don't exist.
    882    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            // Format the newly created source file
    918            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
    943// Update the manifest with the inherited workspace package keys.
    944// If the option is not set, the key is removed from the manifest.
    945// If the option is set, keep the value from the manifest.
    946fn 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    // Inherit keys from the workspace.
    967    // Only keep the value from the manifest if the option is set.
    968    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
    980/// Adds the new package member to the [workspace.members] array.
    981/// - It first checks if the name matches any element in [workspace.exclude],
    982///  and it ignores the name if there is a match.
    983/// - Then it check if the name matches any element already in [workspace.members],
    984/// and it ignores the name if there is a match.
    985/// - If [workspace.members] doesn't exist in the manifest, it will add a new section
    986/// with the new package in it.
    987fn 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 the members element already exist, check if one of the patterns
    997    // in the array already includes the new package's relative path.
    998    // - Add the relative path if the members don't match the new package's path.
    999    // - Create a new members array if there are no members element in the workspace yet.
    1000    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    // Find the relative path for the package from the workspace root directory.
    1037    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
    1062// Check if the package can be a member of the workspace.
    1063fn 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}