mirror of https://github.com/rust-lang/cargo
1063 lines
34 KiB
Rust
1063 lines
34 KiB
Rust
use crate::core::{Edition, Shell, Workspace};
|
|
use crate::util::errors::CargoResult;
|
|
use crate::util::important_paths::find_root_manifest_for_wd;
|
|
use crate::util::toml_mut::is_sorted;
|
|
use crate::util::{existing_vcs_repo, FossilRepo, GitRepo, HgRepo, PijulRepo};
|
|
use crate::util::{restricted_names, GlobalContext};
|
|
use anyhow::{anyhow, Context as _};
|
|
use cargo_util::paths::{self, write_atomic};
|
|
use cargo_util_schemas::manifest::PackageName;
|
|
use serde::de;
|
|
use serde::Deserialize;
|
|
use std::collections::BTreeMap;
|
|
use std::ffi::OsStr;
|
|
use std::io::{BufRead, BufReader, ErrorKind};
|
|
use std::path::{Path, PathBuf};
|
|
use std::str::FromStr;
|
|
use std::{fmt, slice};
|
|
use toml_edit::{Array, Value};
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub enum VersionControl {
|
|
Git,
|
|
Hg,
|
|
Pijul,
|
|
Fossil,
|
|
NoVcs,
|
|
}
|
|
|
|
impl FromStr for VersionControl {
|
|
type Err = anyhow::Error;
|
|
|
|
fn from_str(s: &str) -> Result<Self, anyhow::Error> {
|
|
match s {
|
|
"git" => Ok(VersionControl::Git),
|
|
"hg" => Ok(VersionControl::Hg),
|
|
"pijul" => Ok(VersionControl::Pijul),
|
|
"fossil" => Ok(VersionControl::Fossil),
|
|
"none" => Ok(VersionControl::NoVcs),
|
|
other => anyhow::bail!("unknown vcs specification: `{}`", other),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'de> de::Deserialize<'de> for VersionControl {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: de::Deserializer<'de>,
|
|
{
|
|
let s = String::deserialize(deserializer)?;
|
|
FromStr::from_str(&s).map_err(de::Error::custom)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct NewOptions {
|
|
pub version_control: Option<VersionControl>,
|
|
pub kind: NewProjectKind,
|
|
pub auto_detect_kind: bool,
|
|
/// Absolute path to the directory for the new package
|
|
pub path: PathBuf,
|
|
pub name: Option<String>,
|
|
pub edition: Option<String>,
|
|
pub registry: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum NewProjectKind {
|
|
Bin,
|
|
Lib,
|
|
}
|
|
|
|
impl NewProjectKind {
|
|
fn is_bin(self) -> bool {
|
|
self == NewProjectKind::Bin
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for NewProjectKind {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match *self {
|
|
NewProjectKind::Bin => "binary (application)",
|
|
NewProjectKind::Lib => "library",
|
|
}
|
|
.fmt(f)
|
|
}
|
|
}
|
|
|
|
struct SourceFileInformation {
|
|
relative_path: String,
|
|
bin: bool,
|
|
}
|
|
|
|
struct MkOptions<'a> {
|
|
version_control: Option<VersionControl>,
|
|
path: &'a Path,
|
|
name: &'a str,
|
|
source_files: Vec<SourceFileInformation>,
|
|
edition: Option<&'a str>,
|
|
registry: Option<&'a str>,
|
|
}
|
|
|
|
impl NewOptions {
|
|
pub fn new(
|
|
version_control: Option<VersionControl>,
|
|
bin: bool,
|
|
lib: bool,
|
|
path: PathBuf,
|
|
name: Option<String>,
|
|
edition: Option<String>,
|
|
registry: Option<String>,
|
|
) -> CargoResult<NewOptions> {
|
|
let auto_detect_kind = !bin && !lib;
|
|
|
|
let kind = match (bin, lib) {
|
|
(true, true) => anyhow::bail!("can't specify both lib and binary outputs"),
|
|
(false, true) => NewProjectKind::Lib,
|
|
(_, false) => NewProjectKind::Bin,
|
|
};
|
|
|
|
let opts = NewOptions {
|
|
version_control,
|
|
kind,
|
|
auto_detect_kind,
|
|
path,
|
|
name,
|
|
edition,
|
|
registry,
|
|
};
|
|
Ok(opts)
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
struct CargoNewConfig {
|
|
#[deprecated = "cargo-new no longer supports adding the authors field"]
|
|
#[allow(dead_code)]
|
|
name: Option<String>,
|
|
|
|
#[deprecated = "cargo-new no longer supports adding the authors field"]
|
|
#[allow(dead_code)]
|
|
email: Option<String>,
|
|
|
|
#[serde(rename = "vcs")]
|
|
version_control: Option<VersionControl>,
|
|
}
|
|
|
|
fn get_name<'a>(path: &'a Path, opts: &'a NewOptions) -> CargoResult<&'a str> {
|
|
if let Some(ref name) = opts.name {
|
|
return Ok(name);
|
|
}
|
|
|
|
let file_name = path.file_name().ok_or_else(|| {
|
|
anyhow::format_err!(
|
|
"cannot auto-detect package name from path {:?} ; use --name to override",
|
|
path.as_os_str()
|
|
)
|
|
})?;
|
|
|
|
file_name.to_str().ok_or_else(|| {
|
|
anyhow::format_err!(
|
|
"cannot create package with a non-unicode name: {:?}",
|
|
file_name
|
|
)
|
|
})
|
|
}
|
|
|
|
/// See also `util::toml::embedded::sanitize_name`
|
|
fn check_name(
|
|
name: &str,
|
|
show_name_help: bool,
|
|
has_bin: bool,
|
|
shell: &mut Shell,
|
|
) -> CargoResult<()> {
|
|
// If --name is already used to override, no point in suggesting it
|
|
// again as a fix.
|
|
let name_help = if show_name_help {
|
|
"\nIf you need a package name to not match the directory name, consider using --name flag."
|
|
} else {
|
|
""
|
|
};
|
|
let bin_help = || {
|
|
let mut help = String::from(name_help);
|
|
if has_bin && !name.is_empty() {
|
|
help.push_str(&format!(
|
|
"\n\
|
|
If you need a binary with the name \"{name}\", use a valid package \
|
|
name, and set the binary name to be different from the package. \
|
|
This can be done by setting the binary filename to `src/bin/{name}.rs` \
|
|
or change the name in Cargo.toml with:\n\
|
|
\n \
|
|
[[bin]]\n \
|
|
name = \"{name}\"\n \
|
|
path = \"src/main.rs\"\n\
|
|
",
|
|
name = name
|
|
));
|
|
}
|
|
help
|
|
};
|
|
PackageName::new(name).map_err(|err| {
|
|
let help = bin_help();
|
|
anyhow::anyhow!("{err}{help}")
|
|
})?;
|
|
|
|
if restricted_names::is_keyword(name) {
|
|
anyhow::bail!(
|
|
"the name `{}` cannot be used as a package name, it is a Rust keyword{}",
|
|
name,
|
|
bin_help()
|
|
);
|
|
}
|
|
if restricted_names::is_conflicting_artifact_name(name) {
|
|
if has_bin {
|
|
anyhow::bail!(
|
|
"the name `{}` cannot be used as a package name, \
|
|
it conflicts with cargo's build directory names{}",
|
|
name,
|
|
name_help
|
|
);
|
|
} else {
|
|
shell.warn(format!(
|
|
"the name `{}` will not support binary \
|
|
executables with that name, \
|
|
it conflicts with cargo's build directory names",
|
|
name
|
|
))?;
|
|
}
|
|
}
|
|
if name == "test" {
|
|
anyhow::bail!(
|
|
"the name `test` cannot be used as a package name, \
|
|
it conflicts with Rust's built-in test library{}",
|
|
bin_help()
|
|
);
|
|
}
|
|
if ["core", "std", "alloc", "proc_macro", "proc-macro"].contains(&name) {
|
|
shell.warn(format!(
|
|
"the name `{}` is part of Rust's standard library\n\
|
|
It is recommended to use a different name to avoid problems.{}",
|
|
name,
|
|
bin_help()
|
|
))?;
|
|
}
|
|
if restricted_names::is_windows_reserved(name) {
|
|
if cfg!(windows) {
|
|
anyhow::bail!(
|
|
"cannot use name `{}`, it is a reserved Windows filename{}",
|
|
name,
|
|
name_help
|
|
);
|
|
} else {
|
|
shell.warn(format!(
|
|
"the name `{}` is a reserved Windows filename\n\
|
|
This package will not work on Windows platforms.",
|
|
name
|
|
))?;
|
|
}
|
|
}
|
|
if restricted_names::is_non_ascii_name(name) {
|
|
shell.warn(format!(
|
|
"the name `{}` contains non-ASCII characters\n\
|
|
Non-ASCII crate names are not supported by Rust.",
|
|
name
|
|
))?;
|
|
}
|
|
let name_in_lowercase = name.to_lowercase();
|
|
if name != name_in_lowercase {
|
|
shell.warn(format!(
|
|
"the name `{name}` is not snake_case or kebab-case which is recommended for package names, consider `{name_in_lowercase}`"
|
|
))?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Checks if the path contains any invalid PATH env characters.
|
|
fn check_path(path: &Path, shell: &mut Shell) -> CargoResult<()> {
|
|
// warn if the path contains characters that will break `env::join_paths`
|
|
if let Err(_) = paths::join_paths(slice::from_ref(&OsStr::new(path)), "") {
|
|
let path = path.to_string_lossy();
|
|
shell.warn(format!(
|
|
"the path `{path}` contains invalid PATH characters (usually `:`, `;`, or `\"`)\n\
|
|
It is recommended to use a different name to avoid problems."
|
|
))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn detect_source_paths_and_types(
|
|
package_path: &Path,
|
|
package_name: &str,
|
|
detected_files: &mut Vec<SourceFileInformation>,
|
|
) -> CargoResult<()> {
|
|
let path = package_path;
|
|
let name = package_name;
|
|
|
|
enum H {
|
|
Bin,
|
|
Lib,
|
|
Detect,
|
|
}
|
|
|
|
struct Test {
|
|
proposed_path: String,
|
|
handling: H,
|
|
}
|
|
|
|
let tests = vec![
|
|
Test {
|
|
proposed_path: "src/main.rs".to_string(),
|
|
handling: H::Bin,
|
|
},
|
|
Test {
|
|
proposed_path: "main.rs".to_string(),
|
|
handling: H::Bin,
|
|
},
|
|
Test {
|
|
proposed_path: format!("src/{}.rs", name),
|
|
handling: H::Detect,
|
|
},
|
|
Test {
|
|
proposed_path: format!("{}.rs", name),
|
|
handling: H::Detect,
|
|
},
|
|
Test {
|
|
proposed_path: "src/lib.rs".to_string(),
|
|
handling: H::Lib,
|
|
},
|
|
Test {
|
|
proposed_path: "lib.rs".to_string(),
|
|
handling: H::Lib,
|
|
},
|
|
];
|
|
|
|
for i in tests {
|
|
let pp = i.proposed_path;
|
|
|
|
// path/pp does not exist or is not a file
|
|
if !path.join(&pp).is_file() {
|
|
continue;
|
|
}
|
|
|
|
let sfi = match i.handling {
|
|
H::Bin => SourceFileInformation {
|
|
relative_path: pp,
|
|
bin: true,
|
|
},
|
|
H::Lib => SourceFileInformation {
|
|
relative_path: pp,
|
|
bin: false,
|
|
},
|
|
H::Detect => {
|
|
let content = paths::read(&path.join(pp.clone()))?;
|
|
let isbin = content.contains("fn main");
|
|
SourceFileInformation {
|
|
relative_path: pp,
|
|
bin: isbin,
|
|
}
|
|
}
|
|
};
|
|
detected_files.push(sfi);
|
|
}
|
|
|
|
// Check for duplicate lib attempt
|
|
|
|
let mut previous_lib_relpath: Option<&str> = None;
|
|
let mut duplicates_checker: BTreeMap<&str, &SourceFileInformation> = BTreeMap::new();
|
|
|
|
for i in detected_files {
|
|
if i.bin {
|
|
if let Some(x) = BTreeMap::get::<str>(&duplicates_checker, &name) {
|
|
anyhow::bail!(
|
|
"\
|
|
multiple possible binary sources found:
|
|
{}
|
|
{}
|
|
cannot automatically generate Cargo.toml as the main target would be ambiguous",
|
|
&x.relative_path,
|
|
&i.relative_path
|
|
);
|
|
}
|
|
duplicates_checker.insert(name, i);
|
|
} else {
|
|
if let Some(plp) = previous_lib_relpath {
|
|
anyhow::bail!(
|
|
"cannot have a package with \
|
|
multiple libraries, \
|
|
found both `{}` and `{}`",
|
|
plp,
|
|
i.relative_path
|
|
)
|
|
}
|
|
previous_lib_relpath = Some(&i.relative_path);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn plan_new_source_file(bin: bool) -> SourceFileInformation {
|
|
if bin {
|
|
SourceFileInformation {
|
|
relative_path: "src/main.rs".to_string(),
|
|
bin: true,
|
|
}
|
|
} else {
|
|
SourceFileInformation {
|
|
relative_path: "src/lib.rs".to_string(),
|
|
bin: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn calculate_new_project_kind(
|
|
requested_kind: NewProjectKind,
|
|
auto_detect_kind: bool,
|
|
found_files: &Vec<SourceFileInformation>,
|
|
) -> NewProjectKind {
|
|
let bin_file = found_files.iter().find(|x| x.bin);
|
|
|
|
let kind_from_files = if !found_files.is_empty() && bin_file.is_none() {
|
|
NewProjectKind::Lib
|
|
} else {
|
|
NewProjectKind::Bin
|
|
};
|
|
|
|
if auto_detect_kind {
|
|
return kind_from_files;
|
|
}
|
|
|
|
requested_kind
|
|
}
|
|
|
|
pub fn new(opts: &NewOptions, gctx: &GlobalContext) -> CargoResult<()> {
|
|
let path = &opts.path;
|
|
let name = get_name(path, opts)?;
|
|
gctx.shell()
|
|
.status("Creating", format!("{} `{}` package", opts.kind, name))?;
|
|
|
|
if path.exists() {
|
|
anyhow::bail!(
|
|
"destination `{}` already exists\n\n\
|
|
Use `cargo init` to initialize the directory",
|
|
path.display()
|
|
)
|
|
}
|
|
check_path(path, &mut gctx.shell())?;
|
|
|
|
let is_bin = opts.kind.is_bin();
|
|
|
|
check_name(name, opts.name.is_none(), is_bin, &mut gctx.shell())?;
|
|
|
|
let mkopts = MkOptions {
|
|
version_control: opts.version_control,
|
|
path,
|
|
name,
|
|
source_files: vec![plan_new_source_file(opts.kind.is_bin())],
|
|
edition: opts.edition.as_deref(),
|
|
registry: opts.registry.as_deref(),
|
|
};
|
|
|
|
mk(gctx, &mkopts).with_context(|| {
|
|
format!(
|
|
"Failed to create package `{}` at `{}`",
|
|
name,
|
|
path.display()
|
|
)
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn init(opts: &NewOptions, gctx: &GlobalContext) -> CargoResult<NewProjectKind> {
|
|
// This is here just as a random location to exercise the internal error handling.
|
|
if gctx.get_env_os("__CARGO_TEST_INTERNAL_ERROR").is_some() {
|
|
return Err(crate::util::internal("internal error test"));
|
|
}
|
|
|
|
let path = &opts.path;
|
|
let name = get_name(path, opts)?;
|
|
let mut src_paths_types = vec![];
|
|
detect_source_paths_and_types(path, name, &mut src_paths_types)?;
|
|
let kind = calculate_new_project_kind(opts.kind, opts.auto_detect_kind, &src_paths_types);
|
|
gctx.shell()
|
|
.status("Creating", format!("{} package", opts.kind))?;
|
|
|
|
if path.join("Cargo.toml").exists() {
|
|
anyhow::bail!("`cargo init` cannot be run on existing Cargo packages")
|
|
}
|
|
check_path(path, &mut gctx.shell())?;
|
|
|
|
let has_bin = kind.is_bin();
|
|
|
|
if src_paths_types.is_empty() {
|
|
src_paths_types.push(plan_new_source_file(has_bin));
|
|
} else if src_paths_types.len() == 1 && !src_paths_types.iter().any(|x| x.bin == has_bin) {
|
|
// we've found the only file and it's not the type user wants. Change the type and warn
|
|
let file_type = if src_paths_types[0].bin {
|
|
NewProjectKind::Bin
|
|
} else {
|
|
NewProjectKind::Lib
|
|
};
|
|
gctx.shell().warn(format!(
|
|
"file `{}` seems to be a {} file",
|
|
src_paths_types[0].relative_path, file_type
|
|
))?;
|
|
src_paths_types[0].bin = has_bin
|
|
} else if src_paths_types.len() > 1 && !has_bin {
|
|
// We have found both lib and bin files and the user would like us to treat both as libs
|
|
anyhow::bail!(
|
|
"cannot have a package with \
|
|
multiple libraries, \
|
|
found both `{}` and `{}`",
|
|
src_paths_types[0].relative_path,
|
|
src_paths_types[1].relative_path
|
|
)
|
|
}
|
|
|
|
check_name(name, opts.name.is_none(), has_bin, &mut gctx.shell())?;
|
|
|
|
let mut version_control = opts.version_control;
|
|
|
|
if version_control == None {
|
|
let mut num_detected_vcses = 0;
|
|
|
|
if path.join(".git").exists() {
|
|
version_control = Some(VersionControl::Git);
|
|
num_detected_vcses += 1;
|
|
}
|
|
|
|
if path.join(".hg").exists() {
|
|
version_control = Some(VersionControl::Hg);
|
|
num_detected_vcses += 1;
|
|
}
|
|
|
|
if path.join(".pijul").exists() {
|
|
version_control = Some(VersionControl::Pijul);
|
|
num_detected_vcses += 1;
|
|
}
|
|
|
|
if path.join(".fossil").exists() {
|
|
version_control = Some(VersionControl::Fossil);
|
|
num_detected_vcses += 1;
|
|
}
|
|
|
|
// if none exists, maybe create git, like in `cargo new`
|
|
|
|
if num_detected_vcses > 1 {
|
|
anyhow::bail!(
|
|
"more than one of .hg, .git, .pijul, .fossil configurations \
|
|
found and the ignore file can't be filled in as \
|
|
a result. specify --vcs to override detection"
|
|
);
|
|
}
|
|
}
|
|
|
|
let mkopts = MkOptions {
|
|
version_control,
|
|
path,
|
|
name,
|
|
source_files: src_paths_types,
|
|
edition: opts.edition.as_deref(),
|
|
registry: opts.registry.as_deref(),
|
|
};
|
|
|
|
mk(gctx, &mkopts).with_context(|| {
|
|
format!(
|
|
"Failed to create package `{}` at `{}`",
|
|
name,
|
|
path.display()
|
|
)
|
|
})?;
|
|
Ok(kind)
|
|
}
|
|
|
|
/// IgnoreList
|
|
struct IgnoreList {
|
|
/// git like formatted entries
|
|
ignore: Vec<String>,
|
|
/// mercurial formatted entries
|
|
hg_ignore: Vec<String>,
|
|
/// Fossil-formatted entries.
|
|
fossil_ignore: Vec<String>,
|
|
}
|
|
|
|
impl IgnoreList {
|
|
/// constructor to build a new ignore file
|
|
fn new() -> IgnoreList {
|
|
IgnoreList {
|
|
ignore: Vec::new(),
|
|
hg_ignore: Vec::new(),
|
|
fossil_ignore: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Add a new entry to the ignore list. Requires three arguments with the
|
|
/// entry in possibly three different formats. One for "git style" entries,
|
|
/// one for "mercurial style" entries and one for "fossil style" entries.
|
|
fn push(&mut self, ignore: &str, hg_ignore: &str, fossil_ignore: &str) {
|
|
self.ignore.push(ignore.to_string());
|
|
self.hg_ignore.push(hg_ignore.to_string());
|
|
self.fossil_ignore.push(fossil_ignore.to_string());
|
|
}
|
|
|
|
/// Return the correctly formatted content of the ignore file for the given
|
|
/// version control system as `String`.
|
|
fn format_new(&self, vcs: VersionControl) -> String {
|
|
let ignore_items = match vcs {
|
|
VersionControl::Hg => &self.hg_ignore,
|
|
VersionControl::Fossil => &self.fossil_ignore,
|
|
_ => &self.ignore,
|
|
};
|
|
|
|
ignore_items.join("\n") + "\n"
|
|
}
|
|
|
|
/// format_existing is used to format the IgnoreList when the ignore file
|
|
/// already exists. It reads the contents of the given `BufRead` and
|
|
/// checks if the contents of the ignore list are already existing in the
|
|
/// file.
|
|
fn format_existing<T: BufRead>(&self, existing: T, vcs: VersionControl) -> CargoResult<String> {
|
|
let mut existing_items = Vec::new();
|
|
for (i, item) in existing.lines().enumerate() {
|
|
match item {
|
|
Ok(s) => existing_items.push(s),
|
|
Err(err) => match err.kind() {
|
|
ErrorKind::InvalidData => {
|
|
return Err(anyhow!(
|
|
"Character at line {} is invalid. Cargo only supports UTF-8.",
|
|
i
|
|
))
|
|
}
|
|
_ => return Err(anyhow!(err)),
|
|
},
|
|
}
|
|
}
|
|
|
|
let ignore_items = match vcs {
|
|
VersionControl::Hg => &self.hg_ignore,
|
|
VersionControl::Fossil => &self.fossil_ignore,
|
|
_ => &self.ignore,
|
|
};
|
|
|
|
let mut out = String::new();
|
|
|
|
// Fossil does not support `#` comments.
|
|
if vcs != VersionControl::Fossil {
|
|
out.push_str("\n\n# Added by cargo\n");
|
|
if ignore_items
|
|
.iter()
|
|
.any(|item| existing_items.contains(item))
|
|
{
|
|
out.push_str("#\n# already existing elements were commented out\n");
|
|
}
|
|
out.push('\n');
|
|
}
|
|
|
|
for item in ignore_items {
|
|
if existing_items.contains(item) {
|
|
if vcs == VersionControl::Fossil {
|
|
// Just merge for Fossil.
|
|
continue;
|
|
}
|
|
out.push('#');
|
|
}
|
|
out.push_str(item);
|
|
out.push('\n');
|
|
}
|
|
|
|
Ok(out)
|
|
}
|
|
}
|
|
|
|
/// Writes the ignore file to the given directory. If the ignore file for the
|
|
/// given vcs system already exists, its content is read and duplicate ignore
|
|
/// file entries are filtered out.
|
|
fn write_ignore_file(base_path: &Path, list: &IgnoreList, vcs: VersionControl) -> CargoResult<()> {
|
|
// Fossil only supports project-level settings in a dedicated subdirectory.
|
|
if vcs == VersionControl::Fossil {
|
|
paths::create_dir_all(base_path.join(".fossil-settings"))?;
|
|
}
|
|
|
|
for fp_ignore in match vcs {
|
|
VersionControl::Git => vec![base_path.join(".gitignore")],
|
|
VersionControl::Hg => vec![base_path.join(".hgignore")],
|
|
VersionControl::Pijul => vec![base_path.join(".ignore")],
|
|
// Fossil has a cleaning functionality configured in a separate file.
|
|
VersionControl::Fossil => vec![
|
|
base_path.join(".fossil-settings/ignore-glob"),
|
|
base_path.join(".fossil-settings/clean-glob"),
|
|
],
|
|
VersionControl::NoVcs => return Ok(()),
|
|
} {
|
|
let ignore: String = match paths::open(&fp_ignore) {
|
|
Err(err) => match err.downcast_ref::<std::io::Error>() {
|
|
Some(io_err) if io_err.kind() == ErrorKind::NotFound => list.format_new(vcs),
|
|
_ => return Err(err),
|
|
},
|
|
Ok(file) => list.format_existing(BufReader::new(file), vcs)?,
|
|
};
|
|
|
|
paths::append(&fp_ignore, ignore.as_bytes())?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Initializes the correct VCS system based on the provided config.
|
|
fn init_vcs(path: &Path, vcs: VersionControl, gctx: &GlobalContext) -> CargoResult<()> {
|
|
match vcs {
|
|
VersionControl::Git => {
|
|
if !path.join(".git").exists() {
|
|
// Temporary fix to work around bug in libgit2 when creating a
|
|
// directory in the root of a posix filesystem.
|
|
// See: https://github.com/libgit2/libgit2/issues/5130
|
|
paths::create_dir_all(path)?;
|
|
GitRepo::init(path, gctx.cwd())?;
|
|
}
|
|
}
|
|
VersionControl::Hg => {
|
|
if !path.join(".hg").exists() {
|
|
HgRepo::init(path, gctx.cwd())?;
|
|
}
|
|
}
|
|
VersionControl::Pijul => {
|
|
if !path.join(".pijul").exists() {
|
|
PijulRepo::init(path, gctx.cwd())?;
|
|
}
|
|
}
|
|
VersionControl::Fossil => {
|
|
if !path.join(".fossil").exists() {
|
|
FossilRepo::init(path, gctx.cwd())?;
|
|
}
|
|
}
|
|
VersionControl::NoVcs => {
|
|
paths::create_dir_all(path)?;
|
|
}
|
|
};
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn mk(gctx: &GlobalContext, opts: &MkOptions<'_>) -> CargoResult<()> {
|
|
let path = opts.path;
|
|
let name = opts.name;
|
|
let cfg = gctx.get::<CargoNewConfig>("cargo-new")?;
|
|
|
|
// Using the push method with multiple arguments ensures that the entries
|
|
// for all mutually-incompatible VCS in terms of syntax are in sync.
|
|
let mut ignore = IgnoreList::new();
|
|
ignore.push("/target", "^target$", "target");
|
|
|
|
let vcs = opts.version_control.unwrap_or_else(|| {
|
|
let in_existing_vcs = existing_vcs_repo(path.parent().unwrap_or(path), gctx.cwd());
|
|
match (cfg.version_control, in_existing_vcs) {
|
|
(None, false) => VersionControl::Git,
|
|
(Some(opt), false) => opt,
|
|
(_, true) => VersionControl::NoVcs,
|
|
}
|
|
});
|
|
|
|
init_vcs(path, vcs, gctx)?;
|
|
write_ignore_file(path, &ignore, vcs)?;
|
|
|
|
// Create `Cargo.toml` file with necessary `[lib]` and `[[bin]]` sections, if needed.
|
|
let mut manifest = toml_edit::DocumentMut::new();
|
|
manifest["package"] = toml_edit::Item::Table(toml_edit::Table::new());
|
|
manifest["package"]["name"] = toml_edit::value(name);
|
|
manifest["package"]["version"] = toml_edit::value("0.1.0");
|
|
let edition = match opts.edition {
|
|
Some(edition) => edition.to_string(),
|
|
None => Edition::LATEST_STABLE.to_string(),
|
|
};
|
|
manifest["package"]["edition"] = toml_edit::value(edition);
|
|
if let Some(registry) = opts.registry {
|
|
let mut array = toml_edit::Array::default();
|
|
array.push(registry);
|
|
manifest["package"]["publish"] = toml_edit::value(array);
|
|
}
|
|
let dep_table = toml_edit::Table::default();
|
|
manifest["dependencies"] = toml_edit::Item::Table(dep_table);
|
|
|
|
// Calculate what `[lib]` and `[[bin]]`s we need to append to `Cargo.toml`.
|
|
for i in &opts.source_files {
|
|
if i.bin {
|
|
if i.relative_path != "src/main.rs" {
|
|
let mut bin = toml_edit::Table::new();
|
|
bin["name"] = toml_edit::value(name);
|
|
bin["path"] = toml_edit::value(i.relative_path.clone());
|
|
manifest["bin"]
|
|
.or_insert(toml_edit::Item::ArrayOfTables(
|
|
toml_edit::ArrayOfTables::new(),
|
|
))
|
|
.as_array_of_tables_mut()
|
|
.expect("bin is an array of tables")
|
|
.push(bin);
|
|
}
|
|
} else if i.relative_path != "src/lib.rs" {
|
|
let mut lib = toml_edit::Table::new();
|
|
lib["path"] = toml_edit::value(i.relative_path.clone());
|
|
manifest["lib"] = toml_edit::Item::Table(lib);
|
|
}
|
|
}
|
|
|
|
let manifest_path = path.join("Cargo.toml");
|
|
if let Ok(root_manifest_path) = find_root_manifest_for_wd(&manifest_path) {
|
|
let root_manifest = paths::read(&root_manifest_path)?;
|
|
// Sometimes the root manifest is not a valid manifest, so we only try to parse it if it is.
|
|
// This should not block the creation of the new project. It is only a best effort to
|
|
// inherit the workspace package keys.
|
|
if let Ok(mut workspace_document) = root_manifest.parse::<toml_edit::DocumentMut>() {
|
|
let display_path = get_display_path(&root_manifest_path, &path)?;
|
|
let can_be_a_member = can_be_workspace_member(&display_path, &workspace_document)?;
|
|
// Only try to inherit the workspace stuff if the new package can be a member of the workspace.
|
|
if can_be_a_member {
|
|
if let Some(workspace_package_keys) = workspace_document
|
|
.get("workspace")
|
|
.and_then(|workspace| workspace.get("package"))
|
|
.and_then(|package| package.as_table())
|
|
{
|
|
update_manifest_with_inherited_workspace_package_keys(
|
|
opts,
|
|
&mut manifest,
|
|
workspace_package_keys,
|
|
)
|
|
}
|
|
// Try to inherit the workspace lints key if it exists.
|
|
if workspace_document
|
|
.get("workspace")
|
|
.and_then(|workspace| workspace.get("lints"))
|
|
.is_some()
|
|
{
|
|
let mut table = toml_edit::Table::new();
|
|
table["workspace"] = toml_edit::value(true);
|
|
manifest["lints"] = toml_edit::Item::Table(table);
|
|
}
|
|
|
|
// Try to add the new package to the workspace members.
|
|
if update_manifest_with_new_member(
|
|
&root_manifest_path,
|
|
&mut workspace_document,
|
|
&display_path,
|
|
)? {
|
|
gctx.shell().status(
|
|
"Adding",
|
|
format!(
|
|
"`{}` as member of workspace at `{}`",
|
|
PathBuf::from(&display_path)
|
|
.file_name()
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap(),
|
|
root_manifest_path.parent().unwrap().display()
|
|
),
|
|
)?
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
paths::write(&manifest_path, manifest.to_string())?;
|
|
|
|
// Create all specified source files (with respective parent directories) if they don't exist.
|
|
for i in &opts.source_files {
|
|
let path_of_source_file = path.join(i.relative_path.clone());
|
|
|
|
if let Some(src_dir) = path_of_source_file.parent() {
|
|
paths::create_dir_all(src_dir)?;
|
|
}
|
|
|
|
let default_file_content: &[u8] = if i.bin {
|
|
b"\
|
|
fn main() {
|
|
println!(\"Hello, world!\");
|
|
}
|
|
"
|
|
} else {
|
|
b"\
|
|
pub fn add(left: usize, right: usize) -> usize {
|
|
left + right
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn it_works() {
|
|
let result = add(2, 2);
|
|
assert_eq!(result, 4);
|
|
}
|
|
}
|
|
"
|
|
};
|
|
|
|
if !path_of_source_file.is_file() {
|
|
paths::write(&path_of_source_file, default_file_content)?;
|
|
|
|
// Format the newly created source file
|
|
if let Err(e) = cargo_util::ProcessBuilder::new("rustfmt")
|
|
.arg(&path_of_source_file)
|
|
.exec_with_output()
|
|
{
|
|
tracing::warn!("failed to call rustfmt: {:#}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Err(e) = Workspace::new(&path.join("Cargo.toml"), gctx) {
|
|
crate::display_warning_with_error(
|
|
"compiling this new package may not work due to invalid \
|
|
workspace configuration",
|
|
&e,
|
|
&mut gctx.shell(),
|
|
);
|
|
}
|
|
|
|
gctx.shell().note(
|
|
"see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html",
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Update the manifest with the inherited workspace package keys.
|
|
// If the option is not set, the key is removed from the manifest.
|
|
// If the option is set, keep the value from the manifest.
|
|
fn update_manifest_with_inherited_workspace_package_keys(
|
|
opts: &MkOptions<'_>,
|
|
manifest: &mut toml_edit::DocumentMut,
|
|
workspace_package_keys: &toml_edit::Table,
|
|
) {
|
|
if workspace_package_keys.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let try_remove_and_inherit_package_key = |key: &str, manifest: &mut toml_edit::DocumentMut| {
|
|
let package = manifest["package"]
|
|
.as_table_mut()
|
|
.expect("package is a table");
|
|
package.remove(key);
|
|
let mut table = toml_edit::Table::new();
|
|
table.set_dotted(true);
|
|
table["workspace"] = toml_edit::value(true);
|
|
package.insert(key, toml_edit::Item::Table(table));
|
|
};
|
|
|
|
// Inherit keys from the workspace.
|
|
// Only keep the value from the manifest if the option is set.
|
|
for (key, _) in workspace_package_keys {
|
|
if key == "edition" && opts.edition.is_some() {
|
|
continue;
|
|
}
|
|
if key == "publish" && opts.registry.is_some() {
|
|
continue;
|
|
}
|
|
|
|
try_remove_and_inherit_package_key(key, manifest);
|
|
}
|
|
}
|
|
|
|
/// Adds the new package member to the [workspace.members] array.
|
|
/// - It first checks if the name matches any element in [workspace.exclude],
|
|
/// and it ignores the name if there is a match.
|
|
/// - Then it check if the name matches any element already in [workspace.members],
|
|
/// and it ignores the name if there is a match.
|
|
/// - If [workspace.members] doesn't exist in the manifest, it will add a new section
|
|
/// with the new package in it.
|
|
fn update_manifest_with_new_member(
|
|
root_manifest_path: &Path,
|
|
workspace_document: &mut toml_edit::DocumentMut,
|
|
display_path: &str,
|
|
) -> CargoResult<bool> {
|
|
// If the members element already exist, check if one of the patterns
|
|
// in the array already includes the new package's relative path.
|
|
// - Add the relative path if the members don't match the new package's path.
|
|
// - Create a new members array if there are no members element in the workspace yet.
|
|
if let Some(workspace) = workspace_document.get_mut("workspace") {
|
|
if let Some(members) = workspace
|
|
.get_mut("members")
|
|
.and_then(|members| members.as_array_mut())
|
|
{
|
|
for member in members.iter() {
|
|
let pat = member
|
|
.as_str()
|
|
.with_context(|| format!("invalid non-string member `{}`", member))?;
|
|
let pattern = glob::Pattern::new(pat)
|
|
.with_context(|| format!("cannot build glob pattern from `{}`", pat))?;
|
|
|
|
if pattern.matches(&display_path) {
|
|
return Ok(false);
|
|
}
|
|
}
|
|
|
|
let was_sorted = is_sorted(members.iter().map(Value::as_str));
|
|
members.push(display_path);
|
|
if was_sorted {
|
|
members.sort_by(|lhs, rhs| lhs.as_str().cmp(&rhs.as_str()));
|
|
}
|
|
} else {
|
|
let mut array = Array::new();
|
|
array.push(display_path);
|
|
|
|
workspace["members"] = toml_edit::value(array);
|
|
}
|
|
}
|
|
|
|
write_atomic(
|
|
&root_manifest_path,
|
|
workspace_document.to_string().to_string().as_bytes(),
|
|
)?;
|
|
Ok(true)
|
|
}
|
|
|
|
fn get_display_path(root_manifest_path: &Path, package_path: &Path) -> CargoResult<String> {
|
|
// Find the relative path for the package from the workspace root directory.
|
|
let workspace_root = root_manifest_path.parent().with_context(|| {
|
|
format!(
|
|
"workspace root manifest doesn't have a parent directory `{}`",
|
|
root_manifest_path.display()
|
|
)
|
|
})?;
|
|
let relpath = pathdiff::diff_paths(package_path, workspace_root).with_context(|| {
|
|
format!(
|
|
"path comparison requires two absolute paths; package_path: `{}`, workspace_path: `{}`",
|
|
package_path.display(),
|
|
workspace_root.display()
|
|
)
|
|
})?;
|
|
|
|
let mut components = Vec::new();
|
|
for comp in relpath.iter() {
|
|
let comp = comp.to_str().with_context(|| {
|
|
format!("invalid unicode component in path `{}`", relpath.display())
|
|
})?;
|
|
components.push(comp);
|
|
}
|
|
let display_path = components.join("/");
|
|
Ok(display_path)
|
|
}
|
|
|
|
// Check if the package can be a member of the workspace.
|
|
fn can_be_workspace_member(
|
|
display_path: &str,
|
|
workspace_document: &toml_edit::DocumentMut,
|
|
) -> CargoResult<bool> {
|
|
if let Some(exclude) = workspace_document
|
|
.get("workspace")
|
|
.and_then(|workspace| workspace.get("exclude"))
|
|
.and_then(|exclude| exclude.as_array())
|
|
{
|
|
for member in exclude {
|
|
let pat = member
|
|
.as_str()
|
|
.with_context(|| format!("invalid non-string exclude path `{}`", member))?;
|
|
if pat == display_path {
|
|
return Ok(false);
|
|
}
|
|
}
|
|
}
|
|
Ok(true)
|
|
}
|