diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c086713f2..c6d633524 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -21,6 +21,8 @@ env: RUSTFLAGS: "-D warnings" CARGO_TERM_COLOR: always CICD_INTERMEDIATES_DIR: "_cicd-intermediates" + TEST_FEATURES: "slow_integration_tests" + XDG_CACHE_HOME: ${{ github.workspace }}/.cache jobs: check-rustdoc-links: @@ -118,7 +120,7 @@ jobs: - name: Setup | Install cargo-wix [Windows] continue-on-error: true # aarch64 is only supported in wix 4.0 development builds - if: matrix.os == 'windows-latest' && matrix.target != 'aarch64-pc-windows-msvc' + if: matrix.job.os == 'windows-latest' && matrix.target != 'aarch64-pc-windows-msvc' run: cargo install --version 0.3.4 cargo-wix env: # cargo-wix does not require static crt @@ -130,6 +132,12 @@ jobs: with: tool: cross + - name: Ensure cache directory exists + shell: bash + if: matrix.job.os == 'ubuntu-20.04' && matrix.job.use-cross + run: | + mkdir -p ${XDG_CACHE_HOME} + - name: Overwrite build command env variable if: matrix.job.use-cross shell: bash @@ -211,6 +219,7 @@ jobs: # test only library unit tests and binary for arm-type targets unset CARGO_TEST_OPTIONS unset CARGO_TEST_OPTIONS ; case ${{ matrix.job.target }} in arm-* | aarch64-*) CARGO_TEST_OPTIONS="--bin ${{ needs.crate_metadata.outputs.name }}" ;; esac; + CARGO_TEST_OPTIONS="${CARGO_TEST_OPTIONS} --features ${{ env.TEST_FEATURES }}" echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT - name: Run tests @@ -333,7 +342,7 @@ jobs: - name: "Artifact upload: windows installer" continue-on-error: true - if: matrix.os == 'windows-latest' && matrix.job.target != 'aarch64-pc-windows-msvc' + if: matrix.job.os == 'windows-latest' && matrix.job.target != 'aarch64-pc-windows-msvc' uses: actions/upload-artifact@v3 with: name: pixi-${{ matrix.job.target }}.msi diff --git a/Cargo.toml b/Cargo.toml index 2fd204940..b967de480 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ readme = "docs/README.md" default = ["native-tls"] native-tls = ["reqwest/native-tls", "rattler_repodata_gateway/native-tls", "rattler/native-tls"] rustls-tls = ["reqwest/rustls-tls", "rattler_repodata_gateway/rustls-tls", "rattler/rustls-tls"] +slow_integration_tests = [] [dependencies] anyhow = "1.0.70" diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 000000000..6c1bca980 --- /dev/null +++ b/Cross.toml @@ -0,0 +1,7 @@ +[target.x86_64-unknown-linux-musl.env] +volumes = [ + "XDG_CACHE_HOME", +] +passthrough = [ + "XDG_CACHE_HOME", +] diff --git a/pixi.toml b/pixi.toml index c306de528..826a53e7f 100644 --- a/pixi.toml +++ b/pixi.toml @@ -1,7 +1,7 @@ [project] name = "pixi" version = "0.0.5" -description = "Package manamgent made easy!" +description = "Package management made easy!" authors = ["Wolf Vollprecht ", "Bas Zalmstra ", "Tim de Jager ", "Ruben Arts "] channels = ["conda-forge"] platforms = ["linux-64", "win-64", "osx-64", "osx-arm64"] diff --git a/src/cli/add.rs b/src/cli/add.rs index 57abf3bc9..3a1033040 100644 --- a/src/cli/add.rs +++ b/src/cli/add.rs @@ -15,7 +15,7 @@ use std::collections::HashMap; use std::path::PathBuf; /// Adds a dependency to the project -#[derive(Parser, Debug)] +#[derive(Parser, Debug, Default)] #[clap(arg_required_else_help = true)] pub struct Args { /// Specify the dependencies you wish to add to the project. @@ -34,11 +34,11 @@ pub struct Args { /// Adding multiple dependencies at once is also supported: /// /// - `pixi add python pytest`: This will add both `python` and `pytest` to the project's dependencies. - specs: Vec, + pub specs: Vec, /// The path to 'pixi.toml' #[arg(long)] - manifest_path: Option, + pub manifest_path: Option, } pub async fn execute(args: Args) -> anyhow::Result<()> { @@ -46,10 +46,15 @@ pub async fn execute(args: Args) -> anyhow::Result<()> { Some(path) => Project::load(path.as_path())?, None => Project::discover()?, }; + add_specs_to_project(&mut project, args.specs).await +} +pub async fn add_specs_to_project( + project: &mut Project, + specs: Vec, +) -> anyhow::Result<()> { // Split the specs into package name and version specifier - let new_specs = args - .specs + let new_specs = specs .into_iter() .map(|spec| match &spec.name { Some(name) => Ok((name.clone(), spec.into())), @@ -119,10 +124,14 @@ pub async fn execute(args: Args) -> anyhow::Result<()> { added_specs.push(spec); } + // TODO why is this only needed in the test? + // project.save()?; + // project.reload()?; + // Update the lock file and write to disk update_lock_file( - &project, - load_lock_file(&project).await?, + project, + load_lock_file(project).await?, Some(sparse_repo_data), ) .await?; @@ -135,7 +144,6 @@ pub async fn execute(args: Args) -> anyhow::Result<()> { spec ); } - Ok(()) } diff --git a/src/cli/init.rs b/src/cli/init.rs index 244bd1d85..f3ae3559a 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -9,7 +9,7 @@ use std::{fs, path::PathBuf}; pub struct Args { /// Where to place the project (defaults to current path) #[arg(default_value = ".")] - path: PathBuf, + pub path: PathBuf, } /// The pixi.toml template diff --git a/src/cli/mod.rs b/src/cli/mod.rs index ee98cb6cf..16cf8fcf8 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -7,13 +7,13 @@ use crate::progress; use anyhow::Error; use tracing_subscriber::{filter::LevelFilter, util::SubscriberInitExt, EnvFilter}; -mod add; -mod auth; -mod global; -mod init; -mod install; -mod run; -mod shell; +pub mod add; +pub mod auth; +pub mod global; +pub mod init; +pub mod install; +pub mod run; +pub mod shell; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -37,7 +37,7 @@ pub struct CompletionCommand { } #[derive(Parser, Debug)] -enum Command { +pub enum Command { Completion(CompletionCommand), Init(init::Args), #[clap(alias = "a")] @@ -89,7 +89,13 @@ pub async fn execute() -> anyhow::Result<()> { .finish() .try_init()?; - match args.command { + // Execute the command + execute_command(args.command).await +} + +/// Execute the actual command +pub async fn execute_command(command: Command) -> Result<(), Error> { + match command { Command::Completion(cmd) => completion(cmd), Command::Init(cmd) => init::execute(cmd).await, Command::Add(cmd) => add::execute(cmd).await, diff --git a/src/cli/run.rs b/src/cli/run.rs index cea2c93f1..afefec8f4 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -1,42 +1,51 @@ use std::collections::{HashSet, VecDeque}; use std::path::Path; +use std::string::String; use std::{fmt::Write, path::PathBuf}; -use crate::Project; use clap::Parser; use is_executable::IsExecutable; use rattler_conda_types::Platform; -use crate::command::{CmdArgs, Command, ProcessCmd}; -use crate::environment::get_up_to_date_prefix; -use crate::project::environment::add_metadata_as_env_vars; -use rattler_shell::activation::ActivationResult; +use crate::{ + command::{CmdArgs, Command, ProcessCmd}, + environment::get_up_to_date_prefix, + project::environment::add_metadata_as_env_vars, + Project, +}; use rattler_shell::{ + activation::ActivationResult, activation::{ActivationVariables, Activator}, shell::{Shell, ShellEnum}, }; /// Runs command in project. -#[derive(Parser, Debug)] +#[derive(Parser, Debug, Default)] #[clap(trailing_var_arg = true, arg_required_else_help = true)] pub struct Args { /// The command you want to run in the projects environment. - command: Vec, + pub command: Vec, /// The path to 'pixi.toml' #[arg(long)] - manifest_path: Option, + pub manifest_path: Option, } -pub async fn execute(args: Args) -> anyhow::Result<()> { +pub struct RunScriptCommand { + /// The command to execute + pub command: std::process::Command, + /// Tempfile to keep a handle on, otherwise it is dropped and deleted + _script: tempfile::NamedTempFile, +} + +pub async fn create_command(args: Args) -> anyhow::Result { + let command: Vec<_> = args.command.iter().map(|c| c.to_string()).collect(); let project = match args.manifest_path { Some(path) => Project::load(path.as_path())?, None => Project::discover()?, }; - // Get the script to execute from the command line. - let (command_name, command) = args - .command + let (command_name, command) = command .first() .and_then(|cmd_name| { project @@ -125,12 +134,19 @@ pub async fn execute(args: Args) -> anyhow::Result<()> { std::io::Write::write_all(&mut temp_file, script.as_bytes())?; // Execute the script with the shell - let mut command = shell - .create_run_script_command(temp_file.path()) - .spawn() - .expect("failed to execute process"); + let command = shell.create_run_script_command(temp_file.path()); - std::process::exit(command.wait()?.code().unwrap_or(1)); + Ok(RunScriptCommand { + command, + _script: temp_file, + }) +} + +/// CLI entry point for `pixi run` +pub async fn execute(args: Args) -> anyhow::Result<()> { + let mut script_command = create_command(args).await?; + let status = script_command.command.spawn()?.wait()?.code().unwrap_or(1); + std::process::exit(status); } /// Given a command and arguments to invoke it, format it so that it is as generalized as possible. diff --git a/src/environment.rs b/src/environment.rs index 4ee19a94b..696847bcb 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -105,6 +105,11 @@ pub async fn get_up_to_date_prefix(project: &Project) -> anyhow::Result Ok(prefix) } +/// Loads the lockfile for the specified project or returns a dummy one if none could be found. +pub async fn load_lock_for_manifest_path(path: &Path) -> anyhow::Result { + load_lock_file(&Project::load(path)?).await +} + /// Loads the lockfile for the specified project or returns a dummy one if none could be found. pub async fn load_lock_file(project: &Project) -> anyhow::Result { let lock_file_path = project.lock_file_path(); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 000000000..60696b1a6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +pub mod cli; +pub mod command; +pub mod config; +pub mod consts; +pub mod environment; +pub mod prefix; +pub mod progress; +pub mod project; +pub mod repodata; +pub mod report_error; +pub mod util; +pub mod virtual_packages; + +pub use project::Project; diff --git a/src/main.rs b/src/main.rs index 7a76b1b80..7dda09902 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,6 @@ use console::style; - -mod cli; -mod command; -mod config; -mod consts; -mod environment; -mod prefix; -mod progress; -mod project; -mod repodata; -mod report_error; -mod util; -mod virtual_packages; - -pub use project::Project; +use pixi::cli; +use pixi::report_error; #[tokio::main] pub async fn main() { diff --git a/src/project/mod.rs b/src/project/mod.rs index 9bbb6658f..e2549a5dd 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -52,6 +52,14 @@ impl Project { }) } + pub fn reload(&mut self) -> anyhow::Result<()> { + let project = Self::load(self.root().join(consts::PROJECT_MANIFEST).as_path())?; + self.root = project.root; + self.doc = project.doc; + self.manifest = project.manifest; + Ok(()) + } + /// Loads a project manifest. pub fn from_manifest_str(root: &Path, contents: impl Into) -> anyhow::Result { let contents = contents.into(); diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 000000000..7c1586504 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,123 @@ +use pixi::cli::run::create_command; +use pixi::cli::{add, init, run}; +use pixi::consts; +use rattler_conda_types::conda_lock::CondaLock; +use rattler_conda_types::{MatchSpec, Version}; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use std::str::FromStr; +use tempfile::TempDir; + +/// To control the pixi process +pub struct PixiControl { + /// The path to the project working file + tmpdir: TempDir, +} + +pub struct RunResult { + output: std::process::Output, +} + +impl RunResult { + /// Was the output successful + pub fn success(&self) -> bool { + self.output.status.success() + } + + /// Get the output + pub fn stdout(&self) -> &str { + std::str::from_utf8(&self.output.stdout).expect("could not get output") + } +} + +/// MatchSpecs from an iterator +pub fn matchspec_from_iter(iter: impl IntoIterator>) -> Vec { + iter.into_iter() + .map(|s| MatchSpec::from_str(s.as_ref()).expect("could not parse matchspec")) + .collect() +} + +/// MatchSpecs from an iterator +pub fn string_from_iter(iter: impl IntoIterator>) -> Vec { + iter.into_iter().map(|s| s.as_ref().to_string()).collect() +} + +pub trait LockFileExt { + /// Check if this package is contained in the lockfile + fn contains_package(&self, name: impl AsRef) -> bool; + /// Check if this matchspec is contained in the lockfile + fn contains_matchspec(&self, matchspec: impl AsRef) -> bool; +} + +impl LockFileExt for CondaLock { + fn contains_package(&self, name: impl AsRef) -> bool { + self.package + .iter() + .any(|locked_dep| locked_dep.name == name.as_ref()) + } + + fn contains_matchspec(&self, matchspec: impl AsRef) -> bool { + let matchspec = MatchSpec::from_str(matchspec.as_ref()).expect("could not parse matchspec"); + let name = matchspec.name.expect("expected matchspec to have a name"); + let version = matchspec + .version + .expect("expected versionspec to have a name"); + self.package + .iter() + .find(|locked_dep| { + let package_version = + Version::from_str(&locked_dep.version).expect("could not parse version"); + locked_dep.name == name && version.matches(&package_version) + }) + .is_some() + } +} + +impl PixiControl { + /// Create a new PixiControl instance + pub fn new() -> anyhow::Result { + let tempdir = tempfile::tempdir()?; + Ok(PixiControl { tmpdir: tempdir }) + } + + /// Get the path to the project + pub fn project_path(&self) -> &Path { + self.tmpdir.path() + } + + pub fn manifest_path(&self) -> PathBuf { + self.project_path().join(consts::PROJECT_MANIFEST) + } + + /// Initialize pixi inside a tempdir and set the tempdir as the current working directory. + pub async fn init(&self) -> anyhow::Result<()> { + let args = init::Args { + path: self.project_path().to_path_buf(), + }; + init::execute(args).await?; + Ok(()) + } + + /// Add a dependency to the project + pub async fn add(&mut self, mut args: add::Args) -> anyhow::Result<()> { + args.manifest_path = Some(self.manifest_path()); + add::execute(args).await + } + + /// Run a command + pub async fn run(&self, mut args: run::Args) -> anyhow::Result { + args.manifest_path = Some(self.manifest_path()); + let mut script_command = create_command(args).await?; + let output = script_command + .command + .stdout(Stdio::piped()) + .spawn()? + .wait_with_output()?; + Ok(RunResult { output }) + } + + /// Get the associated lock file + pub async fn lock_file(&self) -> anyhow::Result { + pixi::environment::load_lock_for_manifest_path(&self.manifest_path()).await + } +} diff --git a/tests/install_tests.rs b/tests/install_tests.rs new file mode 100644 index 000000000..66993e39e --- /dev/null +++ b/tests/install_tests.rs @@ -0,0 +1,35 @@ +mod common; + +use crate::common::{matchspec_from_iter, string_from_iter}; +use common::{LockFileExt, PixiControl}; +use pixi::cli::{add, run}; + +/// Should add a python version to the environment and lock file that matches the specified version +/// and run it +#[tokio::test] +#[cfg_attr(not(feature = "slow_integration_tests"), ignore)] +async fn install_run_python() { + let mut pixi = PixiControl::new().unwrap(); + pixi.init().await.unwrap(); + pixi.add(add::Args { + specs: matchspec_from_iter(["python==3.11.0"]), + ..Default::default() + }) + .await + .unwrap(); + + // Check if lock has python version + let lock = pixi.lock_file().await.unwrap(); + assert!(lock.contains_matchspec("python==3.11.0")); + + // Check if python is installed and can be run + let result = pixi + .run(run::Args { + command: string_from_iter(["python", "--version"]), + ..Default::default() + }) + .await + .unwrap(); + assert!(result.success()); + assert_eq!(result.stdout().trim(), "Python 3.11.0"); +}