use anyhow::{bail, Context, Result};
#[cfg(feature = "upload")]
use bytesize::ByteSize;
use cargo_metadata::MetadataCommand;
#[cfg(feature = "upload")]
use configparser::ini::Ini;
use fs_err as fs;
#[cfg(feature = "human-panic")]
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,
};
use std::env;
use std::path::PathBuf;
use structopt::StructOpt;
#[cfg(feature = "upload")]
use {
maturin::{upload, Registry, UploadError},
reqwest::Url,
rpassword,
std::io,
};
#[cfg(feature = "upload")]
fn get_password(_username: &str) -> String {
if let Ok(password) = env::var("MATURIN_PASSWORD") {
return password;
};
#[cfg(feature = "keyring")]
{
let service = env!("CARGO_PKG_NAME");
let keyring = Keyring::new(&service, &_username);
if let Ok(password) = keyring.get_password() {
return password;
};
}
rpassword::prompt_password_stdout("Please enter your password: ").unwrap_or_else(|_| {
let mut password = String::new();
io::stdin()
.read_line(&mut password)
.expect("Failed to read line");
password.trim().to_string()
})
}
#[cfg(feature = "upload")]
fn get_username() -> String {
println!("Please enter your username:");
let mut line = String::new();
io::stdin().read_line(&mut line).unwrap();
line.trim().to_string()
}
#[cfg(feature = "upload")]
fn load_pypirc() -> Ini {
let mut config = Ini::new();
if let Some(mut config_path) = dirs::home_dir() {
config_path.push(".pypirc");
if let Ok(pypirc) = fs::read_to_string(config_path.as_path()) {
let _ = config.read(pypirc);
}
}
config
}
#[cfg(feature = "upload")]
fn load_pypi_cred_from_config(config: &Ini, registry_name: &str) -> Option<(String, String)> {
if let (Some(username), Some(password)) = (
config.get(registry_name, "username"),
config.get(registry_name, "password"),
) {
return Some((username, password));
}
None
}
#[cfg(feature = "upload")]
fn resolve_pypi_cred(
opt: &PublishOpt,
config: &Ini,
registry_name: Option<&str>,
) -> (String, String) {
if let Ok(token) = env::var("MATURIN_PYPI_TOKEN") {
return ("__token__".to_string(), token);
}
if let Some((username, password)) =
registry_name.and_then(|name| load_pypi_cred_from_config(&config, name))
{
println!("🔐 Using credential in pypirc for upload");
return (username, password);
}
let username = opt.username.clone().unwrap_or_else(get_username);
let password = match opt.password {
Some(ref password) => password.clone(),
None => get_password(&username),
};
(username, password)
}
#[cfg(feature = "upload")]
fn complete_registry(opt: &PublishOpt) -> Result<Registry> {
let pypirc = load_pypirc();
let (register_name, registry_url) =
if !opt.registry.starts_with("http://") && !opt.registry.starts_with("https://") {
if let Some(url) = pypirc.get(&opt.registry, "repository") {
(Some(opt.registry.as_str()), url)
} else {
bail!(
"Failed to get registry {} in .pypirc. \
Note: Your index didn't start with http:// or https://, \
which is required for non-pypirc indices.",
opt.registry
);
}
} else if opt.registry == "https://upload.pypi.org/legacy/" {
(Some("pypi"), opt.registry.clone())
} else {
(None, opt.registry.clone())
};
let (username, password) = resolve_pypi_cred(opt, &pypirc, register_name);
let registry = Registry::new(username, password, Url::parse(®istry_url)?);
Ok(registry)
}
#[derive(Debug, StructOpt)]
struct PublishOpt {
#[structopt(
short = "r",
long = "repository-url",
default_value = "https://upload.pypi.org/legacy/"
)]
registry: String,
#[structopt(short, long)]
username: Option<String>,
#[structopt(short, long)]
password: Option<String>,
#[structopt(long = "skip-existing")]
skip_existing: bool,
}
#[derive(Debug, StructOpt)]
#[structopt(name = env!("CARGO_PKG_NAME"))]
#[cfg_attr(feature = "cargo-clippy", allow(clippy::large_enum_variant))]
enum Opt {
#[structopt(name = "build")]
Build {
#[structopt(flatten)]
build: BuildOptions,
#[structopt(long)]
release: bool,
#[structopt(long)]
strip: bool,
#[structopt(long = "no-sdist")]
no_sdist: bool,
},
#[cfg(feature = "upload")]
#[structopt(name = "publish")]
Publish {
#[structopt(flatten)]
build: BuildOptions,
#[structopt(long)]
debug: bool,
#[structopt(long = "no-strip")]
no_strip: bool,
#[structopt(long = "no-sdist")]
no_sdist: bool,
#[structopt(flatten)]
publish: PublishOpt,
},
#[structopt(name = "list-python")]
ListPython,
#[structopt(name = "develop")]
Develop {
#[structopt(short = "b", long = "binding-crate")]
binding_crate: Option<String>,
#[structopt(
short = "m",
long = "manifest-path",
parse(from_os_str),
default_value = "Cargo.toml"
)]
manifest_path: PathBuf,
#[structopt(long)]
release: bool,
#[structopt(long)]
strip: bool,
#[structopt(long = "cargo-extra-args")]
cargo_extra_args: Vec<String>,
#[structopt(long = "rustc-extra-args")]
rustc_extra_args: Vec<String>,
},
#[structopt(name = "sdist")]
SDist {
#[structopt(
short = "m",
long = "manifest-path",
parse(from_os_str),
default_value = "Cargo.toml"
)]
manifest_path: PathBuf,
#[structopt(short, long, parse(from_os_str))]
out: Option<PathBuf>,
},
#[cfg(feature = "upload")]
#[structopt(name = "upload")]
Upload {
#[structopt(flatten)]
publish: PublishOpt,
#[structopt(name = "FILE", parse(from_os_str))]
files: Vec<PathBuf>,
},
#[structopt(name = "pep517", setting = structopt::clap::AppSettings::Hidden)]
Pep517(Pep517Command),
}
#[derive(Debug, StructOpt)]
enum Pep517Command {
#[structopt(name = "write-dist-info")]
WriteDistInfo {
#[structopt(flatten)]
build_options: BuildOptions,
#[structopt(long = "metadata-directory", parse(from_os_str))]
metadata_directory: PathBuf,
#[structopt(long)]
strip: bool,
},
#[structopt(name = "build-wheel")]
BuildWheel {
#[structopt(flatten)]
build_options: BuildOptions,
#[structopt(long)]
strip: bool,
},
#[structopt(name = "write-sdist")]
WriteSDist {
#[structopt(long = "sdist-directory", parse(from_os_str))]
sdist_directory: PathBuf,
#[structopt(
short = "m",
long = "manifest-path",
parse(from_os_str),
default_value = "Cargo.toml",
name = "PATH"
)]
manifest_path: PathBuf,
},
}
fn pep517(subcommand: Pep517Command) -> Result<()> {
match subcommand {
Pep517Command::WriteDistInfo {
build_options,
metadata_directory,
strip,
} => {
assert!(matches!(
build_options.interpreter.as_ref(),
Some(version) if version.len() == 1
));
let context = build_options.into_build_context(true, strip)?;
let tags = match context.bridge {
BridgeModel::Bindings(_) => {
vec![context.interpreter[0].get_tag(PlatformTag::Linux, context.universal2)]
}
BridgeModel::BindingsAbi3(major, minor) => {
let platform = context
.target
.get_platform_tag(PlatformTag::Linux, context.universal2);
vec![format!("cp{}{}-abi3-{}", major, minor, platform)]
}
BridgeModel::Bin | BridgeModel::Cffi => {
context
.target
.get_universal_tags(PlatformTag::Linux, context.universal2)
.1
}
};
let mut writer = PathWriter::from_path(metadata_directory);
write_dist_info(&mut writer, &context.metadata21, &tags)?;
println!("{}", context.metadata21.get_dist_info_dir().display());
}
Pep517Command::BuildWheel {
build_options,
strip,
} => {
let build_context = build_options.into_build_context(true, strip)?;
let wheels = build_context.build_wheels()?;
assert_eq!(wheels.len(), 1);
println!("{}", wheels[0].0.to_str().unwrap());
}
Pep517Command::WriteSDist {
sdist_directory,
manifest_path,
} => {
let cargo_toml = CargoToml::from_path(&manifest_path)?;
let manifest_dir = manifest_path.parent().unwrap();
let metadata21 = Metadata21::from_cargo_toml(&cargo_toml, &manifest_dir)
.context("Failed to parse Cargo.toml into python metadata")?;
let cargo_metadata = MetadataCommand::new()
.manifest_path(&manifest_path)
.exec()
.context("Cargo metadata failed. Do you have cargo in your PATH?")?;
let path = source_distribution(
sdist_directory,
&metadata21,
&manifest_path,
&cargo_metadata,
None,
)
.context("Failed to build source distribution")?;
println!("{}", path.file_name().unwrap().to_str().unwrap());
}
};
Ok(())
}
#[cfg(feature = "upload")]
fn upload_ui(items: &[PathBuf], publish: &PublishOpt) -> Result<()> {
let registry = complete_registry(&publish)?;
println!("🚀 Uploading {} packages", items.len());
for i in items {
let upload_result = upload(®istry, &i);
match upload_result {
Ok(()) => (),
Err(UploadError::AuthenticationError) => {
println!("⛔ Username and/or password are wrong");
#[cfg(feature = "keyring")]
{
let old_username = registry.username.clone();
let keyring = Keyring::new(&env!("CARGO_PKG_NAME"), &old_username);
match keyring.delete_password() {
Ok(()) => {
println!("🔑 Removed wrong password from keyring")
}
Err(KeyringError::NoPasswordFound) | Err(KeyringError::NoBackendFound) => {}
Err(err) => eprintln!("⚠ Failed to remove password from keyring: {}", err),
}
}
bail!("Username and/or password are wrong");
}
Err(err) => {
let filename = i.file_name().unwrap_or_else(|| i.as_os_str());
if let UploadError::FileExistsError(_) = err {
if publish.skip_existing {
eprintln!(
"⚠ Skipping {:?} because it appears to already exist",
filename
);
continue;
}
}
let filesize = fs::metadata(&i)
.map(|x| ByteSize(x.len()).to_string())
.unwrap_or_else(|e| format!("Failed to get the filesize of {:?}: {}", &i, e));
return Err(err)
.context(format!("💥 Failed to upload {:?} ({})", filename, filesize));
}
}
}
println!("✨ Packages uploaded successfully");
#[cfg(feature = "keyring")]
{
let username = registry.username.clone();
let keyring = Keyring::new(&env!("CARGO_PKG_NAME"), &username);
let password = registry.password.clone();
match keyring.set_password(&password) {
Ok(()) => {}
Err(KeyringError::NoBackendFound) => {}
Err(err) => {
eprintln!("⚠ Failed to store the password in the keyring: {:?}", err);
}
}
}
Ok(())
}
fn run() -> Result<()> {
#[cfg(feature = "log")]
pretty_env_logger::init();
let opt = Opt::from_args();
match opt {
Opt::Build {
build,
release,
strip,
no_sdist,
} => {
let build_context = build.into_build_context(release, strip)?;
if !no_sdist {
build_context.build_source_distribution()?;
}
build_context.build_wheels()?;
}
#[cfg(feature = "upload")]
Opt::Publish {
build,
publish,
debug,
no_strip,
no_sdist,
} => {
let build_context = build.into_build_context(!debug, !no_strip)?;
if !build_context.release {
eprintln!("⚠ Warning: You're publishing debug wheels");
}
let mut wheels = build_context.build_wheels()?;
if !no_sdist {
if let Some(sd) = build_context.build_source_distribution()? {
wheels.push(sd);
}
}
let items = wheels.into_iter().map(|wheel| wheel.0).collect::<Vec<_>>();
upload_ui(&items, &publish)?
}
Opt::ListPython => {
let target = Target::from_target_triple(None)?;
let found = PythonInterpreter::find_all(&target, &BridgeModel::Cffi, None)?;
println!("🐍 {} python interpreter found:", found.len());
for interpreter in found {
println!(" - {}", interpreter);
}
}
Opt::Develop {
binding_crate,
manifest_path,
cargo_extra_args,
rustc_extra_args,
release,
strip,
} => {
let venv_dir = match (env::var_os("VIRTUAL_ENV"), env::var_os("CONDA_PREFIX")) {
(Some(dir), None) => PathBuf::from(dir),
(None, Some(dir)) => PathBuf::from(dir),
(Some(_), Some(_)) => {
bail!("Both VIRTUAL_ENV and CONDA_PREFIX are set. Please unset one of them")
}
(None, None) => {
bail!(
"You need to be inside a virtualenv or conda environment to use develop \
(neither VIRTUAL_ENV nor CONDA_PREFIX are set). \
See https://virtualenv.pypa.io/en/latest/index.html on how to use virtualenv or \
use `maturin build` and `pip install <path/to/wheel>` instead."
)
}
};
develop(
binding_crate,
&manifest_path,
cargo_extra_args,
rustc_extra_args,
&venv_dir,
release,
strip,
)?;
}
Opt::SDist { manifest_path, out } => {
let manifest_dir = manifest_path.parent().unwrap();
let pyproject = PyProjectToml::new(&manifest_dir)
.context("A pyproject.toml with a PEP 517 compliant `[build-system]` table is required to build a source distribution")?;
let cargo_toml = CargoToml::from_path(&manifest_path)?;
let metadata21 = Metadata21::from_cargo_toml(&cargo_toml, &manifest_dir)
.context("Failed to parse Cargo.toml into python metadata")?;
let cargo_metadata = MetadataCommand::new()
.manifest_path(&manifest_path)
.exec()
.context("Cargo metadata failed. Do you have cargo in your PATH?")?;
let wheel_dir = match out {
Some(ref dir) => dir.clone(),
None => PathBuf::from(&cargo_metadata.target_directory).join("wheels"),
};
fs::create_dir_all(&wheel_dir)
.context("Failed to create the target directory for the source distribution")?;
source_distribution(
&wheel_dir,
&metadata21,
&manifest_path,
&cargo_metadata,
pyproject.sdist_include(),
)
.context("Failed to build source distribution")?;
}
Opt::Pep517(subcommand) => pep517(subcommand)?,
#[cfg(feature = "upload")]
Opt::Upload { publish, files } => {
if files.is_empty() {
println!("⚠ Warning: No files given, exiting.");
return Ok(());
}
upload_ui(&files, &publish)?
}
}
Ok(())
}
fn main() {
#[cfg(feature = "human-panic")]
{
setup_panic!();
}
if let Err(e) = run() {
eprintln!("💥 maturin failed");
for cause in e.chain().collect::<Vec<_>>().iter() {
eprintln!(" Caused by: {}", cause);
}
std::process::exit(1);
}
}