Skip to content

Commit

Permalink
Add a maturin new command for bootstrapping new projects
Browse files Browse the repository at this point in the history
  • Loading branch information
messense committed Nov 25, 2021
1 parent cd973c1 commit d534858
Show file tree
Hide file tree
Showing 13 changed files with 331 additions and 2 deletions.
61 changes: 61 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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 a `maturin new` command for bootstrapping new projects in [#705](https://github.com/PyO3/maturin/pull/705)

## [0.12.1] - 2021-11-21

Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ bindings = "bin"

[tool.black]
target_version = ['py36']
extend-exclude = '''
# Ignore cargo-generate templates
^/src/templates
'''
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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")]
Expand Down
22 changes: 20 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -270,6 +270,19 @@ enum Opt {
#[structopt(short, long, parse(from_os_str))]
out: Option<PathBuf>,
},
/// 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<String>,
},
/// Uploads python packages to pypi
///
/// It is mostly similar to `twine upload`, but can only upload python wheels
Expand Down Expand Up @@ -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() {
Expand Down
108 changes: 108 additions & 0 deletions src/new_project.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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<String> {
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<String>) -> 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()
}
72 changes: 72 additions & 0 deletions src/templates/.gitignore
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit d534858

Please sign in to comment.