use crate::{dep_types::Req, util};
use regex::Regex;
use std::{collections::HashMap, path::Path};
use std::{env, fs, process::Command};
use termcolor::Color;
fn serialize_py_list(items: &[String], indent_level: u8) -> String {
let mut pad = "".to_string();
for _ in 0..indent_level {
pad.push_str(" ");
}
let mut result = "[\n".to_string();
for item in items.iter() {
result.push_str(&format!("{} \"{}\",\n", &pad, item));
}
result.push_str(&pad);
result.push(']');
result
}
fn _serialize_py_dict(hm: &HashMap<String, Vec<String>>) -> String {
let mut result = "{\n".to_string();
for (key, val) in hm.iter() {
result.push_str(&format!(" \"{}\": {}\n", key, serialize_py_list(val, 0)));
}
result.push('}');
result
}
fn cfg_to_setup(cfg: &crate::Config) -> String {
let cfg = cfg.clone();
let version = match cfg.version {
Some(v) => v.to_string(),
None => "".into(),
};
let mut keywords = String::new();
for (i, kw) in cfg.keywords.iter().enumerate() {
if i != 0 {
keywords.push(' ');
}
keywords.push_str(kw);
}
let author_re = Regex::new(r"^(.*?)\s*(?:<(.*?)>)?\s*$").unwrap();
let mut author = "".to_string();
let mut author_email = "".to_string();
if let Some(first) = cfg.authors.get(0) {
let caps = if let Some(c) = author_re.captures(first) {
c
} else {
util::abort(&format!(
"Problem parsing the `authors` field in `pyproject.toml`: {:?}",
&cfg.authors
));
unreachable!()
};
author = caps.get(1).unwrap().as_str().to_owned();
author_email = caps.get(2).unwrap().as_str().to_owned();
}
let deps: Vec<String> = cfg.reqs.iter().map(Req::to_setup_py_string).collect();
format!(
r#"import setuptools
with open("{}", "r") as fh:
long_description = fh.read()
setuptools.setup(
name="{}",
version="{}",
author="{}",
author_email="{}",
license="{}",
description="{}",
long_description=long_description,
long_description_content_type="text/markdown",
url="{}",
packages=setuptools.find_packages(),
keywords="{}",
classifiers={},
python_requires="{}",
install_requires={},
)
"#,
cfg.readme.unwrap_or_else(|| "README.md".into()),
cfg.name.unwrap_or_else(|| "".into()),
version,
author,
author_email,
cfg.license.unwrap_or_else(|| "".into()),
cfg.description.unwrap_or_else(|| "".into()),
cfg.homepage.unwrap_or_else(|| "".into()),
keywords,
serialize_py_list(&cfg.classifiers, 1),
cfg.python_requires.unwrap_or_else(|| "".into()),
serialize_py_list(&deps, 1),
)
}
fn create_dummy_setup(cfg: &crate::Config, filename: &str) {
fs::write(filename, cfg_to_setup(cfg)).expect("Problem writing dummy setup.py");
if util::wait_for_dirs(&[env::current_dir()
.expect("Problem finding current dir")
.join(filename)])
.is_err()
{
util::abort("Problem waiting for setup.py to be created.")
};
}
pub fn build(
lockpacks: &[crate::dep_types::LockPackage],
paths: &util::Paths,
cfg: &crate::Config,
_extras: &[String],
) {
for lp in lockpacks.iter() {
if lp.rename.is_some() {
util::abort(&format!(
"{} is installed with multiple versions. We can't create a package that \
relies on multiple versions of a dependency - \
this would cause this package not work work correctly if not used with pyflow.",
lp.name
))
}
}
let dummy_setup_fname = "setup_temp_pyflow.py";
let output = Command::new(paths.bin.join("python"))
.args(&["-m", "pip", "install", "twine"])
.output()
.expect("Problem installing Twine");
util::check_command_output(&output, "failed to install twine");
create_dummy_setup(cfg, dummy_setup_fname);
util::set_pythonpath(&[paths.lib.to_owned()]);
println!("🛠️️ Building the package...");
if let Some(build_file) = &cfg.build {
let output = Command::new(paths.bin.join("python"))
.arg(&build_file)
.output()
.unwrap_or_else(|_| panic!("Problem building using {}", build_file));
util::check_command_output(&output, "failed to run build script");
}
util::print_color("Build complete.", Color::Green);
if fs::remove_file(dummy_setup_fname).is_err() {
println!("Problem removing temporary setup file while building ")
};
}
pub(crate) fn publish(bin_path: &Path, cfg: &crate::Config) {
let repo_url = match cfg.package_url.clone() {
Some(pu) => {
let mut r = pu;
if !r.ends_with('/') {
r.push('/');
}
r
}
None => "https://test.pypi.org/legacy/".to_string(),
};
println!("Uploading to {}", repo_url);
let output = Command::new(bin_path.join("twine"))
.args(&["upload", "--repository-url", &repo_url, "dist/*"])
.output()
.expect("Problem publishing");
util::check_command_output(&output, "publishing");
}
#[cfg(test)]
pub mod test {
use super::*;
use crate::dep_types::{
Constraint, Req,
ReqType::{Caret, Exact},
Version,
};
#[test]
fn setup_creation() {
let mut scripts = HashMap::new();
scripts.insert("activate".into(), "jeejah:activate".into());
let cfg = crate::Config {
name: Some("everythingkiller".into()),
py_version: Some(Version::new_short(3, 6)),
version: Some(Version::new(0, 1, 0)),
authors: vec!["Fraa Erasmas <raz@edhar.math>".into()],
homepage: Some("https://everything.math".into()),
description: Some("Small, but packs a punch!".into()),
repository: Some("https://github.com/raz/everythingkiller".into()),
license: Some("MIT".into()),
keywords: vec!["nanotech".into(), "weapons".into()],
classifiers: vec![
"Topic :: System :: Hardware".into(),
"Topic :: Scientific/Engineering :: Human Machine Interfaces".into(),
],
python_requires: Some(">=3.6".into()),
package_url: Some("https://upload.pypi.org/legacy/".into()),
scripts,
readme: Some("README.md".into()),
reqs: vec![
Req::new(
"numpy".into(),
vec![Constraint::new(Caret, Version::new(1, 16, 4))],
),
Req::new(
"manimlib".into(),
vec![Constraint::new(Exact, Version::new(0, 1, 8))],
),
Req::new(
"ipython".into(),
vec![Constraint::new(Caret, Version::new(7, 7, 0))],
),
],
dev_reqs: vec![Req::new(
"black".into(),
vec![Constraint::new(Caret, Version::new(18, 0, 0))],
)],
extras: HashMap::new(),
repo_url: None,
build: None,
};
let expected = r#"import setuptools
with open("README.md", "r") as fh:
long_description = fh.read()
setuptools.setup(
name="everythingkiller",
version="0.1.0",
author="Fraa Erasmas",
author_email="raz@edhar.math",
license="MIT",
description="Small, but packs a punch!",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://everything.math",
packages=setuptools.find_packages(),
keywords="nanotech weapons",
classifiers=[
"Topic :: System :: Hardware",
"Topic :: Scientific/Engineering :: Human Machine Interfaces",
],
python_requires=">=3.6",
install_requires=[
"numpy>=1.16.4",
"manimlib==0.1.8",
"ipython>=7.7.0",
],
)
"#;
assert_eq!(expected, &cfg_to_setup(&cfg));
}
#[test]
fn py_list() {
let expected = r#"[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]"#;
let actual = serialize_py_list(
&vec![
"Programming Language :: Python :: 3".into(),
"License :: OSI Approved :: MIT License".into(),
"Operating System :: OS Independent".into(),
],
0,
);
assert_eq!(expected, actual);
}
}