summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRuben Arts <ruben.arts@hotmail.com>2023-09-28 12:56:17 +0200
committerGitHub <noreply@github.com>2023-09-28 12:56:17 +0200
commite2550d14fe3c90f94c3b72bdb11e099c1e865e32 (patch)
treeac6365c8de835401a144a7f9d397c26fc0184fc2
parente7807bc3a67ad2a72ba225a32c87f8c3ab3e1c8b (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.mdx14
-rw-r--r--src/cli/add.rs50
-rw-r--r--src/cli/install.rs10
-rw-r--r--src/cli/run.rs18
-rw-r--r--src/cli/shell.rs18
-rw-r--r--src/environment.rs20
-rw-r--r--tests/common/builders.rs38
-rw-r--r--tests/common/mod.rs23
-rw-r--r--tests/install_tests.rs65
-rw-r--r--tests/task_tests.rs2
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();