use std::{
fmt, fs,
io::prelude::*,
path,
process::{Command, ExitStatus},
};
use anyhow::{Context, Result};
use clap::{Parser, ValueEnum};
use log::{debug, info};
use miniserde::{Deserialize, Serialize};
#[derive(ValueEnum, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum PackageManager {
Pip,
Pipenv,
Poetry,
}
impl fmt::Display for PackageManager {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "{}", format!("{:?}", self).to_lowercase())
}
}
#[derive(Parser, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct InstallOptions {
#[clap(short, long)]
pub extra_args: Option<String>,
#[clap(short, long, value_enum)]
pub package_manager: PackageManager,
#[clap(short, long)]
pub target: Option<String>,
}
impl InstallOptions {
pub fn new() -> Self {
InstallOptions {
extra_args: None,
package_manager: PackageManager::Pip,
target: None,
}
}
}
impl Default for InstallOptions {
fn default() -> Self {
Self::new()
}
}
pub trait Install {
fn execute(&self) -> Result<()>;
}
fn get_target_dir(target: &Option<String>) -> String {
let working_dir = path::Path::new(".pybuild");
let default_target_path = &working_dir.join("site-packages");
let default_target = default_target_path.to_str().unwrap();
let _target = match target {
Some(target) => target.to_owned(),
None => default_target.to_string(),
};
if !working_dir.exists() {
fs::create_dir_all(working_dir).unwrap();
}
let target_file = &working_dir.join(".target");
let mut file = fs::File::create(target_file).unwrap();
file.write_all(_target.as_bytes()).unwrap();
_target
}
impl Install for InstallOptions {
fn execute(&self) -> Result<()> {
let pkgr = self.package_manager;
let target = get_target_dir(&self.target);
let target = target.as_ref();
let extra_args = self
.extra_args
.as_ref()
.map_or_else(Vec::new, |x| x.split_whitespace().collect());
let requirements_file = "requirements.txt";
let requirements_path = path::Path::new(requirements_file);
match pkgr {
PackageManager::Pip => {
let mut args = vec!["install", "-r", requirements_file, "-t", target];
args.extend_from_slice(&extra_args);
shell_execute(&pkgr.to_string(), &args)?;
}
PackageManager::Pipenv => {
let req_file_args = vec!["lock", "--requirements"];
let _ = shell_stdout_to_file(&pkgr.to_string(), &req_file_args, requirements_path)?;
let mut args = vec!["run", "pip", "install", "-r", requirements_file, "-t", target];
args.extend_from_slice(&extra_args);
shell_execute(&pkgr.to_string(), &args)?;
}
PackageManager::Poetry => {
let req_file_args = vec![
"export",
"-f",
requirements_file,
"--without-hashes",
"--output",
requirements_file,
];
shell_execute(&pkgr.to_string(), &req_file_args)?;
let mut args = vec!["run", "pip", "install", "-r", requirements_file, "-t", target];
args.extend_from_slice(&extra_args);
shell_execute(&pkgr.to_string(), &args)?;
}
}
Ok(())
}
}
fn shell_execute(command: &str, args: &[&str]) -> Result<ExitStatus> {
debug!("cmd: {command}\nargs: {:?}", args);
let output = Command::new(command)
.args(args)
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
info!("stdout: {stdout}");
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
info!("stderr: {stderr}");
Ok(output.status)
}
fn shell_stdout_to_file(command: &str, args: &[&str], requirements_path: &path::Path) -> Result<ExitStatus> {
let output = Command::new(command)
.args(args)
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
info!("{stdout}");
fs::write(requirements_path, stdout)
.with_context(|| format!("Failed to requirements to {:?}", &requirements_path))?;
Ok(output.status)
}