diff --git a/Cargo.lock b/Cargo.lock index 89b92f261..65746d579 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -345,6 +345,21 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06821ea598337a8412cf47c5b71c3bc694a7f0aed188ac28b836fab164a2c202" +[[package]] +name = "console" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "regex", + "terminal_size", + "unicode-width", + "winapi", +] + [[package]] name = "core-foundation" version = "0.9.2" @@ -410,6 +425,18 @@ dependencies = [ "syn", ] +[[package]] +name = "dialoguer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61579ada4ec0c6031cfac3f86fdba0d195a7ebeb5e36693bd53cb5999a25beeb" +dependencies = [ + "console", + "lazy_static", + "tempfile", + "zeroize", +] + [[package]] name = "digest" version = "0.9.0" @@ -439,6 +466,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.29" @@ -1005,6 +1038,8 @@ dependencies = [ "cargo_metadata", "cbindgen", "configparser", + "console", + "dialoguer", "dirs", "fat-macho", "flate2", @@ -1015,6 +1050,7 @@ dependencies = [ "ignore", "indoc", "keyring", + "minijinja", "once_cell", "platform-info", "pretty_env_logger", @@ -1059,6 +1095,15 @@ dependencies = [ "unicase", ] +[[package]] +name = "minijinja" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c8de52c925a3f40bd30cb2041440eb3d11658b9a2b8564c1d66fba41c890dc6" +dependencies = [ + "serde", +] + [[package]] name = "miniz_oxide" version = "0.4.4" @@ -1885,6 +1930,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -2370,6 +2425,12 @@ dependencies = [ "syn", ] +[[package]] +name = "zeroize" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619" + [[package]] name = "zip" version = "0.5.13" diff --git a/Cargo.toml b/Cargo.toml index 4ac63866a..35a50b5af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,9 @@ pyproject-toml = "0.3.0" python-pkginfo = "0.5.0" textwrap = "0.14.2" ignore = "0.4.18" +dialoguer = "0.9.0" +console = "0.15.0" +minijinja = "0.8.2" [dev-dependencies] indoc = "1.0.3" diff --git a/Changelog.md b/Changelog.md index 6f7263459..dbd531f35 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Add support for excluding files from wheels by `.gitignore` in [#695](https://github.com/PyO3/maturin/pull/695) * Fix `pip install maturin` on OpenBSD 6.8 in [#697](https://github.com/PyO3/maturin/pull/697) * Add support for x86, x86_64 and aarch64 on NetBSD in [#704](https://github.com/PyO3/maturin/pull/704) +* Add a `maturin new` command for bootstrapping new projects in [#705](https://github.com/PyO3/maturin/pull/705) ## [0.12.1] - 2021-11-21 diff --git a/hello/.gitignore b/hello/.gitignore new file mode 100644 index 000000000..c8f044299 --- /dev/null +++ b/hello/.gitignore @@ -0,0 +1,72 @@ +/target + +# Byte-compiled / optimized / DLL files +__pycache__/ +.pytest_cache/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +.venv/ +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +include/ +man/ +venv/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +pip-selfcheck.json + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +.DS_Store + +# Sphinx documentation +docs/_build/ + +# PyCharm +.idea/ + +# VSCode +.vscode/ + +# Pyenv +.python-version diff --git a/hello/Cargo.toml b/hello/Cargo.toml new file mode 100644 index 000000000..28b5735a2 --- /dev/null +++ b/hello/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "hello" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "hello" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { version = "0.15.1", features = ["extension-module"] } diff --git a/hello/pyproject.toml b/hello/pyproject.toml new file mode 100644 index 000000000..d99236fde --- /dev/null +++ b/hello/pyproject.toml @@ -0,0 +1,14 @@ +[build-system] +requires = ["maturin>=0.12,<0.13"] +build-backend = "maturin" + +[project] +name = "hello" +requires-python = ">=3.6" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] + + diff --git a/hello/src/lib.rs b/hello/src/lib.rs new file mode 100644 index 000000000..43c6abb9c --- /dev/null +++ b/hello/src/lib.rs @@ -0,0 +1,7 @@ + +use pyo3::prelude::*; + +#[pymodule] +fn hello(_py: Python, m: &PyModule) -> PyResult<()> { + Ok(()) +} diff --git a/pyproject.toml b/pyproject.toml index 7f0b5d7d9..f485ea570 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,3 +19,7 @@ bindings = "bin" [tool.black] target_version = ['py36'] +extend-exclude = ''' +# Ignore cargo-generate templates +^/src/templates +''' diff --git a/src/lib.rs b/src/lib.rs index aa5e7bc75..0fe93f2ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,6 +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::pyproject_toml::PyProjectToml; pub use crate::python_interpreter::PythonInterpreter; pub use crate::target::Target; @@ -56,6 +57,7 @@ mod cross_compile; mod develop; mod metadata; mod module_writer; +mod new_project; mod pyproject_toml; mod python_interpreter; #[cfg(feature = "upload")] diff --git a/src/main.rs b/src/main.rs index 105ea35ba..6e5ca2d0e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,8 +15,8 @@ use human_panic::setup_panic; #[cfg(feature = "password-storage")] use keyring::{Keyring, KeyringError}; use maturin::{ - develop, source_distribution, write_dist_info, BridgeModel, BuildOptions, CargoToml, - Metadata21, PathWriter, PlatformTag, PyProjectToml, PythonInterpreter, Target, + develop, new_project, source_distribution, write_dist_info, BridgeModel, BuildOptions, + CargoToml, Metadata21, PathWriter, PlatformTag, PyProjectToml, PythonInterpreter, Target, }; use std::env; use std::io; @@ -270,6 +270,19 @@ enum Opt { #[structopt(short, long, parse(from_os_str))] out: Option, }, + /// Create a new cargo project + #[structopt(name = "new")] + NewProject { + /// Project name + #[structopt()] + name: String, + /// Use mixed Rust/Python project layout? + #[structopt(long)] + mixed: bool, + /// Which kind of bindings to use. Possible values are pyo3, rust-cpython, cffi and bin + #[structopt(short, long, possible_values = &["pyo3", "rust-cpython", "cffi", "bin"])] + bindings: Option, + }, /// Uploads python packages to pypi /// /// It is mostly similar to `twine upload`, but can only upload python wheels @@ -621,6 +634,11 @@ fn run() -> Result<()> { .context("Failed to build source distribution")?; } Opt::Pep517(subcommand) => pep517(subcommand)?, + Opt::NewProject { + name, + mixed, + bindings, + } => new_project(name, mixed, bindings)?, #[cfg(feature = "upload")] Opt::Upload { publish, files } => { if files.is_empty() { diff --git a/src/new_project.rs b/src/new_project.rs new file mode 100644 index 000000000..06054b72d --- /dev/null +++ b/src/new_project.rs @@ -0,0 +1,108 @@ +use anyhow::{bail, Result}; +use console::style; +use dialoguer::{theme::ColorfulTheme, Select}; +use fs_err as fs; +use minijinja::{context, Environment}; +use std::path::Path; + +struct ProjectGenerator<'a> { + env: Environment<'a>, + project_name: String, + crate_name: String, + bindings: String, + mixed: bool, +} + +impl<'a> ProjectGenerator<'a> { + fn new(project_name: String, mixed: bool, bindings: String) -> Result { + let crate_name = project_name.replace('-', "_"); + let mut env = Environment::new(); + env.add_template(".gitignore", include_str!("templates/.gitignore"))?; + env.add_template("Cargo.toml", include_str!("templates/Cargo.toml"))?; + env.add_template("pyproject.toml", include_str!("templates/pyproject.toml"))?; + env.add_template("lib.rs", include_str!("templates/lib.rs"))?; + env.add_template("main.rs", include_str!("templates/main.rs"))?; + env.add_template("__init__.py", include_str!("templates/__init__.py"))?; + Ok(Self { + env, + project_name, + crate_name, + bindings, + mixed, + }) + } + + fn generate(&self) -> Result<()> { + let project_path = Path::new(&self.project_name); + if project_path.exists() { + bail!("destination `{}` already exists", project_path.display()); + } + + 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)?; + + if self.bindings == "bin" { + let main_rs = self.render_template("main.rs")?; + fs::write(src_path.join("main.rs"), main_rs)?; + } else { + let lib_rs = self.render_template("lib.rs")?; + fs::write(src_path.join("lib.rs"), lib_rs)?; + } + + 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)?; + } + + println!( + " ✨ {} {} {}", + style("Done!").bold().green(), + style("New project created").bold(), + style(&project_path.display()).underlined() + ); + Ok(()) + } + + fn render_template(&self, tmpl_name: &str) -> Result { + let tmpl = self.env.get_template(tmpl_name)?; + let out = + tmpl.render(context!(name => self.project_name, crate_name => self.crate_name, bindings => self.bindings))?; + Ok(out) + } +} + +/// Generate a new cargo project +pub fn new_project(name: String, mixed: bool, bindings: Option) -> Result<()> { + let bindings_items = if mixed { + vec!["pyo3", "rust-cpython", "cffi"] + } else { + vec!["pyo3", "rust-cpython", "cffi", "bin"] + }; + let bindings = if let Some(bindings) = bindings { + bindings + } else { + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt(format!( + "🤷 {}", + style("What kind of bindings to use?").bold() + )) + .items(&bindings_items) + .default(0) + .interact()?; + bindings_items[selection].to_string() + }; + + let generator = ProjectGenerator::new(name, mixed, bindings)?; + generator.generate() +} diff --git a/src/templates/.gitignore b/src/templates/.gitignore new file mode 100644 index 000000000..c8f044299 --- /dev/null +++ b/src/templates/.gitignore @@ -0,0 +1,72 @@ +/target + +# Byte-compiled / optimized / DLL files +__pycache__/ +.pytest_cache/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +.venv/ +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +include/ +man/ +venv/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +pip-selfcheck.json + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +.DS_Store + +# Sphinx documentation +docs/_build/ + +# PyCharm +.idea/ + +# VSCode +.vscode/ + +# Pyenv +.python-version diff --git a/src/templates/Cargo.toml b/src/templates/Cargo.toml new file mode 100644 index 000000000..e9e632382 --- /dev/null +++ b/src/templates/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "{{ name }}" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +{%- if bindings != "bin" -%} +[lib] +name = "{{ crate_name }}" +crate-type = ["cdylib"] +{%- endif -%} + +[dependencies] +{%- if bindings == "pyo3" -%} +pyo3 = { version = "0.15.1", features = ["extension-module"] } +{%- elif bindings == "rust-cpython" -%} +cpython = { version = "0.7.0", features = ["extension-module"] } +{%- endif -%} diff --git a/src/templates/__init__.py b/src/templates/__init__.py new file mode 100644 index 000000000..145182845 --- /dev/null +++ b/src/templates/__init__.py @@ -0,0 +1,4 @@ +from .{{ crate_name }} import * + + +__doc__ = {{ crate_name }}.__doc__ diff --git a/src/templates/lib.rs b/src/templates/lib.rs new file mode 100644 index 000000000..7b5521b36 --- /dev/null +++ b/src/templates/lib.rs @@ -0,0 +1,15 @@ +{%- if bindings == "pyo3" -%} +use pyo3::prelude::*; + +#[pymodule] +fn {{crate_name}}(_py: Python, m: &PyModule) -> PyResult<()> { + Ok(()) +} +{%- elif bindings == "rust-cpython" -%} +use cpython::py_module_initializer; + +py_module_initializer!({{crate_name}}, |py, m| { + m.add(py, "__doc__", "Module documentation string")?; + Ok(()) +}); +{%- endif %} diff --git a/src/templates/main.rs b/src/templates/main.rs new file mode 100644 index 000000000..e7a11a969 --- /dev/null +++ b/src/templates/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/src/templates/pyproject.toml b/src/templates/pyproject.toml new file mode 100644 index 000000000..51639b3f4 --- /dev/null +++ b/src/templates/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = ["maturin>=0.12,<0.13"] +build-backend = "maturin" + +[project] +name = "{{ name }}" +requires-python = ">=3.6" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +{% if bindings == "cffi" -%} +dependencies = ["cffi"] +{%- endif -%} +{% if bindings == "cffi" or bindings == "bin" -%} +[tool.maturin] +bindings = "{{ bindings }}" +{%- endif -%}