diff --git a/src/cli/channel/add.rs b/src/cli/channel/add.rs new file mode 100644 index 000000000..840c656e8 --- /dev/null +++ b/src/cli/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 +#[derive(Parser, Debug, Default)] +pub struct Args { + /// The channel name or URL + #[clap(required = true, num_args=1..)] + pub channel: Vec, + + /// 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::, _>>() + .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/channel/mod.rs b/src/cli/channel/mod.rs new file mode 100644 index 000000000..6d176a3da --- /dev/null +++ b/src/cli/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, + + /// 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/mod.rs b/src/cli/mod.rs index fef64c221..898e501ad 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -11,6 +11,7 @@ use tracing_subscriber::{filter::LevelFilter, util::SubscriberInitExt, EnvFilter pub mod add; pub mod auth; +pub mod channel; pub mod global; pub mod info; pub mod init; @@ -63,6 +64,7 @@ pub enum Command { Task(task::Args), Info(info::Args), Upload(upload::Args), + Channel(channel::Args), } fn completion(args: CompletionCommand) -> miette::Result<()> { @@ -165,6 +167,7 @@ pub async fn execute_command(command: Command) -> miette::Result<()> { Command::Task(cmd) => task::execute(cmd), Command::Info(cmd) => info::execute(cmd).await, Command::Upload(cmd) => upload::execute(cmd).await, + Command::Channel(cmd) => channel::execute(cmd).await, } } diff --git a/src/repodata.rs b/src/repodata.rs index 82659c7fc..6a5d51d97 100644 --- a/src/repodata.rs +++ b/src/repodata.rs @@ -70,7 +70,7 @@ pub async fn fetch_sparse_repodata( &repodata_cache, download_client, progress_bar.clone(), - platform == Platform::NoArch, + platform != Platform::NoArch, ) .await; diff --git a/tests/channel_tests.rs b/tests/channel_tests.rs new file mode 100644 index 000000000..149a2290e --- /dev/null +++ b/tests/channel_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.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)); +} diff --git a/tests/common/builders.rs b/tests/common/builders.rs index fb2a07a30..a6fb63143 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, channel, init, task}; use pixi::project::SpecType; use rattler_conda_types::Platform; use std::future::{Future, IntoFuture}; @@ -163,3 +163,33 @@ impl TaskAliasBuilder { }) } } + +pub struct ChannelAddBuilder { + pub manifest_path: Option, + pub args: channel::add::Args, +} + +impl ChannelAddBuilder { + /// Adds the specified channel + pub fn with_channel(mut self, name: impl Into) -> Self { + self.args.channel.push(name.into()); + self + } + + /// Alias to add a local channel. + pub fn with_local_channel(self, channel: impl AsRef) -> Self { + self.with_channel(Url::from_directory_path(channel).unwrap()) + } +} + +impl IntoFuture for ChannelAddBuilder { + type Output = miette::Result<()>; + type IntoFuture = Pin + Send + 'static>>; + + fn into_future(self) -> Self::IntoFuture { + Box::pin(channel::execute(channel::Args { + manifest_path: self.manifest_path, + command: channel::Command::Add(self.args), + })) + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 9ec51a557..68164936e 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, ChannelAddBuilder, InitBuilder, 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, channel, init, run, task}; use pixi::{consts, Project}; use rattler_conda_types::conda_lock::CondaLock; use rattler_conda_types::{MatchSpec, Platform, Version}; @@ -148,6 +150,17 @@ impl PixiControl { } } + /// Add a new channel to the project. + pub fn channel_add(&self) -> ChannelAddBuilder { + ChannelAddBuilder { + manifest_path: Some(self.manifest_path()), + args: channel::add::Args { + channel: vec![], + no_install: true, + }, + } + } + /// Run a command pub async fn run(&self, mut args: run::Args) -> miette::Result { args.manifest_path = args.manifest_path.or_else(|| Some(self.manifest_path()));