#![allow(dead_code)]
pub mod builders;
pub mod package_database;
use crate::common::builders::{
AddBuilder, InitBuilder, InstallBuilder, ProjectChannelAddBuilder, TaskAddBuilder,
TaskAliasBuilder,
};
use pixi::{
cli::{
add, init,
install::Args,
project, run,
task::{self, AddArgs, AliasArgs},
},
consts, EnvironmentName, ExecutableTask, Project, RunOutput, SearchEnvironments, TaskGraph,
TaskGraphError,
};
use rattler_conda_types::{MatchSpec, Platform};
use miette::{Context, Diagnostic, IntoDiagnostic};
use pixi::cli::run::get_task_env;
use pixi::cli::LockFileUsageArgs;
use pixi::task::TaskName;
use pixi::FeatureName;
use pixi::TaskExecutionError;
use pixi::UpdateLockFileOptions;
use rattler_lock::{LockFile, Package};
use std::{
path::{Path, PathBuf},
process::Output,
str::FromStr,
};
use tempfile::TempDir;
use thiserror::Error;
pub struct PixiControl {
tmpdir: TempDir,
}
pub struct RunResult {
output: Output,
}
impl RunResult {
pub fn success(&self) -> bool {
self.output.status.success()
}
pub fn stdout(&self) -> &str {
std::str::from_utf8(&self.output.stdout).expect("could not get output")
}
}
pub fn string_from_iter(iter: impl IntoIterator<Item = impl AsRef<str>>) -> Vec<String> {
iter.into_iter().map(|s| s.as_ref().to_string()).collect()
}
pub trait LockFileExt {
fn contains_conda_package(&self, environment: &str, platform: Platform, name: &str) -> bool;
fn contains_pypi_package(&self, environment: &str, platform: Platform, name: &str) -> bool;
fn contains_match_spec(
&self,
environment: &str,
platform: Platform,
match_spec: impl IntoMatchSpec,
) -> bool;
fn contains_pep508_requirement(
&self,
environment: &str,
platform: Platform,
requirement: pep508_rs::Requirement,
) -> bool;
}
impl LockFileExt for LockFile {
fn contains_conda_package(&self, environment: &str, platform: Platform, name: &str) -> bool {
let Some(env) = self.environment(environment) else {
return false;
};
let package_found = env
.packages(platform)
.into_iter()
.flatten()
.filter_map(Package::into_conda)
.any(|package| package.package_record().name.as_normalized() == name);
package_found
}
fn contains_pypi_package(&self, environment: &str, platform: Platform, name: &str) -> bool {
let Some(env) = self.environment(environment) else {
return false;
};
let package_found = env
.packages(platform)
.into_iter()
.flatten()
.filter_map(Package::into_pypi)
.any(|pkg| pkg.data().package.name == name);
package_found
}
fn contains_match_spec(
&self,
environment: &str,
platform: Platform,
match_spec: impl IntoMatchSpec,
) -> bool {
let match_spec = match_spec.into();
let Some(env) = self.environment(environment) else {
return false;
};
let package_found = env
.packages(platform)
.into_iter()
.flatten()
.filter_map(Package::into_conda)
.any(move |p| p.satisfies(&match_spec));
package_found
}
fn contains_pep508_requirement(
&self,
environment: &str,
platform: Platform,
requirement: pep508_rs::Requirement,
) -> bool {
let Some(env) = self.environment(environment) else {
return false;
};
let package_found = env
.packages(platform)
.into_iter()
.flatten()
.filter_map(Package::into_pypi)
.any(move |p| p.satisfies(&requirement));
package_found
}
}
impl PixiControl {
pub fn new() -> miette::Result<PixiControl> {
let tempdir = tempfile::tempdir().into_diagnostic()?;
Ok(PixiControl { tmpdir: tempdir })
}
pub fn from_manifest(manifest: &str) -> miette::Result<PixiControl> {
let pixi = Self::new()?;
std::fs::write(&pixi.manifest_path(), manifest)
.into_diagnostic()
.context("failed to write pixi.toml")?;
Ok(pixi)
}
pub fn project(&self) -> miette::Result<Project> {
Project::load_or_else_discover(Some(&self.manifest_path()))
}
pub fn project_path(&self) -> &Path {
self.tmpdir.path()
}
pub fn manifest_path(&self) -> PathBuf {
self.project_path().join(consts::PROJECT_MANIFEST)
}
pub fn init(&self) -> InitBuilder {
InitBuilder {
args: init::Args {
path: self.project_path().to_path_buf(),
channels: None,
platforms: Vec::new(),
env_file: None,
},
}
}
pub fn init_with_platforms(&self, platforms: Vec<String>) -> InitBuilder {
InitBuilder {
args: init::Args {
path: self.project_path().to_path_buf(),
channels: None,
platforms,
env_file: None,
},
}
}
pub fn add(&self, spec: &str) -> AddBuilder {
AddBuilder {
args: add::Args {
manifest_path: Some(self.manifest_path()),
host: false,
specs: vec![spec.to_string()],
build: false,
no_install: true,
no_lockfile_update: false,
platform: Default::default(),
pypi: false,
feature: None,
},
}
}
pub fn project_channel_add(&self) -> ProjectChannelAddBuilder {
ProjectChannelAddBuilder {
manifest_path: Some(self.manifest_path()),
args: project::channel::add::Args {
channel: vec![],
no_install: true,
feature: None,
},
}
}
pub async fn run(&self, mut args: run::Args) -> miette::Result<RunOutput> {
args.manifest_path = args.manifest_path.or_else(|| Some(self.manifest_path()));
let project = self.project()?;
let explicit_environment = args
.environment
.map(|n| EnvironmentName::from_str(n.as_str()))
.transpose()?
.map(|n| {
project
.environment(&n)
.ok_or_else(|| miette::miette!("unknown environment '{n}'"))
})
.transpose()?;
let mut lock_file = project
.up_to_date_lock_file(UpdateLockFileOptions {
lock_file_usage: args.lock_file_usage.into(),
..UpdateLockFileOptions::default()
})
.await?;
let search_env = SearchEnvironments::from_opt_env(
&project,
explicit_environment,
Some(Platform::current()),
);
let task_graph = TaskGraph::from_cmd_args(&project, &search_env, args.task)
.map_err(RunError::TaskGraphError)?;
let mut task_env = None;
let mut result = RunOutput::default();
for task_id in task_graph.topological_order() {
let task = ExecutableTask::from_task_graph(&task_graph, task_id);
let task_env = match task_env.as_ref() {
None => {
let env = get_task_env(&mut lock_file, &task.run_environment).await?;
task_env.insert(env) as &_
}
Some(task_env) => task_env,
};
let output = task.execute_with_pipes(&task_env, None).await?;
result.stdout.push_str(&output.stdout);
result.stderr.push_str(&output.stderr);
result.exit_code = output.exit_code;
if output.exit_code != 0 {
return Err(RunError::NonZeroExitCode(output.exit_code).into());
}
}
return Ok(result);
}
pub fn install(&self) -> InstallBuilder {
InstallBuilder {
args: Args {
environment: None,
manifest_path: Some(self.manifest_path()),
lock_file_usage: LockFileUsageArgs {
frozen: false,
locked: false,
},
},
}
}
pub async fn lock_file(&self) -> miette::Result<LockFile> {
let project = Project::load_or_else_discover(Some(&self.manifest_path()))?;
pixi::load_lock_file(&project).await
}
pub async fn up_to_date_lock_file(&self) -> miette::Result<LockFile> {
let project = self.project()?;
Ok(project
.up_to_date_lock_file(UpdateLockFileOptions::default())
.await?
.lock_file)
}
pub fn tasks(&self) -> TasksControl {
TasksControl { pixi: self }
}
}
pub struct TasksControl<'a> {
pixi: &'a PixiControl,
}
impl TasksControl<'_> {
pub fn add(
&self,
name: TaskName,
platform: Option<Platform>,
feature_name: FeatureName,
) -> TaskAddBuilder {
let feature = feature_name.name().map(|s| s.to_string());
TaskAddBuilder {
manifest_path: Some(self.pixi.manifest_path()),
args: AddArgs {
name,
commands: vec![],
depends_on: None,
platform,
feature,
cwd: None,
},
}
}
pub async fn remove(
&self,
name: TaskName,
platform: Option<Platform>,
feature_name: Option<String>,
) -> miette::Result<()> {
task::execute(task::Args {
manifest_path: Some(self.pixi.manifest_path()),
operation: task::Operation::Remove(task::RemoveArgs {
names: vec![name],
platform,
feature: feature_name,
}),
})
}
pub fn alias(&self, name: TaskName, platform: Option<Platform>) -> TaskAliasBuilder {
TaskAliasBuilder {
manifest_path: Some(self.pixi.manifest_path()),
args: AliasArgs {
platform,
alias: name,
depends_on: vec![],
},
}
}
}
pub trait IntoMatchSpec {
fn into(self) -> MatchSpec;
}
impl IntoMatchSpec for &str {
fn into(self) -> MatchSpec {
MatchSpec::from_str(self).unwrap()
}
}
impl IntoMatchSpec for String {
fn into(self) -> MatchSpec {
MatchSpec::from_str(&self).unwrap()
}
}
impl IntoMatchSpec for MatchSpec {
fn into(self) -> MatchSpec {
self
}
}
#[derive(Error, Debug, Diagnostic)]
enum RunError {
#[error(transparent)]
TaskGraphError(#[from] TaskGraphError),
#[error(transparent)]
ExecutionError(#[from] TaskExecutionError),
#[error("the task executed with a non-zero exit code {0}")]
NonZeroExitCode(i32),
}