diff options
author | Ruben Arts <ruben.arts@hotmail.com> | 2023-09-20 21:07:30 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-20 21:07:30 +0200 |
commit | 4943582575171f85ceb28305561ab3004ffcb448 (patch) | |
tree | b93706bf84a765ee8ae5796b4d26be7d9bcb3edc | |
parent | a09f4b9f4e96529dd7a33f65e7598640dec5fc51 (diff) |
feat: add channel add feature on project subcommand. (#347)
Added the #254 but with the `project` prefix. So the main cli entry
point stays as clean as possible. This is beneficial for future
expansion.
-rw-r--r-- | docs/cli.mdx | 28 | ||||
-rw-r--r-- | src/cli/mod.rs | 3 | ||||
-rw-r--r-- | src/cli/project/channel/add.rs | 77 | ||||
-rw-r--r-- | src/cli/project/channel/mod.rs | 30 | ||||
-rw-r--r-- | src/cli/project/mod.rs | 25 | ||||
-rw-r--r-- | tests/common/builders.rs | 32 | ||||
-rw-r--r-- | tests/common/mod.rs | 17 | ||||
-rw-r--r-- | tests/project_tests.rs | 49 |
8 files changed, 257 insertions, 4 deletions
diff --git a/docs/cli.mdx b/docs/cli.mdx index 1c924cd..7780c15 100644 --- a/docs/cli.mdx +++ b/docs/cli.mdx @@ -243,4 +243,30 @@ pixi global install "python [version='3.11.0', build=he550d4f_1_cpython]" pixi global install python=3.11.0=h10a6764_1_cpython ``` -After using global install you can use the package you installed anywhere on your system. +After using global install, you can use the package you installed anywhere on your system. + +## `project` + +This subcommand allows you to modify the project configuration through the command line interface. + +#### Options + +- `--manifest-path`: the path to `pixi.toml`, by default it searches for one in the parent directories. +- `--no-install`: do not update the environment, only add changed packages to the lock-file. + +### `project channel add` + +Add channels to the channel list in the project configuration. +When you add channels, the channels are tested for existence, added to the lockfile and the environment is reinstalled. + +#### Options + +- `--no-install`: do not update the environment, only add changed packages to the lock-file. + +``` +pixi project channel add robostack +pixi project channel add bioconda conda-forge robostack +pixi project channel add file:///home/user/local_channel +pixi project channel add https://repo.prefix.dev/conda-forge +pixi project channel add --no-install robostack +``` diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 857f772..7d96ec3 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -15,6 +15,7 @@ pub mod global; pub mod info; pub mod init; pub mod install; +pub mod project; pub mod run; pub mod search; pub mod shell; @@ -65,6 +66,7 @@ pub enum Command { Info(info::Args), Upload(upload::Args), Search(search::Args), + Project(project::Args), } fn completion(args: CompletionCommand) -> miette::Result<()> { @@ -168,6 +170,7 @@ pub async fn execute_command(command: Command) -> miette::Result<()> { Command::Info(cmd) => info::execute(cmd).await, Command::Upload(cmd) => upload::execute(cmd).await, Command::Search(cmd) => search::execute(cmd).await, + Command::Project(cmd) => project::execute(cmd).await, } } diff --git a/src/cli/project/channel/add.rs b/src/cli/project/channel/add.rs new file mode 100644 index 0000000..f14080e --- /dev/null +++ b/src/cli/project/channel/add.rs @@ -0,0 +1,77 @@ +use crate::environment::{load_lock_file, update_lock_file, update_prefix}; +use crate::prefix::Prefix; +use crate::Project; +use clap::Parser; +use itertools::Itertools; +use miette::IntoDiagnostic; +use rattler_conda_types::{Channel, ChannelConfig, Platform}; + +/// Adds a channel to the project file and updates the lockfile. +#[derive(Parser, Debug, Default)] +pub struct Args { + /// The channel name or URL + #[clap(required = true, num_args=1..)] + pub channel: Vec<String>, + + /// Don't update the environment, only add changed packages to the lock-file. + #[clap(long)] + pub no_install: bool, +} + +pub async fn execute(mut project: Project, args: Args) -> miette::Result<()> { + // Determine which channels are missing + let channel_config = ChannelConfig::default(); + let channels = args + .channel + .into_iter() + .map(|channel_str| { + Channel::from_str(&channel_str, &channel_config).map(|channel| (channel_str, channel)) + }) + .collect::<Result<Vec<_>, _>>() + .into_diagnostic()?; + + let missing_channels = channels + .into_iter() + .filter(|(_name, channel)| !project.channels().contains(channel)) + .collect_vec(); + + if missing_channels.is_empty() { + eprintln!( + "{}All channel(s) have already been added.", + console::style(console::Emoji("✔ ", "")).green(), + ); + return Ok(()); + } + + // Load the existing lock-file + let lock_file = load_lock_file(&project).await?; + + // Add the channels to the lock-file + project.add_channels(missing_channels.iter().map(|(name, _channel)| name))?; + + // Try to update the lock-file with the new channels + let lock_file = update_lock_file(&project, lock_file, None).await?; + project.save()?; + + // Update the installation if needed + if !args.no_install { + // 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::current()).await?; + } + + // Report back to the user + for (name, channel) in missing_channels { + eprintln!( + "{}Added {} ({})", + console::style(console::Emoji("✔ ", "")).green(), + name, + channel.base_url() + ); + } + + Ok(()) +} diff --git a/src/cli/project/channel/mod.rs b/src/cli/project/channel/mod.rs new file mode 100644 index 0000000..6d176a3 --- /dev/null +++ b/src/cli/project/channel/mod.rs @@ -0,0 +1,30 @@ +pub mod add; + +use crate::Project; +use clap::Parser; +use std::path::PathBuf; + +/// Commands to manage project channels. +#[derive(Parser, Debug)] +pub struct Args { + /// The path to 'pixi.toml' + #[clap(long, global = true)] + pub manifest_path: Option<PathBuf>, + + /// The subcommand to execute + #[clap(subcommand)] + pub command: Command, +} + +#[derive(Parser, Debug)] +pub enum Command { + Add(add::Args), +} + +pub async fn execute(args: Args) -> miette::Result<()> { + let project = Project::load_or_else_discover(args.manifest_path.as_deref())?; + + match args.command { + Command::Add(args) => add::execute(project, args).await, + } +} diff --git a/src/cli/project/mod.rs b/src/cli/project/mod.rs new file mode 100644 index 0000000..ada9579 --- /dev/null +++ b/src/cli/project/mod.rs @@ -0,0 +1,25 @@ +use clap::Parser; +use std::path::PathBuf; + +pub mod channel; + +#[derive(Debug, Parser)] +pub enum Command { + Channel(channel::Args), +} +// Modify the project configuration file through the command line. +#[derive(Debug, Parser)] +pub struct Args { + #[command(subcommand)] + command: Command, + /// The path to 'pixi.toml' + #[arg(long)] + pub manifest_path: Option<PathBuf>, +} + +pub async fn execute(cmd: Args) -> miette::Result<()> { + match cmd.command { + Command::Channel(args) => channel::execute(args).await?, + }; + Ok(()) +} diff --git a/tests/common/builders.rs b/tests/common/builders.rs index ec559bd..2cdeb3b 100644 --- a/tests/common/builders.rs +++ b/tests/common/builders.rs @@ -24,7 +24,7 @@ //! ``` use crate::common::IntoMatchSpec; -use pixi::cli::{add, init, task}; +use pixi::cli::{add, init, project, task}; use pixi::project::SpecType; use rattler_conda_types::Platform; use std::future::{Future, IntoFuture}; @@ -171,3 +171,33 @@ impl TaskAliasBuilder { }) } } + +pub struct ProjectChannelAddBuilder { + pub manifest_path: Option<PathBuf>, + pub args: project::channel::add::Args, +} + +impl ProjectChannelAddBuilder { + /// Adds the specified channel + pub fn with_channel(mut self, name: impl Into<String>) -> Self { + self.args.channel.push(name.into()); + self + } + + /// Alias to add a local channel. + pub fn with_local_channel(self, channel: impl AsRef<Path>) -> Self { + self.with_channel(Url::from_directory_path(channel).unwrap()) + } +} + +impl IntoFuture for ProjectChannelAddBuilder { + type Output = miette::Result<()>; + type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'static>>; + + fn into_future(self) -> Self::IntoFuture { + Box::pin(project::channel::execute(project::channel::Args { + manifest_path: self.manifest_path, + command: project::channel::Command::Add(self.args), + })) + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 2e10a11..c127252 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -3,13 +3,15 @@ pub mod builders; pub mod package_database; -use crate::common::builders::{AddBuilder, InitBuilder, TaskAddBuilder, TaskAliasBuilder}; +use crate::common::builders::{ + AddBuilder, InitBuilder, ProjectChannelAddBuilder, TaskAddBuilder, TaskAliasBuilder, +}; use pixi::cli::install::Args; use pixi::cli::run::{ create_script, execute_script_with_output, get_task_env, order_tasks, RunOutput, }; use pixi::cli::task::{AddArgs, AliasArgs}; -use pixi::cli::{add, init, run, task}; +use pixi::cli::{add, init, project, run, task}; use pixi::{consts, Project}; use rattler_conda_types::conda_lock::CondaLock; use rattler_conda_types::{MatchSpec, PackageName, Platform, Version}; @@ -161,6 +163,17 @@ impl PixiControl { } } + /// Add a new channel to the project. + pub fn project_channel_add(&self) -> ProjectChannelAddBuilder { + ProjectChannelAddBuilder { + manifest_path: Some(self.manifest_path()), + args: project::channel::add::Args { + channel: vec![], + no_install: true, + }, + } + } + /// Run a command pub async fn run(&self, mut args: run::Args) -> miette::Result<RunOutput> { args.manifest_path = args.manifest_path.or_else(|| Some(self.manifest_path())); diff --git a/tests/project_tests.rs b/tests/project_tests.rs new file mode 100644 index 0000000..8e6b1b1 --- /dev/null +++ b/tests/project_tests.rs @@ -0,0 +1,49 @@ +mod common; + +use crate::{common::package_database::PackageDatabase, common::PixiControl}; +use rattler_conda_types::{Channel, ChannelConfig}; +use tempfile::TempDir; +use url::Url; + +#[tokio::test] +async fn add_channel() { + // Create a local package database with no entries and write it to disk. This ensures that we + // have a valid channel. + let package_database = PackageDatabase::default(); + let initial_channel_dir = TempDir::new().unwrap(); + package_database + .write_repodata(initial_channel_dir.path()) + .await + .unwrap(); + + // Run the init command + let pixi = PixiControl::new().unwrap(); + pixi.init() + .with_local_channel(initial_channel_dir.path()) + .await + .unwrap(); + + // Create and add another local package directory + let additional_channel_dir = TempDir::new().unwrap(); + package_database + .write_repodata(additional_channel_dir.path()) + .await + .unwrap(); + pixi.project_channel_add() + .with_local_channel(additional_channel_dir.path()) + .await + .unwrap(); + + // There should be a loadable project manifest in the directory + let project = pixi.project().unwrap(); + + // Our channel should be in the list of channels + let local_channel = Channel::from_str( + Url::from_directory_path(additional_channel_dir.path()) + .unwrap() + .to_string(), + &ChannelConfig::default(), + ) + .unwrap(); + assert!(project.channels().contains(&local_channel)); +} |