use crate::dep_types::{
Constraint, DependencyError, Lock, LockPackage, Package, Rename, Req, ReqType, Version,
};
use crate::util::abort;
use crossterm::{Color, Colored};
use install::PackageType::{Source, Wheel};
use regex::Regex;
use serde::Deserialize;
use std::{collections::HashMap, env, error::Error, fs, io, path::PathBuf, str::FromStr};
use crate::dep_resolution::WarehouseRelease;
use crate::install::PackageType;
use std::io::{BufRead, BufReader};
use std::path::Path;
use structopt::StructOpt;
mod build;
mod commands;
mod dep_resolution;
mod dep_types;
mod files;
mod install;
mod py_versions;
mod util;
type PackToInstall = ((String, Version), Option<(u32, String)>);
#[derive(Copy, Clone, Debug, Deserialize, PartialEq)]
pub enum Os {
Linux32,
Linux,
Windows32,
Windows,
Mac,
Any,
}
impl FromStr for Os {
type Err = dep_types::DependencyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"manylinux1_i686" => Os::Linux32,
"manylinux1_x86_64" => Os::Linux,
"linux" => Os::Linux,
"linux2" => Os::Linux,
"windows" => Os::Windows,
"win" => Os::Windows,
"win32" => Os::Windows32,
"win_amd64" => Os::Windows,
"darwin" => Os::Mac,
"any" => Os::Any,
_ => {
if s.contains("mac") {
Os::Mac
} else {
return Err(DependencyError::new("Problem parsing Os"));
}
}
})
}
}
#[derive(StructOpt, Debug)]
#[structopt(name = "pyflow", about = "Python packaging and publishing")]
struct Opt {
#[structopt(subcommand)]
subcmds: Option<SubCommand>,
#[structopt(name = "script")]
script: Vec<String>,
}
#[derive(StructOpt, Debug)]
enum SubCommand {
#[structopt(name = "new")]
New {
#[structopt(name = "name")]
name: String, },
#[structopt(
name = "install",
help = "
Install packages from `pyproject.toml`, `pyflow.lock`, or speficied ones. Example:
`pyflow install`: sync your installation with `pyproject.toml`, or `pyflow.lock` if it exists.
`pyflow install numpy scipy`: install `numpy` and `scipy`.
"
)]
Install {
#[structopt(name = "packages")]
packages: Vec<String>,
},
#[structopt(name = "uninstall")]
Uninstall {
#[structopt(name = "packages")]
packages: Vec<String>,
},
#[structopt(name = "python")]
Python {
#[structopt(name = "args")]
args: Vec<String>,
},
#[structopt(name = "list")]
List,
#[structopt(name = "package")]
Package {
#[structopt(name = "extras")]
extras: Vec<String>, },
#[structopt(name = "publish")]
Publish,
#[structopt(name = "init")]
Init,
#[structopt(name = "reset")]
Reset,
#[structopt(name = "clear")]
Clear,
#[structopt(name = "run")] Run {
#[structopt(name = "args")]
args: Vec<String>,
},
#[structopt(name = "script")]
Script {
#[structopt(name = "args")]
args: Vec<String>,
},
#[structopt(name = "switch")]
Switch {
#[structopt(name = "version")]
version: String,
},
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct Config {
py_version: Option<Version>,
reqs: Vec<Req>, name: Option<String>,
version: Option<Version>,
author: Option<String>,
author_email: Option<String>,
license: Option<String>,
extras: HashMap<String, String>,
description: Option<String>,
classifiers: Vec<String>, keywords: Vec<String>,
homepage: Option<String>,
repository: Option<String>,
repo_url: Option<String>,
package_url: Option<String>,
readme_filename: Option<String>,
scripts: HashMap<String, String>, }
impl Config {
fn from_file(filename: &str) -> Option<Self> {
let toml_str = match fs::read_to_string(filename) {
Ok(d) => d,
Err(_) => return None,
};
let decoded: files::Pyproject = match toml::from_str(&toml_str) {
Ok(d) => d,
Err(_) => {
abort("Problem parsing `pyproject.toml`");
unreachable!()
}
};
let mut result = Self::default();
if let Some(po) = decoded.tool.poetry {
if let Some(v) = po.name {
result.name = Some(v);
}
if let Some(v) = po.authors {
result.author = Some(v.join(", "));
}
if let Some(v) = po.license {
result.license = Some(v);
}
if let Some(v) = po.homepage {
result.homepage = Some(v);
}
if let Some(v) = po.description {
result.description = Some(v);
}
if let Some(v) = po.repository {
result.repository = Some(v);
}
if let Some(v) = po.classifiers {
result.classifiers = v;
}
if let Some(v) = po.keywords {
result.keywords = v;
}
if let Some(v) = po.extras {
result.extras = v;
}
if let Some(v) = po.version {
result.version = Some(
Version::from_str(&v).expect("Problem parsing version in `pyproject.toml`"),
)
}
if let Some(deps) = po.dependencies {
for (name, data) in deps {
let constraints;
let mut extras = None;
let mut python_version = None;
match data {
files::DepComponentWrapperPoetry::A(constrs) => {
constraints = Constraint::from_str_multiple(&constrs)
.expect("Problem parsing constraints in `pyproject.toml`.");
}
files::DepComponentWrapperPoetry::B(subdata) => {
constraints = Constraint::from_str_multiple(&subdata.constrs)
.expect("Problem parsing constraints in `pyproject.toml`.");
if let Some(ex) = subdata.extras {
extras = Some(ex);
}
if let Some(v) = subdata.python {
python_version = Some(
Constraint::from_str(&v)
.expect("Problem parsing python version in dependency"),
);
}
}
}
if name.to_lowercase() == "python" {
if let Some(constr) = constraints.get(0) {
result.py_version = Some(constr.version)
}
} else {
result.reqs.push(Req {
name,
constraints,
extra: None,
sys_platform: None,
python_version,
install_with_extras: extras,
});
}
}
}
}
if let Some(pp) = decoded.tool.pyflow {
if let Some(v) = pp.name {
result.name = Some(v);
}
if let Some(v) = pp.author {
result.author = Some(v);
}
if let Some(v) = pp.author_email {
result.author_email = Some(v);
}
if let Some(v) = pp.license {
result.license = Some(v);
}
if let Some(v) = pp.homepage {
result.homepage = Some(v);
}
if let Some(v) = pp.description {
result.description = Some(v);
}
if let Some(v) = pp.repository {
result.repository = Some(v);
}
if let Some(v) = pp.classifiers {
result.classifiers = v;
}
if let Some(v) = pp.keywords {
result.keywords = v;
}
if let Some(v) = pp.scripts {
result.scripts = v;
}
if let Some(v) = pp.version {
result.version = Some(
Version::from_str(&v).expect("Problem parsing version in `pyproject.toml`"),
)
}
if let Some(v) = pp.py_version {
result.py_version = Some(
Version::from_str(&v)
.expect("Problem parsing python version in `pyproject.toml`"),
);
}
if let Some(deps) = pp.dependencies {
for (name, data) in deps {
let constraints;
let mut extras = None;
let mut python_version = None;
match data {
files::DepComponentWrapper::A(constrs) => {
constraints = Constraint::from_str_multiple(&constrs)
.expect("Problem parsing constraints in `pyproject.toml`.");
}
files::DepComponentWrapper::B(subdata) => {
constraints = Constraint::from_str_multiple(&subdata.constrs)
.expect("Problem parsing constraints in `pyproject.toml`.");
if let Some(ex) = subdata.extras {
extras = Some(ex);
}
if let Some(v) = subdata.python {
python_version = Some(
Constraint::from_str(&v)
.expect("Problem parsing python version in dependency"),
);
}
}
}
result.reqs.push(Req {
name,
constraints,
extra: None,
sys_platform: None,
python_version,
install_with_extras: extras,
});
}
}
}
Some(result)
}
fn write_file(&self, filename: &str) {
let file = PathBuf::from(filename);
if file.exists() {
abort("`pyproject.toml` already exists")
}
let mut result =
"# See PEP 518: https://www.python.org/dev/peps/pep-0518/ for info on this \
file's structure.\n"
.to_string();
result.push_str("\n[tool.pyflow]\n");
if let Some(name) = &self.name {
result.push_str(&("name = \"".to_owned() + name + "\"\n"));
} else {
result.push_str(&("name = \"\"".to_owned() + "\n"));
}
if let Some(py_v) = &self.py_version {
result.push_str(&("py_version = \"".to_owned() + &py_v.to_string2() + "\"\n"));
} else {
result.push_str(&("py_version = \"3.7\"".to_owned() + "\n"));
}
if let Some(vers) = self.version {
result.push_str(&(format!("version = \"{}\"", vers.to_string2()) + "\n"));
}
if let Some(author) = &self.author {
result.push_str(&(format!("author = \"{}\"", author) + "\n"));
}
if let Some(v) = &self.author_email {
result.push_str(&(format!("author_email = \"{}\"", v) + "\n"));
}
if let Some(v) = &self.description {
result.push_str(&(format!("description = \"{}\"", v) + "\n"));
}
if let Some(v) = &self.homepage {
result.push_str(&(format!("homepage = \"{}\"", v) + "\n"));
}
result.push_str("\n\n");
result.push_str("[tool.pyflow.dependencies]\n\n");
for dep in self.reqs.iter() {
result.push_str(&(dep.to_cfg_string() + "\n"));
}
match fs::write(file, result) {
Ok(_) => util::print_color("Created `pyproject.toml`", Color::Green),
Err(_) => abort("Problem writing `pyproject.toml`"),
}
}
}
pub fn new(name: &str) -> Result<(), Box<dyn Error>> {
if !PathBuf::from(name).exists() {
fs::create_dir_all(&format!("{}/{}", name, name))?;
fs::File::create(&format!("{}/{}/main.py", name, name))?;
fs::File::create(&format!("{}/README.md", name))?;
fs::File::create(&format!("{}/LICENSE", name))?;
fs::File::create(&format!("{}/pyproject.toml", name))?;
fs::File::create(&format!("{}/.gitignore", name))?;
}
let gitignore_init = r##"# General Python ignores
build/
dist/
__pycache__/
__pypackages__/
.ipynb_checkpoints/
*.pyc
*~
*/.mypy_cache/
# Project ignores
"##;
let pyproject_init = &format!(
r##"#See PEP 518: https://www.python.org/dev/peps/pep-0518/ for info on this file's structure.
[tool.pyflow]
name = "{}"
py_version = "3.7"
version = "0.1.0"
description = ""
author = ""
pyackage_url = "https://test.pypi.org"
# pyackage_url = "https://pypi.org"
[tool.pyflow.dependencies]
"##,
name
);
let readme_init = &format!("# {}", name);
fs::write(&format!("{}/.gitignore", name), gitignore_init)?;
fs::write(&format!("{}/pyproject.toml", name), pyproject_init)?;
fs::write(&format!("{}/README.md", name), readme_init)?;
Ok(())
}
fn read_lock(path: &Path) -> Result<(Lock), Box<dyn Error>> {
let data = fs::read_to_string(path)?;
Ok(toml::from_str(&data)?)
}
fn write_lock(path: &Path, data: &Lock) -> Result<(), Box<dyn Error>> {
let data = toml::to_string(data)?;
fs::write(path, data)?;
Ok(())
}
fn os_from_wheel_fname(filename: &str) -> Result<(Os), dep_types::DependencyError> {
let re = Regex::new(r"^(?:.*?-)+(.*).whl$").unwrap();
if let Some(caps) = re.captures(filename) {
let parsed = caps.get(1).unwrap().as_str();
return Ok(
Os::from_str(parsed).unwrap_or_else(|_| panic!("Problem parsing Os: {}", parsed))
);
}
Err(dep_types::DependencyError::new(
"Problem parsing os from wheel name",
))
}
fn parse_lockpack_rename(rename: &str) -> (u32, String) {
let re = Regex::new(r"^(\d+)\s(.*)$").unwrap();
let caps = re
.captures(&rename)
.expect("Problem reading lock file rename");
let id = caps.get(1).unwrap().as_str().parse::<u32>().unwrap();
let name = caps.get(2).unwrap().as_str().to_owned();
(id, name)
}
fn find_best_release(
data: &[WarehouseRelease],
name: &str,
version: &Version,
os: Os,
python_vers: &Version,
) -> (WarehouseRelease, PackageType) {
let mut compatible_releases = vec![];
let mut source_releases = vec![];
for rel in data.iter() {
let mut compatible = true;
match rel.packagetype.as_ref() {
"bdist_wheel" => {
if let Some(py_ver) = &rel.requires_python {
let py_constrs = Constraint::from_str_multiple(&py_ver)
.expect("Problem parsing constraint from requires_python");
for constr in py_constrs.iter() {
if !constr.is_compatible(&python_vers) {
compatible = false;
}
}
}
let wheel_os =
os_from_wheel_fname(&rel.filename).expect("Problem getting os from wheel name");
if wheel_os != os && wheel_os != Os::Any {
compatible = false;
}
match Constraint::from_wh_py_vers(&rel.python_version) {
Ok(constrs) => {
let mut compat_py_v = false;
for constr in constrs.iter() {
if constr.is_compatible(python_vers) {
compat_py_v = true;
}
}
if !compat_py_v {
compatible = false;
}
}
Err(_) => {
(println!(
"Unable to match python version from python_version: {}",
&rel.python_version
))
}
}
if compatible {
compatible_releases.push(rel.clone());
}
}
"sdist" => source_releases.push(rel.clone()),
"bdist_egg" => println!("Found bdist_egg... skipping"),
"bdist_wininst" => (), "bdist_msi" => (), _ => {
println!("Found surprising package type: {}", rel.packagetype);
continue;
}
}
}
let best_release;
let package_type;
if compatible_releases.is_empty() {
if source_releases.is_empty() {
abort(&format!(
"Unable to find a compatible release for {}: {}",
name,
version.to_string()
));
unreachable!()
} else {
best_release = source_releases[0].clone();
package_type = Source;
}
} else {
best_release = compatible_releases[0].clone();
package_type = Wheel;
}
(best_release, package_type)
}
fn sync_deps(
bin_path: &Path,
lib_path: &Path,
cache_path: &Path,
lock_packs: &[LockPackage],
installed: &[(String, Version, Vec<String>)],
os: Os,
python_vers: &Version,
) {
let packages: Vec<PackToInstall> = lock_packs
.iter()
.map(|lp| {
(
(
util::standardize_name(&lp.name),
Version::from_str(&lp.version).expect("Problem parsing lock version"),
),
match &lp.rename {
Some(rn) => Some(parse_lockpack_rename(&rn)),
None => None,
},
)
})
.collect();
let installed: Vec<(String, Version)> = installed
.iter()
.map(|t| (util::standardize_name(&t.0), t.1))
.collect();
let to_install: Vec<&PackToInstall> = packages
.iter()
.filter(|(pack, _)| !installed.contains(pack))
.collect();
let packages_only: Vec<&(String, Version)> = packages.iter().map(|(p, _)| p).collect();
let to_uninstall: Vec<&(String, Version)> = installed
.iter()
.filter(|inst| {
let inst = (util::standardize_name(&inst.0), inst.1);
!packages_only.contains(&&inst)
})
.collect();
for (name, version) in to_uninstall.iter() {
install::uninstall(name, version, lib_path)
}
for ((name, version), rename) in to_install.iter() {
let data = dep_resolution::get_warehouse_release(&name, &version)
.expect("Problem getting warehouse data");
let (best_release, package_type) =
find_best_release(&data, &name, &version, os, python_vers);
println!(
"⬇️ Installing {}{}{} {} ...",
Colored::Fg(Color::Cyan),
&name,
Colored::Fg(Color::Reset),
&version
);
if install::download_and_install_package(
&name,
&version,
&best_release.url,
&best_release.filename,
&best_release.digests.sha256,
lib_path,
bin_path,
cache_path,
package_type,
rename,
)
.is_err()
{
abort("Problem downloading packages");
}
}
for ((name, version), rename) in to_install.iter() {
if let Some((id, new)) = rename {
install::rename_package_files(&lib_path.join(util::standardize_name(new)), name, &new);
let parent = lock_packs
.iter()
.find(|lp| lp.id == *id)
.expect("Can't find parent calling renamed package");
install::rename_package_files(
&lib_path.join(util::standardize_name(&parent.name)),
name,
&new,
);
install::rename_metadata(
&lib_path.join(&format!("{}-{}.dist-info", name, version.to_string2())),
name,
&new,
);
}
}
}
fn already_locked(locked: &[Package], name: &str, constraints: &[Constraint]) -> bool {
let mut result = true;
for constr in constraints.iter() {
if !locked
.iter()
.any(|p| util::compare_names(&p.name, name) && constr.is_compatible(&p.version))
{
result = false;
break;
}
}
result
}
fn run_cli_tool(
lib_path: &Path,
bin_path: &Path,
vers_path: &Path,
cfg: &Config,
args: Vec<String>,
) {
if args.is_empty() {
return;
}
let name = match args.get(0) {
Some(a) => a.clone(),
None => {
abort("`run` must be followed by the script to run, eg `pyflow run black`");
unreachable!()
}
};
let re = Regex::new(r"(.*?):(.*)").unwrap();
let mut specified_args: Vec<String> = args.into_iter().skip(1).collect();
if let Some(s) = cfg.scripts.get(&name) {
let abort_msg = format!(
"Problem running the script {}, specified in `pyproject.toml`",
name,
);
match re.captures(s) {
Some(caps) => {
let module = caps.get(1).unwrap().as_str();
let function = caps.get(2).unwrap().as_str();
let mut args_to_pass = vec![
"-c".to_owned(),
format!(r#""import {}; {}.{}()""#, module, module, function),
];
args_to_pass.append(&mut specified_args);
if commands::run_python(&bin_path, &lib_path, &args_to_pass).is_err() {
abort(&abort_msg);
}
}
None => {
abort(&format!("Problem parsing the following script: {:#?}. Must be in the format module:function_name", s));
unreachable!()
}
}
return;
}
let abort_msg = format!(
"Problem running the script {}. Is it installed? \
Try running `pyflow install {}`",
name, name
);
let script_path = vers_path.join("bin").join(name);
if !script_path.exists() {
abort(&abort_msg);
}
let mut args_to_pass = vec![script_path
.to_str()
.expect("Can't find script path")
.to_owned()];
args_to_pass.append(&mut specified_args);
if commands::run_python(&bin_path, &lib_path, &args_to_pass).is_err() {
abort(&abort_msg);
}
}
fn find_deps_from_script(file_path: &Path) -> Vec<String> {
let f = fs::File::open(file_path).expect("Problem opening the Python script file.");
let re = Regex::new(r"^__requires__\s*=\s*\[(.*?)\]$").unwrap();
let mut result = vec![];
for line in BufReader::new(f).lines() {
if let Ok(l) = line {
if let Some(c) = re.captures(&l) {
let deps_list = c.get(1).unwrap().as_str().to_owned();
let deps: Vec<&str> = deps_list.split(',').collect();
result = deps
.into_iter()
.map(|d| {
d.to_owned()
.replace(" ", "")
.replace("\"", "")
.replace("'", "")
})
.collect();
}
}
}
result
}
fn run_script(script_env_path: &Path, cache_path: &Path, os: Os, args: &mut Vec<String>) {
let filename = match args.get(0) {
Some(a) => a.clone(),
None => {
abort("`run` must be followed by the script to run, eg `pyflow script myscript.py`");
unreachable!()
}
};
let filename = util::standardize_name(&filename);
let env_path = script_env_path.join(&filename);
if !env_path.exists() {
fs::create_dir_all(&env_path).expect("Problem creating environment for the script");
}
let cfg_vers;
let py_vers_path = env_path.join("py_vers.txt");
if py_vers_path.exists() {
cfg_vers = Version::from_str(
&fs::read_to_string(py_vers_path)
.expect("Problem reading Python version for this script")
.replace("\n", ""),
)
.expect("Problem parsing version from file");
} else {
cfg_vers = {
util::print_color(
"Please enter the Python version for this project:",
Color::Magenta,
);
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("Unable to read user input for version");
input.pop();
match Version::from_str(&input) {
Ok(v) => v,
Err(_) => {
util::abort("Problem parsing the Python version you entered. It should look like this: 3.7 or 3.7.1");
unreachable!()
}
}
};
fs::File::create(&py_vers_path)
.expect("Problem creating a file to store the Python version for this script");
fs::write(py_vers_path, &cfg_vers.to_string2())
.expect("Problem writing Python version file.");
}
let pyflows_dir = env_path.join("__pypackages__");
let (vers_path, py_vers) = util::find_venv_info(&cfg_vers, &pyflows_dir);
let bin_path = vers_path.join(".venv").join("bin");
let lib_path = vers_path.join("lib");
let lock_path = env_path.join("pyproject.lock");
let deps = find_deps_from_script(&PathBuf::from(&filename));
let lock = match read_lock(&lock_path) {
Ok(l) => l,
Err(_) => Lock::default(),
};
let lockpacks = lock.package.unwrap_or_else(|| vec![]);
let reqs: Vec<Req> = deps
.iter()
.map(|name| {
let (fmtd_name, version) = match lockpacks
.iter()
.find(|lp| util::compare_names(&lp.name, name))
{
Some(lp) => (
lp.name.clone(),
Version::from_str(&lp.version).expect("Problem getting version"),
),
None => {
let vinfo = dep_resolution::get_version_info(&name)
.unwrap_or_else(|_| panic!("Problem getting version info for {}", &name));
(vinfo.0, vinfo.1)
}
};
Req::new(
fmtd_name.clone(),
vec![Constraint::new(ReqType::Caret, version)],
)
})
.collect();
sync(
&bin_path,
&lib_path,
&cache_path,
&lockpacks,
&reqs,
os,
&py_vers,
&lock_path,
);
if commands::run_python(&bin_path, &lib_path, args).is_err() {
abort("Problem running this script")
};
}
fn sync(
bin_path: &Path,
lib_path: &Path,
cache_path: &Path,
lockpacks: &[LockPackage],
reqs: &[Req],
os: Os,
py_vers: &Version,
lock_path: &Path,
) {
let installed = util::find_installed(&lib_path);
let dep_re = Regex::new(r"^(.*?)\s(.*)\s.*$").unwrap();
let locked: Vec<Package> = lockpacks
.iter()
.map(|lp| {
let mut deps = vec![];
for dep in lp.dependencies.as_ref().unwrap_or(&vec![]) {
let caps = dep_re
.captures(&dep)
.expect("Problem reading lock file dependencies");
let name = caps.get(1).unwrap().as_str().to_owned();
let vers = Version::from_str(caps.get(2).unwrap().as_str())
.expect("Problem parsing version from lock");
deps.push((999, name, vers)); }
Package {
id: lp.id, parent: 0, name: lp.name.clone(),
version: Version::from_str(&lp.version).expect("Problem parsing lock version"),
deps,
rename: Rename::No, }
})
.collect();
#[cfg(target_os = "windows")]
println!("Resolving dependencies...");
#[cfg(target_os = "linux")]
println!("🔍 Resolving dependencies...");
#[cfg(target_os = "macos")]
println!("🔍 Resolving dependencies...");
let resolved = match dep_resolution::resolve(&reqs, &locked, os, &py_vers) {
Ok(r) => r,
Err(_) => {
abort("Problem resolving dependencies");
unreachable!()
}
};
let mut updated_lock_packs = vec![];
for package in resolved.iter() {
let dummy_constraints = vec![Constraint::new(ReqType::Exact, package.version)];
if already_locked(&locked, &package.name, &dummy_constraints) {
let existing: Vec<&LockPackage> = lockpacks
.iter()
.filter(|lp| util::compare_names(&lp.name, &package.name))
.collect();
let existing2 = existing[0];
updated_lock_packs.push(existing2.clone());
continue;
}
let deps = package
.deps
.iter()
.map(|(_, name, version)| {
format!(
"{} {} pypi+https://pypi.org/pypi/{}/{}/json",
name,
version.to_string2(),
name,
version.to_string2(),
)
})
.collect();
updated_lock_packs.push(LockPackage {
id: package.id,
name: package.name.clone(),
version: package.version.to_string(),
source: Some(format!(
"pypi+https://pypi.org/pypi/{}/{}/json",
package.name,
package.version.to_string()
)),
dependencies: Some(deps),
rename: match &package.rename {
Rename::Yes(parent_id, _, name) => Some(format!("{} {}", parent_id, name)),
Rename::No => None,
},
});
}
let updated_lock = Lock {
metadata: HashMap::new(), package: Some(updated_lock_packs.clone()),
};
if write_lock(lock_path, &updated_lock).is_err() {
abort("Problem writing lock file");
}
sync_deps(
&bin_path,
&lib_path,
&cache_path,
&updated_lock_packs,
&installed,
os,
&py_vers,
);
}
fn main() {
let cfg_filename = "pyproject.toml";
let lock_filename = "pyflow.lock";
let python_installs_dir = dirs::home_dir()
.expect("Problem finding home directory")
.join(".python-installs");
let cache_path = python_installs_dir.join("dependency-cache");
let script_env_path = python_installs_dir.join("script-envs");
#[cfg(target_os = "windows")]
let os = Os::Windows;
#[cfg(target_os = "linux")]
let os = Os::Linux;
#[cfg(target_os = "macos")]
let os = Os::Mac;
let opt = Opt::from_args();
let subcmd = match opt.subcmds {
Some(sc) => sc,
None => SubCommand::Run { args: opt.script },
};
if let SubCommand::Script { mut args } = subcmd {
run_script(&script_env_path, &cache_path, os, &mut args);
return;
}
let pypackages_dir = env::current_dir()
.expect("Can't find current path")
.join("__pypackages__");
let mut cfg = Config::from_file(cfg_filename).unwrap_or_default();
match subcmd {
SubCommand::New { name } => {
new(&name).expect("Problem creating project");
util::print_color(
&format!("Created a new Python project named {}", name),
Color::Green,
);
return;
}
SubCommand::Init {} => {
files::parse_req_dot_text(&mut cfg);
files::parse_pipfile(&mut cfg);
if PathBuf::from(cfg_filename).exists() {
abort("pyproject.toml already exists - not overwriting.")
}
cfg.write_file(cfg_filename);
}
SubCommand::Reset {} => {
if pypackages_dir.exists() && fs::remove_dir_all(&pypackages_dir).is_err() {
abort("Problem removing `__pypackages__` directory")
}
if Path::new(lock_filename).exists() && fs::remove_file(lock_filename).is_err() {
abort("Problem removing `pyflow.lock`")
}
util::print_color("Reset complete", Color::Green);
return;
}
SubCommand::Switch { version } => {
let specified = match Version::from_str(&version) {
Ok(v) => v,
Err(_) => {
util::abort("Problem parsing the Python version you entered. It should look like this: 3.7 or 3.7.1");
unreachable!()
}
};
files::change_py_vers(&PathBuf::from(&cfg_filename), &specified);
util::print_color(
&format!(
"Switched to Python version {}.{}",
&specified.major, &specified.minor
),
Color::Green,
);
return;
}
SubCommand::Clear {} => {
util::wipe_dir(&cache_path);
util::wipe_dir(&script_env_path);
}
_ => (),
}
let cfg_vers = match cfg.py_version {
Some(v) => v,
None => {
util::print_color(
"Please enter the Python version for this project:",
Color::Magenta,
);
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("Unable to read user input for version");
input.pop();
let specified = match Version::from_str(&input) {
Ok(v) => v,
Err(_) => {
util::abort("Problem parsing the Python version you entered. It should look like this: 3.7 or 3.7.1");
unreachable!()
}
};
if !PathBuf::from(cfg_filename).exists() {
cfg.write_file(cfg_filename);
}
files::change_py_vers(&PathBuf::from(&cfg_filename), &specified);
specified
}
};
let (vers_path, py_vers) = util::find_venv_info(&cfg_vers, &pypackages_dir);
let lib_path = vers_path.join("lib");
let bin_path = util::find_bin_path(&vers_path);
let mut found_lock = false;
let lock = match read_lock(&PathBuf::from(lock_filename)) {
Ok(l) => {
found_lock = true;
l
}
Err(_) => Lock::default(),
};
let lockpacks = lock.package.unwrap_or_else(|| vec![]);
match subcmd {
SubCommand::Install { packages } => {
if !PathBuf::from(cfg_filename).exists() {
cfg.write_file(cfg_filename);
}
if found_lock {
println!("Found lockfile");
}
let updated_reqs = util::merge_reqs(&packages, &cfg, cfg_filename);
sync(
&bin_path,
&lib_path,
&cache_path,
&lockpacks,
&updated_reqs,
os,
&py_vers,
&PathBuf::from(lock_filename),
);
util::print_color("Installation complete", Color::Green);
}
SubCommand::Uninstall { packages } => {
let removed_reqs: Vec<String> = packages
.into_iter()
.map(|p| {
Req::from_str(&p, false)
.expect("Problem parsing req while uninstalling")
.name
})
.collect();
println!("(dbg) to remove {:#?}", &removed_reqs);
files::remove_reqs_from_cfg(cfg_filename, &removed_reqs);
let updated_reqs: Vec<Req> = cfg
.reqs
.into_iter()
.filter(|req| !removed_reqs.contains(&req.name))
.collect();
sync(
&bin_path,
&lib_path,
&cache_path,
&lockpacks,
&updated_reqs,
os,
&py_vers,
&PathBuf::from(lock_filename),
);
util::print_color("Uninstall complete", Color::Green);
}
SubCommand::Python { args } => {
if commands::run_python(&bin_path, &lib_path, &args).is_err() {
abort("Problem running Python");
}
}
SubCommand::Package { extras } => {
build::build(&lockpacks, &bin_path, &lib_path, &cfg, extras)
}
SubCommand::Publish {} => build::publish(&bin_path, &cfg),
SubCommand::Run { args } => {
run_cli_tool(&lib_path, &bin_path, &vers_path, &cfg, args);
}
SubCommand::List {} => util::show_installed(&lib_path),
_ => (),
}
}
#[cfg(test)]
pub mod tests {}