diff options
author | Ruben Arts <ruben.arts@hotmail.com> | 2023-09-28 12:56:17 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-28 12:56:17 +0200 |
commit | e2550d14fe3c90f94c3b72bdb11e099c1e865e32 (patch) | |
tree | ac6365c8de835401a144a7f9d397c26fc0184fc2 | |
parent | e7807bc3a67ad2a72ba225a32c87f8c3ab3e1c8b (diff) |
feat: add --locked and --frozen to getting an up-to-date prefix (#363)
Closes #349
Add the `--frozen` and `--locked` like how cargo does it.
Allow users to force CI to not touch the lock file.
-rw-r--r-- | docs/cli.mdx | 14 | ||||
-rw-r--r-- | src/cli/add.rs | 50 | ||||
-rw-r--r-- | src/cli/install.rs | 10 | ||||
-rw-r--r-- | src/cli/run.rs | 18 | ||||
-rw-r--r-- | src/cli/shell.rs | 18 | ||||
-rw-r--r-- | src/environment.rs | 20 | ||||
-rw-r--r-- | tests/common/builders.rs | 38 | ||||
-rw-r--r-- | tests/common/mod.rs | 23 | ||||
-rw-r--r-- | tests/install_tests.rs | 65 | ||||
-rw-r--r-- | tests/task_tests.rs | 2 |
10 files changed, 218 insertions, 40 deletions
diff --git a/docs/cli.mdx b/docs/cli.mdx index 0b8dd81..9497f4a 100644 --- a/docs/cli.mdx +++ b/docs/cli.mdx @@ -59,10 +59,14 @@ Which gets generated on `pixi add` or when you manually change the `pixi.toml` f #### Options - `--manifest-path`: the path to `pixi.toml`, by default it searches for one in the parent directories. +- `--frozen`: install the environment as defined in the lockfile. Without checking the status of the lockfile. +- `--locked`: only install if lockfile is up-to-date. Conflicts with `--frozen`. ```shell pixi install pixi install --manifest-path ~/myproject/pixi.toml +pixi install --frozen +pixi install --locked ``` ## `run` @@ -76,11 +80,15 @@ You cannot run `pixi run source setup.bash` as `source` is not available in the #### Options - `--manifest-path`: the path to `pixi.toml`, by default it searches for one in the parent directories. +- `--frozen`: install the environment as defined in the lockfile. Without checking the status of the lockfile. +- `--locked`: only install if lockfile is up-to-date. Conflicts with `--frozen`. ```shell pixi run python pixi run cowpy "Hey pixi user" pixi run --manifest-path ~/myproject/pixi.toml python +pixi run --frozen python +pixi run --locked python # If you have specified a custom task in the pixi.toml you can run it with run as well pixi run build ``` @@ -158,12 +166,18 @@ To exit the pixi shell, simply run `exit`. #### Options - `--manifest-path`: the path to `pixi.toml`, by default it searches for one in the parent directories. +- `--frozen`: install the environment as defined in the lockfile. Without checking the status of the lockfile. +- `--locked`: only install if lockfile is up-to-date. Conflicts with `--frozen`. ```shell pixi shell exit pixi shell --manifest-path ~/myproject/pixi.toml exit +pixi shell --frozen +exit +pixi shell --locked +exit ``` ## `info` diff --git a/src/cli/add.rs b/src/cli/add.rs index bf5c4ad..eea45a8 100644 --- a/src/cli/add.rs +++ b/src/cli/add.rs @@ -65,6 +65,10 @@ pub struct Args { #[arg(long, conflicts_with = "host")] pub build: bool, + /// Don't update lockfile, implies the no-install as well. + #[clap(long, conflicts_with = "no_install")] + pub no_lockfile_update: bool, + /// Don't install the package to the environment, only add the package to the lock-file. #[arg(long)] pub no_install: bool, @@ -104,6 +108,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { args.specs, spec_type, args.no_install, + args.no_lockfile_update, spec_platforms, ) .await @@ -114,6 +119,7 @@ pub async fn add_specs_to_project( specs: Vec<MatchSpec>, spec_type: SpecType, no_install: bool, + no_update_lockfile: bool, specs_platforms: Vec<Platform>, ) -> miette::Result<()> { // Split the specs into package name and version specifier @@ -206,27 +212,35 @@ pub async fn add_specs_to_project( added_specs.push(spec); } - - // Update the lock file and write to disk - let lock_file = update_lock_file( - project, - load_lock_file(project).await?, - Some(sparse_repo_data), - ) - .await?; project.save()?; - if !no_install { - let platform = Platform::current(); - if project.platforms().contains(&platform) { - // Get the currently installed packages - let prefix = Prefix::new(project.root().join(".pixi/env"))?; - let installed_packages = prefix.find_installed_packages(None).await?; + // Update the lock file + let lock_file = if !no_update_lockfile { + Some( + update_lock_file( + project, + load_lock_file(project).await?, + Some(sparse_repo_data), + ) + .await?, + ) + } else { + None + }; - // Update the prefix - update_prefix(&prefix, installed_packages, &lock_file, platform).await?; - } else { - eprintln!("{} skipping installation of environment because your platform ({platform}) is not supported by this project.", style("!").yellow().bold()) + if let Some(lock_file) = lock_file { + if !no_install { + let platform = Platform::current(); + if project.platforms().contains(&platform) { + // Get the currently installed packages + let prefix = Prefix::new(project.root().join(".pixi/env"))?; + let installed_packages = prefix.find_installed_packages(None).await?; + + // Update the prefix + update_prefix(&prefix, installed_packages, &lock_file, platform).await?; + } else { + eprintln!("{} skipping installation of environment because your platform ({platform}) is not supported by this project.", style("!").yellow().bold()) + } } } diff --git a/src/cli/install.rs b/src/cli/install.rs index 8f9d4bf..d2596ed 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -9,12 +9,20 @@ pub struct Args { /// The path to 'pixi.toml' #[arg(long)] pub manifest_path: Option<PathBuf>, + + /// Require pixi.lock is up-to-date + #[clap(long, conflicts_with = "frozen")] + pub locked: bool, + + /// Don't check if pixi.lock is up-to-date, install as lockfile states + #[clap(long, conflicts_with = "locked")] + pub frozen: bool, } pub async fn execute(args: Args) -> miette::Result<()> { let project = Project::load_or_else_discover(args.manifest_path.as_deref())?; - get_up_to_date_prefix(&project).await?; + get_up_to_date_prefix(&project, args.frozen, args.locked).await?; // Emit success eprintln!( diff --git a/src/cli/run.rs b/src/cli/run.rs index 966cc20..cf28302 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -38,6 +38,14 @@ pub struct Args { /// The path to 'pixi.toml' #[arg(long)] pub manifest_path: Option<PathBuf>, + + /// Require pixi.lock is up-to-date + #[clap(long, conflicts_with = "frozen")] + pub locked: bool, + + /// Don't check if pixi.lock is up-to-date, install as lockfile states + #[clap(long, conflicts_with = "locked")] + pub frozen: bool, } pub fn order_tasks( @@ -188,7 +196,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { let mut ordered_commands = order_tasks(args.task, &project)?; // Get the environment to run the commands in. - let command_env = get_task_env(&project).await?; + let command_env = get_task_env(&project, args.locked, args.frozen).await?; // Execute the commands in the correct order while let Some((command, args)) = ordered_commands.pop_back() { @@ -217,9 +225,13 @@ pub async fn execute(args: Args) -> miette::Result<()> { /// activation scripts from the environment and stores the environment variables it added, it adds /// environment variables set by the project and merges all of that with the system environment /// variables. -pub async fn get_task_env(project: &Project) -> miette::Result<HashMap<String, String>> { +pub async fn get_task_env( + project: &Project, + frozen: bool, + locked: bool, +) -> miette::Result<HashMap<String, String>> { // Get the prefix which we can then activate. - let prefix = get_up_to_date_prefix(project).await?; + let prefix = get_up_to_date_prefix(project, frozen, locked).await?; // Get environment variables from the activation let activation_env = run_activation_async(project, prefix).await?; diff --git a/src/cli/shell.rs b/src/cli/shell.rs index 07eec78..43d979f 100644 --- a/src/cli/shell.rs +++ b/src/cli/shell.rs @@ -23,6 +23,14 @@ pub struct Args { /// The path to 'pixi.toml' #[arg(long)] manifest_path: Option<PathBuf>, + + /// Require pixi.lock is up-to-date + #[clap(long, conflicts_with = "frozen")] + locked: bool, + + /// Don't check if pixi.lock is up-to-date, install as lockfile states + #[clap(long, conflicts_with = "locked")] + frozen: bool, } fn start_powershell( @@ -126,9 +134,13 @@ async fn start_unix_shell<T: Shell + Copy>( /// function as if the environment has been activated. This method runs the activation scripts from /// the environment and stores the environment variables it added, finally it adds environment /// variables from the project. -pub async fn get_shell_env(project: &Project) -> miette::Result<HashMap<String, String>> { +pub async fn get_shell_env( + project: &Project, + frozen: bool, + locked: bool, +) -> miette::Result<HashMap<String, String>> { // Get the prefix which we can then activate. - let prefix = get_up_to_date_prefix(project).await?; + let prefix = get_up_to_date_prefix(project, frozen, locked).await?; // Get environment variables from the activation let activation_env = run_activation_async(project, prefix).await?; @@ -152,7 +164,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { let project = Project::load_or_else_discover(args.manifest_path.as_deref())?; // Get the environment variables we need to set activate the project in the shell. - let env = get_shell_env(&project).await?; + let env = get_shell_env(&project, args.frozen, args.locked).await?; // Start the shell as the last part of the activation script based on the default shell. let interactive_shell: ShellEnum = ShellEnum::from_parent_process() diff --git a/src/environment.rs b/src/environment.rs index 3fb76c4..5d81585 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -36,7 +36,13 @@ use std::{ /// Returns the prefix associated with the given environment. If the prefix doesn't exist or is not /// up to date it is updated. -pub async fn get_up_to_date_prefix(project: &Project) -> miette::Result<Prefix> { +/// Use `frozen` or `locked` to skip the update of the lockfile. Use frozen when you don't even want +/// to check the lockfile status. +pub async fn get_up_to_date_prefix( + project: &Project, + frozen: bool, + locked: bool, +) -> miette::Result<Prefix> { // Make sure the project supports the current platform let platform = Platform::current(); if !project.platforms().contains(&platform) { @@ -65,11 +71,17 @@ pub async fn get_up_to_date_prefix(project: &Project) -> miette::Result<Prefix> }; // Update the lock-file if it is out of date. + if frozen && locked { + miette::bail!("Frozen and Locked can't be true at the same time, as using frozen will ignore the locked variable."); + } let lock_file = load_lock_file(project).await?; - let lock_file = if !lock_file_up_to_date(project, &lock_file)? { - update_lock_file(project, lock_file, None).await? - } else { + let lock_file = if frozen || lock_file_up_to_date(project, &lock_file)? { lock_file + } else { + if locked { + miette::bail!("Lockfile not up-to-date with the project"); + } + update_lock_file(project, lock_file, None).await? }; // Update the environment diff --git a/tests/common/builders.rs b/tests/common/builders.rs index 2cdeb3b..6e4ed50 100644 --- a/tests/common/builders.rs +++ b/tests/common/builders.rs @@ -24,7 +24,7 @@ //! ``` use crate::common::IntoMatchSpec; -use pixi::cli::{add, init, project, task}; +use pixi::cli::{add, init, install, project, task}; use pixi::project::SpecType; use rattler_conda_types::Platform; use std::future::{Future, IntoFuture}; @@ -37,7 +37,7 @@ pub fn string_from_iter(iter: impl IntoIterator<Item = impl AsRef<str>>) -> Vec< iter.into_iter().map(|s| s.as_ref().to_string()).collect() } -/// Contains the arguments to pass to `init::execute()`. Call `.await` to call the CLI execute +/// Contains the arguments to pass to [`init::execute()`]. Call `.await` to call the CLI execute /// method and await the result at the same time. pub struct InitBuilder { pub args: init::Args, @@ -71,7 +71,7 @@ impl IntoFuture for InitBuilder { } } -/// Contains the arguments to pass to `add::execute()`. Call `.await` to call the CLI execute method +/// Contains the arguments to pass to [`add::execute()`]. Call `.await` to call the CLI execute method /// and await the result at the same time. pub struct AddBuilder { pub args: add::Args, @@ -109,6 +109,13 @@ impl AddBuilder { self } + /// Skip updating lockfile, this will only check if it can add a dependencies. + /// If it can add it will only add it to the manifest. Install will be skipped by default. + pub fn without_lockfile_update(mut self) -> Self { + self.args.no_lockfile_update = true; + self + } + pub fn set_platforms(mut self, platforms: &[Platform]) -> Self { self.args.platform.extend(platforms.iter()); self @@ -201,3 +208,28 @@ impl IntoFuture for ProjectChannelAddBuilder { })) } } + +/// Contains the arguments to pass to [`install::execute()`]. Call `.await` to call the CLI execute method +/// and await the result at the same time. +pub struct InstallBuilder { + pub args: install::Args, +} + +impl InstallBuilder { + pub fn with_locked(mut self) -> Self { + self.args.locked = true; + self + } + pub fn with_frozen(mut self) -> Self { + self.args.frozen = true; + self + } +} + +impl IntoFuture for InstallBuilder { + type Output = miette::Result<()>; + type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'static>>; + fn into_future(self) -> Self::IntoFuture { + Box::pin(install::execute(self.args)) + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 6115d8b..14a88ab 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -4,7 +4,8 @@ pub mod builders; pub mod package_database; use crate::common::builders::{ - AddBuilder, InitBuilder, ProjectChannelAddBuilder, TaskAddBuilder, TaskAliasBuilder, + AddBuilder, InitBuilder, InstallBuilder, ProjectChannelAddBuilder, TaskAddBuilder, + TaskAliasBuilder, }; use pixi::cli::install::Args; use pixi::cli::run::{ @@ -158,6 +159,7 @@ impl PixiControl { specs: vec![spec.into()], build: false, no_install: true, + no_lockfile_update: false, platform: Default::default(), }, } @@ -180,7 +182,9 @@ impl PixiControl { let mut tasks = order_tasks(args.task, &self.project().unwrap())?; let project = self.project().unwrap(); - let task_env = get_task_env(&project).await.unwrap(); + let task_env = get_task_env(&project, args.frozen, args.locked) + .await + .unwrap(); let mut result = RunOutput::default(); while let Some((command, args)) = tasks.pop_back() { @@ -198,12 +202,15 @@ impl PixiControl { Ok(result) } - /// Create an installed environment. I.e a resolved and installed prefix - pub async fn install(&self) -> miette::Result<()> { - pixi::cli::install::execute(Args { - manifest_path: Some(self.manifest_path()), - }) - .await + /// Returns a [`InstallBuilder`]. To execute the command and await the result call `.await` on the return value. + pub fn install(&self) -> InstallBuilder { + InstallBuilder { + args: Args { + manifest_path: Some(self.manifest_path()), + locked: false, + frozen: false, + }, + } } /// Get the associated lock file diff --git a/tests/install_tests.rs b/tests/install_tests.rs index dc073d1..f548f62 100644 --- a/tests/install_tests.rs +++ b/tests/install_tests.rs @@ -103,3 +103,68 @@ async fn test_incremental_lock_file() { "expected `bar` to remain locked to version 1." ); } + +/// Test the `pixi install --locked` functionality. +#[tokio::test] +#[cfg_attr(not(feature = "slow_integration_tests"), ignore)] +async fn install_locked() { + let pixi = PixiControl::new().unwrap(); + pixi.init().await.unwrap(); + // Add and update lockfile with this version of python + pixi.add("python==3.10.0").await.unwrap(); + + // Add new version of python only to the manifest + pixi.add("python==3.11.0") + .without_lockfile_update() + .await + .unwrap(); + + assert!(pixi.install().with_locked().await.is_err(), "should error when installing with locked but there is a mismatch in the dependencies and the lockfile."); + + // Check if it didn't accidentally update the lockfile + let lock = pixi.lock_file().await.unwrap(); + assert!(lock.contains_matchspec("python==3.10.0")); + + // After an install with lockfile update the locked install should succeed. + pixi.install().await.unwrap(); + pixi.install().with_locked().await.unwrap(); + + // Check if lock has python version updated + let lock = pixi.lock_file().await.unwrap(); + assert!(lock.contains_matchspec("python==3.11.0")); +} + +/// Test `pixi install/run --frozen` functionality +#[tokio::test] +#[cfg_attr(not(feature = "slow_integration_tests"), ignore)] +async fn install_frozen() { + let pixi = PixiControl::new().unwrap(); + pixi.init().await.unwrap(); + // Add and update lockfile with this version of python + pixi.add("python==3.10.0").await.unwrap(); + + // Add new version of python only to the manifest + pixi.add("python==3.11.0") + .without_lockfile_update() + .await + .unwrap(); + + pixi.install().with_frozen().await.unwrap(); + + // Check if it didn't accidentally update the lockfile + let lock = pixi.lock_file().await.unwrap(); + assert!(lock.contains_matchspec("python==3.10.0")); + + // Check if running with frozen doesn't suddenly install the latest update. + let result = pixi + .run(run::Args { + frozen: true, + task: string_from_iter(["python", "--version"]), + ..Default::default() + }) + .await + .unwrap(); + assert_eq!(result.exit_code, 0); + assert_eq!(result.stdout.trim(), "Python 3.10.0"); + assert!(result.stderr.is_empty()); +} diff --git a/tests/task_tests.rs b/tests/task_tests.rs index beecf1e..c3f4ad1 100644 --- a/tests/task_tests.rs +++ b/tests/task_tests.rs @@ -94,6 +94,8 @@ async fn test_alias() { .run(Args { task: vec!["helloworld".to_string()], manifest_path: None, + locked: false, + frozen: false, }) .await .unwrap(); |