#![warn(clippy::all)]
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fs,
io::{self, Write},
path::{Path, PathBuf},
process::Command,
};
use toml::Value;
mod cli;
#[derive(Debug, Serialize, Deserialize)]
struct CargoToml {
package: HashMap<String, Value>,
#[serde(default = "HashMap::new")]
dependencies: HashMap<String, Value>,
#[serde(default = "HashMap::new")]
lib: HashMap<String, Value>,
}
impl CargoToml {
pub fn add_dependencies(mut self, dependencies: &HashMap<String, String>) -> Self {
for (dependency, version) in dependencies {
self.dependencies
.insert(dependency.into(), Value::String(version.into()));
}
self
}
pub fn add_library_config(mut self, name: &str) -> Self {
self.lib.insert("name".into(), Value::String(name.into()));
self.lib.insert(
"crate-type".into(),
Value::Array(vec![Value::String("cdylib".into())]),
);
self
}
pub fn add_package_info(
mut self,
author_name: &str,
author_email: &str,
version: &str,
) -> Self {
self.package.insert(
"authors".into(),
Value::Array(vec![Value::String(format!(
"{} <{}>",
author_name, author_email
))]),
);
self.package.insert("version".into(), version.into());
self
}
}
fn run_and_get_output(prog: &str, args: &Vec<&str>) -> Result<String, std::io::Error> {
let output = Command::new(prog).args(args).output()?.stdout;
Ok(String::from_utf8_lossy(&output).into())
}
fn get_value(
value: Option<&str>,
default: Option<&str>,
prompt: &str,
non_interactive: bool,
) -> std::io::Result<String> {
if let Some(v) = value {
return Ok(v.into());
}
if non_interactive {
if let Some(v) = default {
return Ok(v.into());
} else {
panic!("no value or default available");
}
}
let mut input = String::new();
loop {
if let Some(v) = default {
print!("{} [{}]: ", prompt, v);
} else {
print!("{}", prompt);
}
io::stdout().flush()?;
io::stdin().read_line(&mut input)?;
input = input.trim_end().into();
if input.is_empty() {
if let Some(v) = default {
return Ok(v.into());
} else {
continue;
}
} else {
return Ok(input);
}
}
}
fn main() -> Result<(), std::io::Error> {
let app = cli::get_app().get_matches();
let path = Path::new(app.value_of("path").unwrap());
if fs::metadata(path).is_ok() {
println!("Target path already exists, exiting...");
std::process::exit(1);
}
let non_interactive = app.is_present("non_interactive");
let project_name = get_value(
app.value_of("name"),
path.file_name()
.expect("could not get project name from path")
.to_str(),
"Project name",
non_interactive,
)?
.replace('-', "_")
.replace(' ', "_");
let description = get_value(
app.value_of("description"),
Some("Generated with pyo3-setup"),
"Project description",
non_interactive,
)?;
let project_version = get_value(
app.value_of("version"),
Some("0.1.0"),
"Project version",
non_interactive,
)?;
let author = get_value(
app.value_of("author"),
Some(run_and_get_output("git", &vec!["config", "user.name"])?.trim()),
"Author name",
non_interactive,
)?;
let email = get_value(
app.value_of("email"),
Some(run_and_get_output("git", &vec!["config", "user.email"])?.trim()),
"Author email",
non_interactive,
)?;
let pyo3_version = if let Some(version) = app.value_of("pyo3_version") {
version.to_string()
} else {
use cargo_edit::get_latest_dependency;
get_latest_dependency("pyo3", false, path, &None)
.expect("could not query for latest version of PyO3")
.version()
.unwrap()
.into()
};
let setuptools_version = app.value_of("setuptools_version").unwrap();
let setuptools_rust_version = app.value_of("setuptools_rust_version").unwrap();
let mut dependencies = HashMap::<String, String>::new();
dependencies.insert("pyo3".into(), pyo3_version.clone());
println!("Creating project directory...");
fs::create_dir_all(path)?;
println!("Initializing with Cargo...");
Command::new("cargo")
.arg("init")
.arg("--name")
.arg(&project_name)
.arg("--lib")
.arg(&format!("{}", path.display()))
.spawn()?
.wait()?;
println!("Updating Cargo configuration...");
println!("Cargo dependencies:");
println!("PyO3: {}", &pyo3_version);
let cargo_toml_path = PathBuf::from(format!("{}/Cargo.toml", path.display()));
let requirements_txt_path = PathBuf::from(format!("{}/requirements.txt", path.display()));
let manifest_in_path = PathBuf::from(format!("{}/MANIFEST.in", path.display()));
let pyproject_path = PathBuf::from(format!("{}/pyproject.toml", path.display()));
let setup_py_path = PathBuf::from(format!("{}/setup.py", path.display()));
let py_module_path = PathBuf::from(format!("{}/{}", path.display(), project_name));
let readme_path = PathBuf::from(format!("{}/README.md", path.display()));
let lib_rs_path = PathBuf::from(format!("{}/src/lib.rs", path.display()));
let gitignore_path = PathBuf::from(format!("{}/.gitignore", path.display()));
{
let cargo_toml_raw = fs::read_to_string(&cargo_toml_path)?;
let cargo_toml = {
toml::from_str::<CargoToml>(&cargo_toml_raw)
.expect("could not parse Cargo.toml")
.add_dependencies(&dependencies)
.add_library_config(&project_name)
.add_package_info(&author, &email, &project_version)
};
fs::write(&cargo_toml_path, toml::to_string(&cargo_toml).unwrap())?;
println!("Wrote updated Cargo.toml");
}
{
println!("Updating lib.rs...");
let lib_rs = format!(
r#"//! {description}
use pyo3::prelude::*;
#[pymodule]
fn {name}(_py: Python, m: &PyModule) -> PyResult<()> {{
// Add stuff to your module here
// Info available in the PyO3 docs
Ok(())
}}
"#,
name = project_name,
description = description,
);
fs::write(&lib_rs_path, &lib_rs)?;
}
{
println!("Creating default Python package...");
fs::create_dir_all(&py_module_path)?;
let init_py = format!(
concat!(r#""""Main {name} module""""#, "\nfrom .{name} import *\n"),
name = project_name
);
fs::write(
&format!("{}/__init__.py", py_module_path.display()),
&init_py,
)?;
}
{
println!("Creating requirements.txt (for building from source)...");
println!("Python dependencies:");
println!("setuptools: {}", &setuptools_version);
println!("setuptools-rust: {}", &setuptools_rust_version);
let requirements = format!(
"# These requirements are for building the package from source\n\
setuptools{}\n\
setuptools-rust{}\n",
setuptools_version, setuptools_rust_version
);
fs::write(&requirements_txt_path, &requirements)?;
}
{
println!("Creating MAINFEST.in...");
let manifest = "include Cargo.toml\n\
recursive-include src *\n";
fs::write(&manifest_in_path, &manifest)?;
}
{
println!("Creating pyproject.toml...");
let pyproject = r#"[build-system]
requires = ["setuptools", "wheel", "setuptools-rust"]
"#;
fs::write(&pyproject_path, &pyproject)?;
}
{
println!("Creating setup.py...");
let setup_py = format!(
r#"from setuptools import setup
from setuptools_rust import Binding, RustExtension
with open("README.md", "r") as f:
long_description = f.read()
setup(
name="{name}",
version="{version}",
author="{author}",
author_email="{author_email}",
description="{description}",
long_description=long_description,
long_description_content_type="text/markdown",
# license="MIT OR Apache-2.0",
# license_files=("LICENSE-MIT", "LICENSE-APACHE"),
rust_extensions=[RustExtension("{name}.{name}", binding=Binding.PyO3)],
packages=["{name}"],
classifiers=[
"Intended Audience :: Developers",
# "License :: OSI Approved :: Apache Software License",
# "License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Rust",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
],
python_requires="~=3.6",
zip_safe=False,
)
"#,
name = project_name,
description = description,
version = project_version,
author = author,
author_email = email,
);
fs::write(&setup_py_path, &setup_py)?;
}
{
println!("Creating README.md...");
let readme = format!(
r#"# {name}
{description}
## Installation
To install, run:
```sh
$ pip install {name}
```
## Building from source
[Rust](https://www.rust-lang.org/learn/get-started) is required to build this project from source.
Python dependencies can be installed with `pip install -r requirements.txt`.
To build for development:
```sh
$ python setup.py develop
```
To build for publishing to PyPI:
```sh
$ python setup.py sdist bdist_wheel
```
To install for use in other projects:
```sh
$ python setup.py install
```
"#,
name = project_name,
description = description,
);
fs::write(&readme_path, &readme)?;
}
{
println!("Creating .gitignore...");
let gitignore = r#"# python files
*.py[c,d]
/*.egg-info/
/build/
/dist/
# rust files
/target/
Cargo.lock
"#;
fs::write(&gitignore_path, &gitignore)?;
}
println!("Done!");
println!("Happy hacking :)");
Ok(())
}