From 2779f1486a4904f2891e0a8b7831b430a0469b9e Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Tue, 30 Nov 2021 08:14:03 +0000 Subject: [PATCH] init: new command similar to `cargo init` --- Changelog.md | 4 +- src/lib.rs | 2 +- src/main.rs | 33 +++++++------- src/new_project.rs | 105 ++++++++++++++++++++++++++++++--------------- 4 files changed, 91 insertions(+), 53 deletions(-) diff --git a/Changelog.md b/Changelog.md index f52416ce4..302e413a7 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Add a `maturin init` command as a companion to `maturin new` in [#719](https://github.com/PyO3/maturin/pull/719) + ## [0.12.3] - 2021-11-29 * Use platform tag from `sysconfig.platform` on non-portable Linux in [#709](https://github.com/PyO3/maturin/pull/709) @@ -122,7 +124,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.10.3] - 2021-04-13 * The `upload` command is now implemented, it is mostly similar to `twine upload`. [#484](https://github.com/PyO3/maturin/pull/484) - * Interpreter search now uses python 3.6 to 3.12 + * Interpreter search now uses python 3.6 to 3.12 * Add basic support for OpenBSD in [#496](https://github.com/PyO3/maturin/pull/496) * Fix the PowerPC platform by messense in [#503](https://github.com/PyO3/maturin/pull/503) diff --git a/src/lib.rs b/src/lib.rs index 0fe93f2ab..ba7c44928 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,7 +36,7 @@ pub use crate::metadata::{Metadata21, WheelMetadata}; pub use crate::module_writer::{ write_dist_info, ModuleWriter, PathWriter, SDistWriter, WheelWriter, }; -pub use crate::new_project::new_project; +pub use crate::new_project::{init_project, new_project, GenerateProjectOptions}; pub use crate::pyproject_toml::PyProjectToml; pub use crate::python_interpreter::PythonInterpreter; pub use crate::target::Target; diff --git a/src/main.rs b/src/main.rs index a087d0058..75aeff54c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,9 +14,11 @@ use fs_err as fs; use human_panic::setup_panic; #[cfg(feature = "password-storage")] use keyring::{Keyring, KeyringError}; +use maturin::GenerateProjectOptions; use maturin::{ - develop, new_project, source_distribution, write_dist_info, BridgeModel, BuildOptions, - CargoToml, Metadata21, PathWriter, PlatformTag, PyProjectToml, PythonInterpreter, Target, + develop, init_project, new_project, source_distribution, write_dist_info, BridgeModel, + BuildOptions, CargoToml, Metadata21, PathWriter, PlatformTag, PyProjectToml, PythonInterpreter, + Target, }; use std::env; use std::io; @@ -270,20 +272,21 @@ enum Opt { #[structopt(short, long, parse(from_os_str))] out: Option, }, + /// Create a new cargo project in an existing directory + #[structopt(name = "init")] + InitProject { + /// Project path + path: Option, + #[structopt(flatten)] + options: GenerateProjectOptions, + }, /// Create a new cargo project #[structopt(name = "new")] NewProject { /// Project path path: String, - /// Set the resulting package name, defaults to the directory name - #[structopt(long)] - name: Option, - /// Use mixed Rust/Python project layout - #[structopt(long)] - mixed: bool, - /// Which kind of bindings to use - #[structopt(short, long, possible_values = &["pyo3", "rust-cpython", "cffi", "bin"])] - bindings: Option, + #[structopt(flatten)] + options: GenerateProjectOptions, }, /// Uploads python packages to pypi /// @@ -636,12 +639,8 @@ fn run() -> Result<()> { .context("Failed to build source distribution")?; } Opt::Pep517(subcommand) => pep517(subcommand)?, - Opt::NewProject { - path, - name, - mixed, - bindings, - } => new_project(path, name, mixed, bindings)?, + Opt::InitProject { path, options } => init_project(path, options)?, + Opt::NewProject { path, options } => new_project(path, options)?, #[cfg(feature = "upload")] Opt::Upload { publish, files } => { if files.is_empty() { diff --git a/src/new_project.rs b/src/new_project.rs index 82d0baddc..39f1ddb37 100644 --- a/src/new_project.rs +++ b/src/new_project.rs @@ -4,6 +4,7 @@ use dialoguer::{theme::ColorfulTheme, Select}; use fs_err as fs; use minijinja::{context, Environment}; use std::path::Path; +use structopt::StructOpt; struct ProjectGenerator<'a> { env: Environment<'a>, @@ -11,10 +12,11 @@ struct ProjectGenerator<'a> { crate_name: String, bindings: String, mixed: bool, + overwrite: bool, } impl<'a> ProjectGenerator<'a> { - fn new(project_name: String, mixed: bool, bindings: String) -> Result { + fn new(project_name: String, mixed: bool, bindings: String, overwrite: bool) -> Result { let crate_name = project_name.replace('-', "_"); let mut env = Environment::new(); env.add_template(".gitignore", include_str!("templates/.gitignore.j2"))?; @@ -33,6 +35,7 @@ impl<'a> ProjectGenerator<'a> { crate_name, bindings, mixed, + overwrite, }) } @@ -40,41 +43,26 @@ impl<'a> ProjectGenerator<'a> { let src_path = project_path.join("src"); fs::create_dir_all(&src_path)?; - let gitignore = self.render_template(".gitignore")?; - fs::write(project_path.join(".gitignore"), gitignore)?; - - let cargo_toml = self.render_template("Cargo.toml")?; - fs::write(project_path.join("Cargo.toml"), cargo_toml)?; - - let pyproject_toml = self.render_template("pyproject.toml")?; - fs::write(project_path.join("pyproject.toml"), pyproject_toml)?; + self.write_project_file(project_path, ".gitignore")?; + self.write_project_file(project_path, "Cargo.toml")?; + self.write_project_file(project_path, "pyproject.toml")?; if self.bindings == "bin" { - let main_rs = self.render_template("main.rs")?; - fs::write(src_path.join("main.rs"), main_rs)?; + self.write_project_file(&src_path, "main.rs")?; } else { - let lib_rs = self.render_template("lib.rs")?; - fs::write(src_path.join("lib.rs"), lib_rs)?; + self.write_project_file(&src_path, "lib.rs")?; } let gh_action_path = project_path.join(".github").join("workflows"); fs::create_dir_all(&gh_action_path)?; - let ci_yml = self.render_template("CI.yml")?; - fs::write(gh_action_path.join("CI.yml"), ci_yml)?; + self.write_project_file(&gh_action_path, "CI.yml")?; if self.mixed { let py_path = project_path.join(&self.crate_name); fs::create_dir_all(&py_path)?; - let init_py = self.render_template("__init__.py")?; - fs::write(py_path.join("__init__.py"), init_py)?; + self.write_project_file(&py_path, "__init__.py")?; } - println!( - " ✨ {} {} {}", - style("Done!").bold().green(), - style("New project created").bold(), - style(&project_path.display()).underlined() - ); Ok(()) } @@ -91,37 +79,86 @@ impl<'a> ProjectGenerator<'a> { ))?; Ok(out) } + + fn write_project_file(&self, directory: &Path, file: &str) -> Result<()> { + let path = directory.join(file); + if self.overwrite || !path.exists() { + fs::write(path, self.render_template(file)?)?; + } + Ok(()) + } } -/// Generate a new cargo project -pub fn new_project( - path: String, +#[derive(Debug, StructOpt)] +/// Options common to `maturin new` and `maturin init`. +pub struct GenerateProjectOptions { + /// Set the resulting package name, defaults to the directory name + #[structopt(long)] name: Option, + /// Use mixed Rust/Python project layout + #[structopt(long)] mixed: bool, + /// Which kind of bindings to use + #[structopt(short, long, possible_values = &["pyo3", "rust-cpython", "cffi", "bin"])] bindings: Option, -) -> Result<()> { +} + +/// Generate a new cargo project +pub fn new_project(path: String, options: GenerateProjectOptions) -> Result<()> { let project_path = Path::new(&path); if project_path.exists() { bail!("destination `{}` already exists", project_path.display()); } + generate_project(project_path, options, true)?; + println!( + " ✨ {} {} {}", + style("Done!").bold().green(), + style("New project created").bold(), + style(&project_path.display()).underlined() + ); + Ok(()) +} + +/// Generate a new cargo project in an existing directory +pub fn init_project(path: Option, options: GenerateProjectOptions) -> Result<()> { + let project_path = path + .map(Into::into) + .map_or_else(std::env::current_dir, Ok)?; + if project_path.join("pyproject.toml").exists() || project_path.join("Cargo.toml").exists() { + bail!("`maturin init` cannot be run on existing projects"); + } + generate_project(&project_path, options, false)?; + println!( + " ✨ {} {} {}", + style("Done!").bold().green(), + style("Initialized project").bold(), + style(&project_path.display()).underlined() + ); + Ok(()) +} - let name = if let Some(name) = name { +fn generate_project( + project_path: &Path, + options: GenerateProjectOptions, + overwrite: bool, +) -> Result<()> { + let name = if let Some(name) = options.name { name } else { - let file_name = project_path - .file_name() - .context("Fail to get name from path")?; + let file_name = project_path.file_name().with_context(|| { + format!("Failed to get name from path '{}'", project_path.display()) + })?; file_name .to_str() .context("Filename isn't valid Unicode")? .to_string() }; - let bindings_items = if mixed { + let bindings_items = if options.mixed { vec!["pyo3", "rust-cpython", "cffi"] } else { vec!["pyo3", "rust-cpython", "cffi", "bin"] }; - let bindings = if let Some(bindings) = bindings { + let bindings = if let Some(bindings) = options.bindings { bindings } else { let selection = Select::with_theme(&ColorfulTheme::default()) @@ -135,6 +172,6 @@ pub fn new_project( bindings_items[selection].to_string() }; - let generator = ProjectGenerator::new(name, mixed, bindings)?; + let generator = ProjectGenerator::new(name, options.mixed, bindings, overwrite)?; generator.generate(project_path) }