summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRuben Arts <ruben.arts@hotmail.com>2023-09-20 21:07:30 +0200
committerGitHub <noreply@github.com>2023-09-20 21:07:30 +0200
commit4943582575171f85ceb28305561ab3004ffcb448 (patch)
treeb93706bf84a765ee8ae5796b4d26be7d9bcb3edc
parenta09f4b9f4e96529dd7a33f65e7598640dec5fc51 (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.mdx28
-rw-r--r--src/cli/mod.rs3
-rw-r--r--src/cli/project/channel/add.rs77
-rw-r--r--src/cli/project/channel/mod.rs30
-rw-r--r--src/cli/project/mod.rs25
-rw-r--r--tests/common/builders.rs32
-rw-r--r--tests/common/mod.rs17
-rw-r--r--tests/project_tests.rs49
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));
+}