From f20ab1964a99fdf95a6d697c60df38425e28af1a Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Mon, 3 Jul 2023 16:57:43 +0200 Subject: [PATCH 1/8] feat: added the abilty to add commands through the command line and test these --- src/cli/command.rs | 94 ++++++++++++++++++++++++ src/cli/mod.rs | 5 ++ src/project/mod.rs | 87 ++++++++++++++++++++++ tests/command_tests.rs | 58 +++++++++++++++ tests/common/builders.rs | 152 +++++++++++++++++++++++++++++++++++++++ tests/common/mod.rs | 118 +++++++++++------------------- tests/install_tests.rs | 2 +- 7 files changed, 438 insertions(+), 78 deletions(-) create mode 100644 src/cli/command.rs create mode 100644 tests/command_tests.rs create mode 100644 tests/common/builders.rs diff --git a/src/cli/command.rs b/src/cli/command.rs new file mode 100644 index 000000000..c16ffce7a --- /dev/null +++ b/src/cli/command.rs @@ -0,0 +1,94 @@ +use crate::command::{AliasCmd, CmdArgs, ProcessCmd}; +use crate::Project; +use clap::Parser; +use std::path::PathBuf; + +#[derive(Parser, Debug)] +pub enum Operation { + /// Add a command to the project + Add(AddArgs), + + /// Remove a command from the project + Remove(RemoveArgs), + + /// Alias another specific command + Alias(AliasArgs), +} + +#[derive(Parser, Debug)] +pub struct RemoveArgs { + /// Command name + pub name: String, +} + +#[derive(Parser, Debug)] +pub struct AddArgs { + /// Command name + pub name: String, + + /// One or more commands to actually execute + pub commands: Vec, + + /// Depends on these other commands + #[clap(long)] + pub depends_on: Option>, +} + +#[derive(Parser, Debug)] +pub struct AliasArgs { + /// Alias name + pub name: String, + + /// Depends on these commands to execute + pub depends_on: Vec, +} + +impl From for crate::command::Command { + fn from(value: AddArgs) -> Self { + let first_command = value.commands.get(0).cloned().unwrap_or_default(); + let depends_on = value.depends_on.unwrap_or_default(); + + if value.commands.len() < 2 && depends_on.is_empty() { + return Self::Plain(first_command); + } + + Self::Process(ProcessCmd { + cmd: if value.commands.len() == 1 { + CmdArgs::Single(first_command) + } else { + CmdArgs::Multiple(value.commands) + }, + depends_on, + }) + } +} + +impl From for crate::command::Command { + fn from(value: AliasArgs) -> Self { + Self::Alias(AliasCmd { + depends_on: value.depends_on, + }) + } +} + +/// Command management in project +#[derive(Parser, Debug)] +#[clap(trailing_var_arg = true, arg_required_else_help = true)] +pub struct Args { + /// Add, remove, or update a command + #[clap(subcommand)] + pub operation: Operation, + + /// The path to 'pixi.toml' + #[arg(long)] + pub manifest_path: Option, +} + +pub fn execute(args: Args) -> anyhow::Result<()> { + let mut project = Project::load_or_else_discover(args.manifest_path.as_deref())?; + match args.operation { + Operation::Add(args) => project.add_command(&args.name.clone(), args.into()), + Operation::Remove(args) => project.remove_command(&args.name), + Operation::Alias(args) => project.add_command(&args.name.clone(), args.into()), + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 16cf8fcf8..01e9bdcf3 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -9,6 +9,7 @@ use tracing_subscriber::{filter::LevelFilter, util::SubscriberInitExt, EnvFilter pub mod add; pub mod auth; +pub mod command; pub mod global; pub mod init; pub mod install; @@ -49,7 +50,10 @@ pub enum Command { #[clap(alias = "g")] Global(global::Args), Auth(auth::Args), + #[clap(alias = "i")] Install(install::Args), + #[clap(alias = "c")] + Command(command::Args), } fn completion(args: CompletionCommand) -> Result<(), Error> { @@ -104,5 +108,6 @@ pub async fn execute_command(command: Command) -> Result<(), Error> { Command::Auth(cmd) => auth::execute(cmd).await, Command::Install(cmd) => install::execute(cmd).await, Command::Shell(cmd) => shell::execute(cmd).await, + Command::Command(cmd) => command::execute(cmd), } } diff --git a/src/project/mod.rs b/src/project/mod.rs index bc725c4ff..e57850aff 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -2,6 +2,7 @@ pub mod environment; pub mod manifest; mod serde; +use crate::command::{CmdArgs, Command as PixiCommand, Command}; use crate::consts; use crate::consts::PROJECT_MANIFEST; use crate::project::manifest::{ProjectManifest, TargetMetadata, TargetSelector}; @@ -28,6 +29,37 @@ pub struct Project { pub manifest: ProjectManifest, } +/// Returns a command a a toml item +fn command_as_toml(command: PixiCommand) -> Item { + match command { + Command::Plain(str) => Item::Value(str.into()), + Command::Process(process) => { + let mut table = Table::new().into_inline_table(); + match process.cmd { + CmdArgs::Single(cmd_str) => { + table.insert("cmd", cmd_str.into()); + } + CmdArgs::Multiple(cmd_strs) => { + table.insert("cmd", Value::Array(Array::from_iter(cmd_strs.into_iter()))); + } + } + table.insert( + "depends_on", + Value::Array(Array::from_iter(process.depends_on.into_iter())), + ); + Item::Value(Value::InlineTable(table)) + } + Command::Alias(alias) => { + let mut table = Table::new().into_inline_table(); + table.insert( + "depends_on", + Value::Array(Array::from_iter(alias.depends_on.into_iter())), + ); + Item::Value(Value::InlineTable(table)) + } + } +} + impl Project { /// Discovers the project manifest file in the current directory or any of the parent /// directories. @@ -54,6 +86,61 @@ impl Project { }) } + /// Add a command to the project + pub fn add_command( + &mut self, + name: impl AsRef, + command: PixiCommand, + ) -> anyhow::Result<()> { + if self.manifest.commands.contains_key(name.as_ref()) { + anyhow::bail!("command {} already exists", name.as_ref()); + }; + + let commands_table = &mut self.doc["commands"]; + // If it doesnt exist create a proper table + if commands_table.is_none() { + *commands_table = Item::Table(Table::new()); + } + + // Cast the item into a table + let commands_table = commands_table.as_table_like_mut().ok_or_else(|| { + anyhow::anyhow!("commands in {} are malformed", consts::PROJECT_MANIFEST) + })?; + + // Add the command to the table + commands_table.insert(name.as_ref(), command_as_toml(command.clone())); + + self.manifest + .commands + .insert(name.as_ref().to_string(), command); + + self.save()?; + + Ok(()) + } + + /// Remove a command from the project + pub fn remove_command(&mut self, name: impl AsRef) -> anyhow::Result<()> { + match self.manifest.commands.remove(name.as_ref()) { + None => anyhow::bail!("command {} does not exist", name.as_ref()), + Some(_) => {} + } + let commands_table = &mut self.doc["commands"]; + if commands_table.is_none() { + anyhow::bail!("internal data-structure inconsistent with toml"); + } + let commands_table = commands_table.as_table_like_mut().ok_or_else(|| { + anyhow::anyhow!("commands in {} are malformed", consts::PROJECT_MANIFEST) + })?; + + // If it does not exist in toml, consider this ok as we want to remove it anyways + commands_table.remove(name.as_ref()); + + self.save()?; + + Ok(()) + } + pub fn load_or_else_discover(manifest_path: Option<&Path>) -> anyhow::Result { let project = match manifest_path { Some(path) => Project::load(path)?, diff --git a/tests/command_tests.rs b/tests/command_tests.rs new file mode 100644 index 000000000..739674a0f --- /dev/null +++ b/tests/command_tests.rs @@ -0,0 +1,58 @@ +use crate::common::PixiControl; + +mod common; + +#[tokio::test] +pub async fn add_command() { + let pixi = PixiControl::new().unwrap(); + pixi.init().await.unwrap(); + + // Simple command + pixi.command() + .add("test") + .with_commands(&["echo hello"]) + .execute() + .unwrap(); + + let project = pixi.project().unwrap(); + let cmd = project.manifest.commands.get("test").unwrap(); + assert!(matches!(cmd, pixi::command::Command::Plain(s) if s == "echo hello")); + + // Remove the command + pixi.command().remove("test").await.unwrap(); + assert_eq!(pixi.project().unwrap().manifest.commands.len(), 0); + + // Add a command with dependencies + pixi.command() + .add("test") + .with_commands(&["echo hello"]) + .with_depends_on(&["something_else"]) + .execute() + .unwrap(); + pixi.command() + .add("test2") + .with_commands(&["echo hello", "Bonjour"]) + .execute() + .unwrap(); + + let project = pixi.project().unwrap(); + let cmd = project.manifest.commands.get("test").unwrap(); + assert!(matches!(cmd, pixi::command::Command::Process(cmd) if cmd.depends_on.len() > 0)); + assert!( + matches!(cmd, pixi::command::Command::Process(cmd) if matches!(cmd.cmd, pixi::command::CmdArgs::Single(_))) + ); + let cmd = project.manifest.commands.get("test2").unwrap(); + assert!( + matches!(cmd, pixi::command::Command::Process(cmd) if matches!(cmd.cmd, pixi::command::CmdArgs::Multiple(_))) + ); + + // Create an alias + pixi.command() + .alias("testing") + .with_depends_on(&["test"]) + .execute() + .unwrap(); + let project = pixi.project().unwrap(); + let cmd = project.manifest.commands.get("testing").unwrap(); + assert!(matches!(cmd, pixi::command::Command::Alias(a) if a.depends_on.len() > 0)); +} diff --git a/tests/common/builders.rs b/tests/common/builders.rs new file mode 100644 index 000000000..37fead50f --- /dev/null +++ b/tests/common/builders.rs @@ -0,0 +1,152 @@ +//! Contains builders for the CLI commands +//! We are using a builder pattern here to make it easier to write tests. +//! And are kinda abusing the `IntoFuture` trait to make it easier to execute as close +//! as we can get to the command line args +//! +//! # Using IntoFuture +//! +//! When `.await` is called on an object that is not a `Future` the compiler will first check if the +//! type implements `IntoFuture`. If it does it will call the `IntoFuture::into_future()` method and +//! await the resulting `Future`. We can abuse this behavior in builder patterns because the +//! `into_future` method can also be used as a `finish` function. This allows you to reduce the +//! required code. +//! +//! ```rust +//! impl IntoFuture for InitBuilder { +//! type Output = anyhow::Result<()>; +//! type IntoFuture = Pin + Send + 'static>>; +//! +//! fn into_future(self) -> Self::IntoFuture { +//! Box::pin(init::execute(self.args)) +//! } +//! } +//! +//! ``` + +use crate::common::IntoMatchSpec; +use pixi::cli::add::SpecType; +use pixi::cli::{add, command, init}; +use std::future::{Future, IntoFuture}; +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use url::Url; + +/// Strings from an iterator +pub fn string_from_iter(iter: impl IntoIterator>) -> 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 +/// method and await the result at the same time. +pub struct InitBuilder { + pub args: init::Args, +} + +impl InitBuilder { + pub fn with_channel(mut self, channel: impl ToString) -> Self { + self.args.channels.push(channel.to_string()); + self + } + + pub fn with_local_channel(self, channel: impl AsRef) -> Self { + self.with_channel(Url::from_directory_path(channel).unwrap()) + } +} + +impl IntoFuture for InitBuilder { + type Output = anyhow::Result<()>; + type IntoFuture = Pin + Send + 'static>>; + + fn into_future(self) -> Self::IntoFuture { + Box::pin(init::execute(self.args)) + } +} + +/// 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, +} + +impl AddBuilder { + pub fn with_spec(mut self, spec: impl IntoMatchSpec) -> Self { + self.args.specs.push(spec.into()); + self + } + + /// Set as a host + pub fn set_type(mut self, t: SpecType) -> Self { + match t { + SpecType::Host => { + self.args.host = true; + self.args.build = false; + } + SpecType::Build => { + self.args.host = false; + self.args.build = true; + } + SpecType::Run => { + self.args.host = false; + self.args.build = false; + } + } + self + } +} + +impl IntoFuture for AddBuilder { + type Output = anyhow::Result<()>; + type IntoFuture = Pin + Send + 'static>>; + + fn into_future(self) -> Self::IntoFuture { + Box::pin(add::execute(self.args)) + } +} + +pub struct CommandAddBuilder { + pub manifest_path: Option, + pub args: command::AddArgs, +} + +impl CommandAddBuilder { + /// Execute these commands + pub fn with_commands(mut self, commands: impl IntoIterator>) -> Self { + self.args.commands = string_from_iter(commands); + self + } + + /// Depends on these commands + pub fn with_depends_on(mut self, depends: impl IntoIterator>) -> Self { + self.args.depends_on = Some(string_from_iter(depends)); + self + } + + /// Execute the CLI command + pub fn execute(self) -> anyhow::Result<()> { + command::execute(command::Args { + operation: command::Operation::Add(self.args), + manifest_path: self.manifest_path, + }) + } +} + +pub struct CommandAliasBuilder { + pub manifest_path: Option, + pub args: command::AliasArgs, +} + +impl CommandAliasBuilder { + /// Depends on these commands + pub fn with_depends_on(mut self, depends: impl IntoIterator>) -> Self { + self.args.depends_on = string_from_iter(depends); + self + } + + /// Execute the CLI command + pub fn execute(self) -> anyhow::Result<()> { + command::execute(command::Args { + operation: command::Operation::Alias(self.args), + manifest_path: self.manifest_path, + }) + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f532244c2..a8b8f3715 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,21 +1,22 @@ #![allow(dead_code)] +pub mod builders; pub mod package_database; -use pixi::cli::add::SpecType; +use crate::common::builders::{AddBuilder, CommandAddBuilder, CommandAliasBuilder, InitBuilder}; +use pixi::cli::command::{AddArgs, AliasArgs}; use pixi::cli::install::Args; use pixi::cli::run::create_command; -use pixi::cli::{add, init, run}; +use pixi::cli::{add, command, init, run}; use pixi::{consts, Project}; use rattler_conda_types::conda_lock::CondaLock; use rattler_conda_types::{MatchSpec, Version}; -use std::future::{Future, IntoFuture}; + use std::path::{Path, PathBuf}; -use std::pin::Pin; + use std::process::Stdio; use std::str::FromStr; use tempfile::TempDir; -use url::Url; /// To control the pixi process pub struct PixiControl { @@ -39,11 +40,6 @@ impl RunResult { } } -/// MatchSpecs from an iterator -pub fn string_from_iter(iter: impl IntoIterator>) -> Vec { - iter.into_iter().map(|s| s.as_ref().to_string()).collect() -} - pub trait LockFileExt { /// Check if this package is contained in the lockfile fn contains_package(&self, name: impl AsRef) -> bool; @@ -120,6 +116,11 @@ impl PixiControl { } } + /// Access the command control, which allows to add and remove commands + pub fn command(&self) -> CommandControl { + CommandControl { pixi: self } + } + /// Run a command pub async fn run(&self, mut args: run::Args) -> anyhow::Result { args.manifest_path = args.manifest_path.or_else(|| Some(self.manifest_path())); @@ -146,80 +147,43 @@ impl PixiControl { } } -/// 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 { - args: init::Args, +pub struct CommandControl<'a> { + /// Reference to the pixi control + pixi: &'a PixiControl, } -impl InitBuilder { - pub fn with_channel(mut self, channel: impl ToString) -> Self { - self.args.channels.push(channel.to_string()); - self - } - - pub fn with_local_channel(self, channel: impl AsRef) -> Self { - self.with_channel(Url::from_directory_path(channel).unwrap()) - } -} - -// When `.await` is called on an object that is not a `Future` the compiler will first check if the -// type implements `IntoFuture`. If it does it will call the `IntoFuture::into_future()` method and -// await the resulting `Future`. We can abuse this behavior in builder patterns because the -// `into_future` method can also be used as a `finish` function. This allows you to reduce the -// required code. -impl IntoFuture for InitBuilder { - type Output = anyhow::Result<()>; - type IntoFuture = Pin + Send + 'static>>; - - fn into_future(self) -> Self::IntoFuture { - Box::pin(init::execute(self.args)) +impl CommandControl<'_> { + /// Add a command + pub fn add(&self, name: impl ToString) -> CommandAddBuilder { + CommandAddBuilder { + manifest_path: Some(self.pixi.manifest_path()), + args: AddArgs { + name: name.to_string(), + commands: vec![], + depends_on: None, + }, + } } -} -/// 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 { - args: add::Args, -} - -impl AddBuilder { - pub fn with_spec(mut self, spec: impl IntoMatchSpec) -> Self { - self.args.specs.push(spec.into()); - self + /// Remove a command + pub async fn remove(&self, name: impl ToString) -> anyhow::Result<()> { + command::execute(command::Args { + manifest_path: Some(self.pixi.manifest_path()), + operation: command::Operation::Remove(command::RemoveArgs { + name: name.to_string(), + }), + }) } - /// Set as a host - pub fn set_type(mut self, t: SpecType) -> Self { - match t { - SpecType::Host => { - self.args.host = true; - self.args.build = false; - } - SpecType::Build => { - self.args.host = false; - self.args.build = true; - } - SpecType::Run => { - self.args.host = false; - self.args.build = false; - } + /// Alias a command + pub fn alias(&self, name: impl ToString) -> CommandAliasBuilder { + CommandAliasBuilder { + manifest_path: Some(self.pixi.manifest_path()), + args: AliasArgs { + name: name.to_string(), + depends_on: vec![], + }, } - self - } -} - -// When `.await` is called on an object that is not a `Future` the compiler will first check if the -// type implements `IntoFuture`. If it does it will call the `IntoFuture::into_future()` method and -// await the resulting `Future`. We can abuse this behavior in builder patterns because the -// `into_future` method can also be used as a `finish` function. This allows you to reduce the -// required code. -impl IntoFuture for AddBuilder { - type Output = anyhow::Result<()>; - type IntoFuture = Pin + Send + 'static>>; - - fn into_future(self) -> Self::IntoFuture { - Box::pin(add::execute(self.args)) } } diff --git a/tests/install_tests.rs b/tests/install_tests.rs index c0977b958..3197d7769 100644 --- a/tests/install_tests.rs +++ b/tests/install_tests.rs @@ -1,7 +1,7 @@ mod common; +use crate::common::builders::string_from_iter; use crate::common::package_database::{Package, PackageDatabase}; -use crate::common::string_from_iter; use common::{LockFileExt, PixiControl}; use pixi::cli::run; use tempfile::TempDir; From 86cffd3dda0f8314e3ebfe30e1f572fbcdc70402 Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Tue, 4 Jul 2023 14:37:11 +0200 Subject: [PATCH 2/8] fix: improved testing and cli output for adding commands --- src/cli/command.rs | 30 ++++++++++++++++++++++++------ src/cli/init.rs | 2 +- src/project/mod.rs | 17 +++++++++++++++++ tests/command_tests.rs | 30 +++++++++++++++++------------- tests/common/mod.rs | 2 +- 5 files changed, 60 insertions(+), 21 deletions(-) diff --git a/src/cli/command.rs b/src/cli/command.rs index c16ffce7a..d59a1670e 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -37,7 +37,7 @@ pub struct AddArgs { #[derive(Parser, Debug)] pub struct AliasArgs { /// Alias name - pub name: String, + pub alias: String, /// Depends on these commands to execute pub depends_on: Vec, @@ -86,9 +86,27 @@ pub struct Args { pub fn execute(args: Args) -> anyhow::Result<()> { let mut project = Project::load_or_else_discover(args.manifest_path.as_deref())?; - match args.operation { - Operation::Add(args) => project.add_command(&args.name.clone(), args.into()), - Operation::Remove(args) => project.remove_command(&args.name), - Operation::Alias(args) => project.add_command(&args.name.clone(), args.into()), - } + let name = match args.operation { + Operation::Add(args) => { + let name = args.name.clone(); + project.add_command(&name, args.into())?; + name + } + Operation::Remove(args) => { + project.remove_command(&args.name)?; + args.name + } + Operation::Alias(args) => { + let name = args.alias.clone(); + project.add_command(&name, args.into())?; + name + } + }; + + eprintln!( + "{}Added command {}", + console::style(console::Emoji("✔ ", "")).green(), + &name, + ); + Ok(()) } diff --git a/src/cli/init.rs b/src/cli/init.rs index 0b5f58d3b..c5dfc6fac 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -141,7 +141,7 @@ mod tests { ); assert_eq!( get_dir(std::env::current_dir().unwrap()).unwrap(), - PathBuf::from(std::env::current_dir().unwrap().canonicalize().unwrap()) + std::env::current_dir().unwrap().canonicalize().unwrap() ); } diff --git a/src/project/mod.rs b/src/project/mod.rs index e57850aff..dd22aafd0 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -18,6 +18,7 @@ use std::{ env, fs, path::{Path, PathBuf}, }; + use toml_edit::{Array, Document, Item, Table, TomlError, Value}; /// A project represented by a pixi.toml file. @@ -107,6 +108,22 @@ impl Project { anyhow::anyhow!("commands in {} are malformed", consts::PROJECT_MANIFEST) })?; + let depends_on = match &command { + Command::Process(cmd) => cmd.depends_on.iter().collect(), + Command::Alias(alias) => alias.depends_on.iter().collect(), + _ => vec![], + }; + + for depends in depends_on { + if !self.manifest.commands.contains_key(depends) { + anyhow::bail!( + "depends_on {} for {} does not exist", + name.as_ref(), + depends + ); + } + } + // Add the command to the table commands_table.insert(name.as_ref(), command_as_toml(command.clone())); diff --git a/tests/command_tests.rs b/tests/command_tests.rs index 739674a0f..88a63c417 100644 --- a/tests/command_tests.rs +++ b/tests/command_tests.rs @@ -3,14 +3,14 @@ use crate::common::PixiControl; mod common; #[tokio::test] -pub async fn add_command() { +pub async fn add_remove_command() { let pixi = PixiControl::new().unwrap(); pixi.init().await.unwrap(); // Simple command pixi.command() .add("test") - .with_commands(&["echo hello"]) + .with_commands(["echo hello"]) .execute() .unwrap(); @@ -21,38 +21,42 @@ pub async fn add_command() { // Remove the command pixi.command().remove("test").await.unwrap(); assert_eq!(pixi.project().unwrap().manifest.commands.len(), 0); +} + +#[tokio::test] +pub async fn add_command_types() { + let pixi = PixiControl::new().unwrap(); + pixi.init().await.unwrap(); // Add a command with dependencies pixi.command() .add("test") - .with_commands(&["echo hello"]) - .with_depends_on(&["something_else"]) + .with_commands(["echo hello"]) .execute() .unwrap(); pixi.command() .add("test2") - .with_commands(&["echo hello", "Bonjour"]) + .with_commands(["echo hello", "echo bonjour"]) + .with_depends_on(["test"]) .execute() .unwrap(); let project = pixi.project().unwrap(); - let cmd = project.manifest.commands.get("test").unwrap(); - assert!(matches!(cmd, pixi::command::Command::Process(cmd) if cmd.depends_on.len() > 0)); - assert!( - matches!(cmd, pixi::command::Command::Process(cmd) if matches!(cmd.cmd, pixi::command::CmdArgs::Single(_))) - ); let cmd = project.manifest.commands.get("test2").unwrap(); assert!( - matches!(cmd, pixi::command::Command::Process(cmd) if matches!(cmd.cmd, pixi::command::CmdArgs::Multiple(_))) + matches!(cmd, pixi::command::Command::Process(cmd) if matches!(cmd.cmd, pixi::command::CmdArgs::Multiple(ref vec) if vec.len() == 2)) ); + assert!(matches!(cmd, pixi::command::Command::Process(cmd) if !cmd.depends_on.is_empty())); // Create an alias pixi.command() .alias("testing") - .with_depends_on(&["test"]) + .with_depends_on(["test"]) .execute() .unwrap(); let project = pixi.project().unwrap(); let cmd = project.manifest.commands.get("testing").unwrap(); - assert!(matches!(cmd, pixi::command::Command::Alias(a) if a.depends_on.len() > 0)); + assert!( + matches!(cmd, pixi::command::Command::Alias(a) if a.depends_on.get(0).unwrap() == "test") + ); } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 30b924f2c..2e840f774 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -205,7 +205,7 @@ impl CommandControl<'_> { CommandAliasBuilder { manifest_path: Some(self.pixi.manifest_path()), args: AliasArgs { - name: name.to_string(), + alias: name.to_string(), depends_on: vec![], }, } From 4f6b265c9b299cc4922c98c3179a02a28c698d17 Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Tue, 4 Jul 2023 15:41:55 +0200 Subject: [PATCH 3/8] feat: now uses single command whenever you expect it to --- src/cli/command.rs | 30 +++++++++++++----------------- src/project/mod.rs | 12 +++++++----- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/cli/command.rs b/src/cli/command.rs index d59a1670e..b7b1c4e81 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -45,21 +45,16 @@ pub struct AliasArgs { impl From for crate::command::Command { fn from(value: AddArgs) -> Self { - let first_command = value.commands.get(0).cloned().unwrap_or_default(); let depends_on = value.depends_on.unwrap_or_default(); - if value.commands.len() < 2 && depends_on.is_empty() { - return Self::Plain(first_command); + if depends_on.is_empty() { + Self::Plain(shlex::join(value.commands.iter().map(AsRef::as_ref))) + } else { + Self::Process(ProcessCmd { + cmd: CmdArgs::Single(shlex::join(value.commands.iter().map(AsRef::as_ref))), + depends_on, + }) } - - Self::Process(ProcessCmd { - cmd: if value.commands.len() == 1 { - CmdArgs::Single(first_command) - } else { - CmdArgs::Multiple(value.commands) - }, - depends_on, - }) } } @@ -86,26 +81,27 @@ pub struct Args { pub fn execute(args: Args) -> anyhow::Result<()> { let mut project = Project::load_or_else_discover(args.manifest_path.as_deref())?; - let name = match args.operation { + let (op, name) = match args.operation { Operation::Add(args) => { let name = args.name.clone(); project.add_command(&name, args.into())?; - name + ("Added", name) } Operation::Remove(args) => { project.remove_command(&args.name)?; - args.name + ("Added alias", args.name) } Operation::Alias(args) => { let name = args.alias.clone(); project.add_command(&name, args.into())?; - name + ("Removed", name) } }; eprintln!( - "{}Added command {}", + "{}{} command {}", console::style(console::Emoji("✔ ", "")).green(), + op, &name, ); Ok(()) diff --git a/src/project/mod.rs b/src/project/mod.rs index dd22aafd0..f89abfef4 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -44,10 +44,12 @@ fn command_as_toml(command: PixiCommand) -> Item { table.insert("cmd", Value::Array(Array::from_iter(cmd_strs.into_iter()))); } } - table.insert( - "depends_on", - Value::Array(Array::from_iter(process.depends_on.into_iter())), - ); + if !process.depends_on.is_empty() { + table.insert( + "depends_on", + Value::Array(Array::from_iter(process.depends_on.into_iter())), + ); + } Item::Value(Value::InlineTable(table)) } Command::Alias(alias) => { @@ -117,7 +119,7 @@ impl Project { for depends in depends_on { if !self.manifest.commands.contains_key(depends) { anyhow::bail!( - "depends_on {} for {} does not exist", + "depends_on '{}' for '{}' does not exist", name.as_ref(), depends ); From 894f67de73814c5b8a629c06f0fed9b833d47ba7 Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Tue, 4 Jul 2023 17:08:52 +0200 Subject: [PATCH 4/8] feat: now prints warning and doesnt quote if its a single argument --- src/cli/command.rs | 66 ++++++++++++++++++++++++++++++++---------- src/command/mod.rs | 43 +++++++++++++++++++++++++++ src/project/mod.rs | 32 +++++++++++++------- tests/command_tests.rs | 2 +- 4 files changed, 116 insertions(+), 27 deletions(-) diff --git a/src/cli/command.rs b/src/cli/command.rs index b7b1c4e81..ff814ffbf 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -1,6 +1,7 @@ -use crate::command::{AliasCmd, CmdArgs, ProcessCmd}; +use crate::command::{AliasCmd, CmdArgs, Command as PixiCommand, ProcessCmd}; use crate::Project; use clap::Parser; +use itertools::Itertools; use std::path::PathBuf; #[derive(Parser, Debug)] @@ -48,10 +49,18 @@ impl From for crate::command::Command { let depends_on = value.depends_on.unwrap_or_default(); if depends_on.is_empty() { - Self::Plain(shlex::join(value.commands.iter().map(AsRef::as_ref))) + Self::Plain(if value.commands.len() == 1 { + value.commands[0].clone() + } else { + shlex::join(value.commands.iter().map(AsRef::as_ref)) + }) } else { Self::Process(ProcessCmd { - cmd: CmdArgs::Single(shlex::join(value.commands.iter().map(AsRef::as_ref))), + cmd: CmdArgs::Single(if value.commands.len() == 1 { + value.commands[0].clone() + } else { + shlex::join(value.commands.iter().map(AsRef::as_ref)) + }), depends_on, }) } @@ -81,28 +90,53 @@ pub struct Args { pub fn execute(args: Args) -> anyhow::Result<()> { let mut project = Project::load_or_else_discover(args.manifest_path.as_deref())?; - let (op, name) = match args.operation { + match args.operation { Operation::Add(args) => { let name = args.name.clone(); - project.add_command(&name, args.into())?; - ("Added", name) + let command: PixiCommand = args.into(); + project.add_command(&name, command.clone())?; + eprintln!( + "{}Added command {}: {}", + console::style(console::Emoji("✔ ", "+")).green(), + console::style(&name).bold(), + command, + ); } Operation::Remove(args) => { - project.remove_command(&args.name)?; - ("Added alias", args.name) + let name = args.name; + project.remove_command(&name)?; + let depends_on = project.commands_depend_on(&name); + if !depends_on.is_empty() { + eprintln!( + "{}: {}", + console::style("Warning, the following commands depend on this command/s") + .yellow(), + console::style(depends_on.iter().to_owned().join(", ")).bold() + ); + eprintln!( + "{}", + console::style("Be sure to modify these after the removal\n").yellow() + ); + } + + eprintln!( + "{}Removed command {} ", + console::style(console::Emoji("❌ ", "X")).yellow(), + console::style(&name).bold(), + ); } Operation::Alias(args) => { let name = args.alias.clone(); - project.add_command(&name, args.into())?; - ("Removed", name) + let command: PixiCommand = args.into(); + project.add_command(&name, command.clone())?; + eprintln!( + "{} Added alias {}: {}", + console::style("@").blue(), + console::style(&name).bold(), + command, + ); } }; - eprintln!( - "{}{} command {}", - console::style(console::Emoji("✔ ", "")).green(), - op, - &name, - ); Ok(()) } diff --git a/src/command/mod.rs b/src/command/mod.rs index d75b9aab1..72eb5ceec 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -1,5 +1,7 @@ +use itertools::Itertools; use serde::Deserialize; use serde_with::{formats::PreferMany, serde_as, OneOrMany}; +use std::fmt::{Display, Formatter}; /// Represents different types of scripts #[derive(Debug, Clone, Deserialize)] @@ -10,6 +12,16 @@ pub enum Command { Alias(AliasCmd), } +impl Command { + pub fn depends_on(&self) -> &[String] { + match self { + Command::Plain(_) => &[], + Command::Process(cmd) => &cmd.depends_on, + Command::Alias(cmd) => &cmd.depends_on, + } + } +} + /// A command script executes a single command from the environment #[serde_as] #[derive(Debug, Clone, Deserialize)] @@ -39,3 +51,34 @@ pub struct AliasCmd { #[serde_as(deserialize_as = "OneOrMany<_, PreferMany>")] pub depends_on: Vec, } + +impl Display for Command { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Command::Plain(cmd) => { + write!(f, "{}", cmd)?; + } + Command::Process(cmd) => { + match &cmd.cmd { + CmdArgs::Single(cmd) => write!(f, "{}", cmd)?, + CmdArgs::Multiple(mult) => write!(f, "{}", mult.join(" "))?, + }; + if !cmd.depends_on.is_empty() { + write!(f, ", ")?; + } + } + _ => {} + }; + + let depends_on = self.depends_on(); + if !depends_on.is_empty() { + if depends_on.len() == 1 { + write!(f, "depends_on = '{}'", depends_on.iter().join(",")) + } else { + write!(f, "depends_on = [{}]", depends_on.iter().join(",")) + } + } else { + Ok(()) + } + } +} diff --git a/src/project/mod.rs b/src/project/mod.rs index f89abfef4..099d1565d 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -89,6 +89,21 @@ impl Project { }) } + /// Get a specific command + pub fn commands_depend_on(&self, name: impl AsRef) -> Vec<&String> { + let command = self.manifest.commands.get(name.as_ref()); + if command.is_some() { + self.manifest + .commands + .iter() + .filter(|(_, c)| c.depends_on().contains(&name.as_ref().to_string())) + .map(|(name, _)| name) + .collect() + } else { + vec![] + } + } + /// Add a command to the project pub fn add_command( &mut self, @@ -110,11 +125,7 @@ impl Project { anyhow::anyhow!("commands in {} are malformed", consts::PROJECT_MANIFEST) })?; - let depends_on = match &command { - Command::Process(cmd) => cmd.depends_on.iter().collect(), - Command::Alias(alias) => alias.depends_on.iter().collect(), - _ => vec![], - }; + let depends_on = command.depends_on(); for depends in depends_on { if !self.manifest.commands.contains_key(depends) { @@ -138,12 +149,13 @@ impl Project { Ok(()) } - /// Remove a command from the project + /// Remove a command from the project, and the commands it depends on pub fn remove_command(&mut self, name: impl AsRef) -> anyhow::Result<()> { - match self.manifest.commands.remove(name.as_ref()) { - None => anyhow::bail!("command {} does not exist", name.as_ref()), - Some(_) => {} - } + self.manifest + .commands + .get(name.as_ref()) + .ok_or_else(|| anyhow::anyhow!("command {} does not exist", name.as_ref()))?; + let commands_table = &mut self.doc["commands"]; if commands_table.is_none() { anyhow::bail!("internal data-structure inconsistent with toml"); diff --git a/tests/command_tests.rs b/tests/command_tests.rs index 88a63c417..9d7b504e2 100644 --- a/tests/command_tests.rs +++ b/tests/command_tests.rs @@ -44,7 +44,7 @@ pub async fn add_command_types() { let project = pixi.project().unwrap(); let cmd = project.manifest.commands.get("test2").unwrap(); assert!( - matches!(cmd, pixi::command::Command::Process(cmd) if matches!(cmd.cmd, pixi::command::CmdArgs::Multiple(ref vec) if vec.len() == 2)) + matches!(cmd, pixi::command::Command::Process(cmd) if matches!(cmd.cmd, pixi::command::CmdArgs::Single(_))) ); assert!(matches!(cmd, pixi::command::Command::Process(cmd) if !cmd.depends_on.is_empty())); From 7f861587d355f502d1b585404a18b69bf7e07015 Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Thu, 6 Jul 2023 14:26:13 +0200 Subject: [PATCH 5/8] feat: renamed command to task --- src/cli/mod.rs | 6 +- src/cli/run.rs | 66 +++++++-------- src/cli/{command.rs => task.rs} | 35 ++++---- src/lib.rs | 2 +- src/project/manifest.rs | 6 +- src/project/mod.rs | 82 +++++++++---------- ...ect__manifest__test__dependency_types.snap | 3 +- ...ject__manifest__test__target_specific.snap | 3 +- src/{command => task}/mod.rs | 24 +++--- tests/common/builders.rs | 22 ++--- tests/common/mod.rs | 50 +++++------ tests/install_tests.rs | 2 +- tests/{command_tests.rs => task_tests.rs} | 37 ++++----- 13 files changed, 160 insertions(+), 178 deletions(-) rename src/cli/{command.rs => task.rs} (78%) rename src/{command => task}/mod.rs (84%) rename tests/{command_tests.rs => task_tests.rs} (50%) diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 01e9bdcf3..2643162d2 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -9,12 +9,12 @@ use tracing_subscriber::{filter::LevelFilter, util::SubscriberInitExt, EnvFilter pub mod add; pub mod auth; -pub mod command; pub mod global; pub mod init; pub mod install; pub mod run; pub mod shell; +pub mod task; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -53,7 +53,7 @@ pub enum Command { #[clap(alias = "i")] Install(install::Args), #[clap(alias = "c")] - Command(command::Args), + Command(task::Args), } fn completion(args: CompletionCommand) -> Result<(), Error> { @@ -108,6 +108,6 @@ pub async fn execute_command(command: Command) -> Result<(), Error> { Command::Auth(cmd) => auth::execute(cmd).await, Command::Install(cmd) => install::execute(cmd).await, Command::Shell(cmd) => shell::execute(cmd).await, - Command::Command(cmd) => command::execute(cmd), + Command::Command(cmd) => task::execute(cmd), } } diff --git a/src/cli/run.rs b/src/cli/run.rs index 6224a47c1..7f7f5876f 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -12,68 +12,62 @@ use rattler_conda_types::Platform; use crate::prefix::Prefix; use crate::progress::await_in_progress; use crate::project::environment::get_metadata_env; -use crate::{ - command::{CmdArgs, Command, ProcessCmd}, - environment::get_up_to_date_prefix, - Project, -}; +use crate::task::{CmdArgs, Execute, Task}; +use crate::{environment::get_up_to_date_prefix, Project}; use rattler_shell::{ activation::{ActivationVariables, Activator, PathModificationBehaviour}, shell::ShellEnum, }; -/// Runs command in project. +/// Runs task in project. #[derive(Parser, Debug, Default)] #[clap(trailing_var_arg = true, arg_required_else_help = true)] pub struct Args { - /// The command you want to run in the projects environment. - pub command: Vec, + /// The task you want to run in the projects environment. + pub task: Vec, /// The path to 'pixi.toml' #[arg(long)] pub manifest_path: Option, } -pub fn order_commands( - commands: Vec, - project: &Project, -) -> anyhow::Result> { - let command: Vec<_> = commands.iter().map(|c| c.to_string()).collect(); +pub fn order_tasks(tasks: Vec, project: &Project) -> anyhow::Result> { + let tasks: Vec<_> = tasks.iter().map(|c| c.to_string()).collect(); - let (command_name, command) = command + let (task_name, task) = tasks .first() .and_then(|cmd_name| { project - .command_opt(cmd_name) + .task_opt(cmd_name) .map(|cmd| (Some(cmd_name.clone()), cmd.clone())) }) .unwrap_or_else(|| { ( None, - Command::Process(ProcessCmd { - cmd: CmdArgs::Multiple(commands), + Task::Execute(Execute { + cmd: CmdArgs::Multiple(tasks), depends_on: vec![], }), ) }); - // Perform post order traversal of the commands and their `depends_on` to make sure they are + // Perform post order traversal of the tasks and their `depends_on` to make sure they are // executed in the right order. let mut s1 = VecDeque::new(); let mut s2 = VecDeque::new(); let mut added = HashSet::new(); - // Add the command specified on the command line first - s1.push_back(command); - if let Some(command_name) = command_name { + // Add the task specified on the command line first + s1.push_back(task); + if let Some(command_name) = task_name { added.insert(command_name); } - while let Some(command) = s1.pop_back() { + while let Some(task) = s1.pop_back() { // Get the dependencies of the command - let depends_on = match &command { - Command::Process(process) => process.depends_on.as_slice(), - Command::Alias(alias) => &alias.depends_on, + let depends_on = match &task { + Task::Execute(process) => process.depends_on.as_slice(), + Task::Alias(alias) => &alias.depends_on, _ => &[], }; @@ -81,7 +75,7 @@ pub fn order_commands( for dependency in depends_on.iter() { if !added.contains(dependency) { let cmd = project - .command_opt(dependency) + .task_opt(dependency) .ok_or_else(|| anyhow::anyhow!("failed to find dependency {}", dependency))? .clone(); @@ -90,27 +84,27 @@ pub fn order_commands( } } - s2.push_back(command) + s2.push_back(task) } Ok(s2) } -pub async fn create_command( - command: Command, +pub async fn create_task( + command: Task, project: &Project, command_env: &HashMap, ) -> anyhow::Result> { // Command arguments let args = match command { - Command::Process(ProcessCmd { + Task::Execute(Execute { cmd: CmdArgs::Single(cmd), .. }) - | Command::Plain(cmd) => { + | Task::Plain(cmd) => { shlex::split(&cmd).ok_or_else(|| anyhow::anyhow!("invalid quoted command arguments"))? } - Command::Process(ProcessCmd { + Task::Execute(Execute { cmd: CmdArgs::Multiple(cmd), .. }) => cmd, @@ -145,14 +139,14 @@ pub async fn execute(args: Args) -> anyhow::Result<()> { let project = Project::load_or_else_discover(args.manifest_path.as_deref())?; // Get the correctly ordered commands - let mut ordered_commands = order_commands(args.command, &project)?; + let mut ordered_commands = order_tasks(args.task, &project)?; // Get the environment to run the commands in. - let command_env = get_command_env(&project).await?; + let command_env = get_task_env(&project).await?; // Execute the commands in the correct order while let Some(command) = ordered_commands.pop_back() { - if let Some(mut command) = create_command(command, &project, &command_env).await? { + if let Some(mut command) = create_task(command, &project, &command_env).await? { let status = command.spawn()?.wait()?.code().unwrap_or(1); if status != 0 { std::process::exit(status); @@ -164,7 +158,7 @@ pub async fn execute(args: Args) -> anyhow::Result<()> { } /// Determine the environment variables to use when executing a command. -pub async fn get_command_env(project: &Project) -> anyhow::Result> { +pub async fn get_task_env(project: &Project) -> anyhow::Result> { // Get the prefix which we can then activate. let prefix = get_up_to_date_prefix(project).await?; diff --git a/src/cli/command.rs b/src/cli/task.rs similarity index 78% rename from src/cli/command.rs rename to src/cli/task.rs index ff814ffbf..3aa1918c9 100644 --- a/src/cli/command.rs +++ b/src/cli/task.rs @@ -1,4 +1,4 @@ -use crate::command::{AliasCmd, CmdArgs, Command as PixiCommand, ProcessCmd}; +use crate::task::{Alias, CmdArgs, Execute, Task}; use crate::Project; use clap::Parser; use itertools::Itertools; @@ -44,7 +44,7 @@ pub struct AliasArgs { pub depends_on: Vec, } -impl From for crate::command::Command { +impl From for Task { fn from(value: AddArgs) -> Self { let depends_on = value.depends_on.unwrap_or_default(); @@ -55,7 +55,7 @@ impl From for crate::command::Command { shlex::join(value.commands.iter().map(AsRef::as_ref)) }) } else { - Self::Process(ProcessCmd { + Self::Execute(Execute { cmd: CmdArgs::Single(if value.commands.len() == 1 { value.commands[0].clone() } else { @@ -67,9 +67,9 @@ impl From for crate::command::Command { } } -impl From for crate::command::Command { +impl From for Task { fn from(value: AliasArgs) -> Self { - Self::Alias(AliasCmd { + Self::Alias(Alias { depends_on: value.depends_on, }) } @@ -79,7 +79,7 @@ impl From for crate::command::Command { #[derive(Parser, Debug)] #[clap(trailing_var_arg = true, arg_required_else_help = true)] pub struct Args { - /// Add, remove, or update a command + /// Add, remove, or update a task #[clap(subcommand)] pub operation: Operation, @@ -93,24 +93,23 @@ pub fn execute(args: Args) -> anyhow::Result<()> { match args.operation { Operation::Add(args) => { let name = args.name.clone(); - let command: PixiCommand = args.into(); - project.add_command(&name, command.clone())?; + let task: Task = args.into(); + project.add_task(&name, task.clone())?; eprintln!( - "{}Added command {}: {}", + "{}Added task {}: {}", console::style(console::Emoji("✔ ", "+")).green(), console::style(&name).bold(), - command, + task, ); } Operation::Remove(args) => { let name = args.name; - project.remove_command(&name)?; - let depends_on = project.commands_depend_on(&name); + project.remove_task(&name)?; + let depends_on = project.task_depends_on(&name); if !depends_on.is_empty() { eprintln!( "{}: {}", - console::style("Warning, the following commands depend on this command/s") - .yellow(), + console::style("Warning, the following task/s depend on this task").yellow(), console::style(depends_on.iter().to_owned().join(", ")).bold() ); eprintln!( @@ -120,20 +119,20 @@ pub fn execute(args: Args) -> anyhow::Result<()> { } eprintln!( - "{}Removed command {} ", + "{}Removed task {} ", console::style(console::Emoji("❌ ", "X")).yellow(), console::style(&name).bold(), ); } Operation::Alias(args) => { let name = args.alias.clone(); - let command: PixiCommand = args.into(); - project.add_command(&name, command.clone())?; + let task: Task = args.into(); + project.add_task(&name, task.clone())?; eprintln!( "{} Added alias {}: {}", console::style("@").blue(), console::style(&name).bold(), - command, + task, ); } }; diff --git a/src/lib.rs b/src/lib.rs index 60696b1a6..acceaf6da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ pub mod cli; -pub mod command; pub mod config; pub mod consts; pub mod environment; @@ -8,6 +7,7 @@ pub mod progress; pub mod project; pub mod repodata; pub mod report_error; +pub mod task; pub mod util; pub mod virtual_packages; diff --git a/src/project/manifest.rs b/src/project/manifest.rs index c59a0b987..ddd723cb9 100644 --- a/src/project/manifest.rs +++ b/src/project/manifest.rs @@ -1,6 +1,6 @@ -use crate::command::Command; use crate::consts::PROJECT_MANIFEST; use crate::report_error::ReportError; +use crate::task::Task; use ::serde::Deserialize; use ariadne::{ColorGenerator, Fmt, Label, Report, ReportKind, Source}; use indexmap::IndexMap; @@ -20,9 +20,9 @@ pub struct ProjectManifest { /// Information about the project pub project: ProjectMetadata, - /// Commands defined in the project + /// Tasks defined in the project #[serde(default)] - pub commands: HashMap, + pub tasks: HashMap, /// Additional system requirements #[serde(default, rename = "system-requirements")] diff --git a/src/project/mod.rs b/src/project/mod.rs index 099d1565d..bc4001e80 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -2,11 +2,11 @@ pub mod environment; pub mod manifest; mod serde; -use crate::command::{CmdArgs, Command as PixiCommand, Command}; use crate::consts; use crate::consts::PROJECT_MANIFEST; use crate::project::manifest::{ProjectManifest, TargetMetadata, TargetSelector}; use crate::report_error::ReportError; +use crate::task::{CmdArgs, Task}; use anyhow::Context; use ariadne::{Label, Report, ReportKind, Source}; use indexmap::IndexMap; @@ -30,11 +30,11 @@ pub struct Project { pub manifest: ProjectManifest, } -/// Returns a command a a toml item -fn command_as_toml(command: PixiCommand) -> Item { - match command { - Command::Plain(str) => Item::Value(str.into()), - Command::Process(process) => { +/// Returns a task a a toml item +fn task_as_toml(task: Task) -> Item { + match task { + Task::Plain(str) => Item::Value(str.into()), + Task::Execute(process) => { let mut table = Table::new().into_inline_table(); match process.cmd { CmdArgs::Single(cmd_str) => { @@ -52,7 +52,7 @@ fn command_as_toml(command: PixiCommand) -> Item { } Item::Value(Value::InlineTable(table)) } - Command::Alias(alias) => { + Task::Alias(alias) => { let mut table = Table::new().into_inline_table(); table.insert( "depends_on", @@ -89,12 +89,12 @@ impl Project { }) } - /// Get a specific command - pub fn commands_depend_on(&self, name: impl AsRef) -> Vec<&String> { - let command = self.manifest.commands.get(name.as_ref()); - if command.is_some() { + /// Find task dependencies + pub fn task_depends_on(&self, name: impl AsRef) -> Vec<&String> { + let task = self.manifest.tasks.get(name.as_ref()); + if task.is_some() { self.manifest - .commands + .tasks .iter() .filter(|(_, c)| c.depends_on().contains(&name.as_ref().to_string())) .map(|(name, _)| name) @@ -104,31 +104,27 @@ impl Project { } } - /// Add a command to the project - pub fn add_command( - &mut self, - name: impl AsRef, - command: PixiCommand, - ) -> anyhow::Result<()> { - if self.manifest.commands.contains_key(name.as_ref()) { - anyhow::bail!("command {} already exists", name.as_ref()); + /// Add a task to the project + pub fn add_task(&mut self, name: impl AsRef, task: Task) -> anyhow::Result<()> { + if self.manifest.tasks.contains_key(name.as_ref()) { + anyhow::bail!("task {} already exists", name.as_ref()); }; - let commands_table = &mut self.doc["commands"]; + let tasks_table = &mut self.doc["tasks"]; // If it doesnt exist create a proper table - if commands_table.is_none() { - *commands_table = Item::Table(Table::new()); + if tasks_table.is_none() { + *tasks_table = Item::Table(Table::new()); } // Cast the item into a table - let commands_table = commands_table.as_table_like_mut().ok_or_else(|| { - anyhow::anyhow!("commands in {} are malformed", consts::PROJECT_MANIFEST) + let tasks_table = tasks_table.as_table_like_mut().ok_or_else(|| { + anyhow::anyhow!("tasks in {} are malformed", consts::PROJECT_MANIFEST) })?; - let depends_on = command.depends_on(); + let depends_on = task.depends_on(); for depends in depends_on { - if !self.manifest.commands.contains_key(depends) { + if !self.manifest.tasks.contains_key(depends) { anyhow::bail!( "depends_on '{}' for '{}' does not exist", name.as_ref(), @@ -137,35 +133,33 @@ impl Project { } } - // Add the command to the table - commands_table.insert(name.as_ref(), command_as_toml(command.clone())); + // Add the task to the table + tasks_table.insert(name.as_ref(), task_as_toml(task.clone())); - self.manifest - .commands - .insert(name.as_ref().to_string(), command); + self.manifest.tasks.insert(name.as_ref().to_string(), task); self.save()?; Ok(()) } - /// Remove a command from the project, and the commands it depends on - pub fn remove_command(&mut self, name: impl AsRef) -> anyhow::Result<()> { + /// Remove a task from the project, and the tasks that depend on it + pub fn remove_task(&mut self, name: impl AsRef) -> anyhow::Result<()> { self.manifest - .commands + .tasks .get(name.as_ref()) - .ok_or_else(|| anyhow::anyhow!("command {} does not exist", name.as_ref()))?; + .ok_or_else(|| anyhow::anyhow!("task {} does not exist", name.as_ref()))?; - let commands_table = &mut self.doc["commands"]; - if commands_table.is_none() { + let tasks_table = &mut self.doc["tasks"]; + if tasks_table.is_none() { anyhow::bail!("internal data-structure inconsistent with toml"); } - let commands_table = commands_table.as_table_like_mut().ok_or_else(|| { - anyhow::anyhow!("commands in {} are malformed", consts::PROJECT_MANIFEST) + let tasks_table = tasks_table.as_table_like_mut().ok_or_else(|| { + anyhow::anyhow!("tasks in {} are malformed", consts::PROJECT_MANIFEST) })?; // If it does not exist in toml, consider this ok as we want to remove it anyways - commands_table.remove(name.as_ref()); + tasks_table.remove(name.as_ref()); self.save()?; @@ -503,9 +497,9 @@ impl Project { self.manifest.project.platforms.as_ref().as_slice() } - /// Get the command with the specified name or `None` if no such command exists. - pub fn command_opt(&self, name: &str) -> Option<&crate::command::Command> { - self.manifest.commands.get(name) + /// Get the task with the specified name or `None` if no such task exists. + pub fn task_opt(&self, name: &str) -> Option<&Task> { + self.manifest.tasks.get(name) } /// Get the system requirements defined under the `system-requirements` section of the project manifest. diff --git a/src/project/snapshots/pixi__project__manifest__test__dependency_types.snap b/src/project/snapshots/pixi__project__manifest__test__dependency_types.snap index bc333aad1..98dbf77d5 100644 --- a/src/project/snapshots/pixi__project__manifest__test__dependency_types.snap +++ b/src/project/snapshots/pixi__project__manifest__test__dependency_types.snap @@ -1,6 +1,5 @@ --- source: src/project/manifest.rs -assertion_line: 337 expression: "toml_edit::de::from_str::(&contents).expect(\"parsing should succeed!\")" --- ProjectManifest { @@ -21,7 +20,7 @@ ProjectManifest { value: [], }, }, - commands: {}, + tasks: {}, system_requirements: SystemRequirements { windows: None, unix: None, diff --git a/src/project/snapshots/pixi__project__manifest__test__target_specific.snap b/src/project/snapshots/pixi__project__manifest__test__target_specific.snap index 837d56457..6bb1ba1a0 100644 --- a/src/project/snapshots/pixi__project__manifest__test__target_specific.snap +++ b/src/project/snapshots/pixi__project__manifest__test__target_specific.snap @@ -1,6 +1,5 @@ --- source: src/project/manifest.rs -assertion_line: 316 expression: "toml_edit::de::from_str::(&contents).expect(\"parsing should succeed!\")" --- ProjectManifest { @@ -21,7 +20,7 @@ ProjectManifest { value: [], }, }, - commands: {}, + tasks: {}, system_requirements: SystemRequirements { windows: None, unix: None, diff --git a/src/command/mod.rs b/src/task/mod.rs similarity index 84% rename from src/command/mod.rs rename to src/task/mod.rs index 72eb5ceec..0575acef7 100644 --- a/src/command/mod.rs +++ b/src/task/mod.rs @@ -6,18 +6,18 @@ use std::fmt::{Display, Formatter}; /// Represents different types of scripts #[derive(Debug, Clone, Deserialize)] #[serde(untagged)] -pub enum Command { +pub enum Task { Plain(String), - Process(ProcessCmd), - Alias(AliasCmd), + Execute(Execute), + Alias(Alias), } -impl Command { +impl Task { pub fn depends_on(&self) -> &[String] { match self { - Command::Plain(_) => &[], - Command::Process(cmd) => &cmd.depends_on, - Command::Alias(cmd) => &cmd.depends_on, + Task::Plain(_) => &[], + Task::Execute(cmd) => &cmd.depends_on, + Task::Alias(cmd) => &cmd.depends_on, } } } @@ -25,7 +25,7 @@ impl Command { /// A command script executes a single command from the environment #[serde_as] #[derive(Debug, Clone, Deserialize)] -pub struct ProcessCmd { +pub struct Execute { // A list of arguments, the first argument denotes the command to run. When deserializing both // an array of strings and a single string are supported. pub cmd: CmdArgs, @@ -46,19 +46,19 @@ pub enum CmdArgs { #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] #[serde_as] -pub struct AliasCmd { +pub struct Alias { /// A list of commands that should be run before this one #[serde_as(deserialize_as = "OneOrMany<_, PreferMany>")] pub depends_on: Vec, } -impl Display for Command { +impl Display for Task { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - Command::Plain(cmd) => { + Task::Plain(cmd) => { write!(f, "{}", cmd)?; } - Command::Process(cmd) => { + Task::Execute(cmd) => { match &cmd.cmd { CmdArgs::Single(cmd) => write!(f, "{}", cmd)?, CmdArgs::Multiple(mult) => write!(f, "{}", mult.join(" "))?, diff --git a/tests/common/builders.rs b/tests/common/builders.rs index 37fead50f..ac03ad642 100644 --- a/tests/common/builders.rs +++ b/tests/common/builders.rs @@ -25,7 +25,7 @@ use crate::common::IntoMatchSpec; use pixi::cli::add::SpecType; -use pixi::cli::{add, command, init}; +use pixi::cli::{add, init, task}; use std::future::{Future, IntoFuture}; use std::path::{Path, PathBuf}; use std::pin::Pin; @@ -103,12 +103,12 @@ impl IntoFuture for AddBuilder { } } -pub struct CommandAddBuilder { +pub struct TaskAddBuilder { pub manifest_path: Option, - pub args: command::AddArgs, + pub args: task::AddArgs, } -impl CommandAddBuilder { +impl TaskAddBuilder { /// Execute these commands pub fn with_commands(mut self, commands: impl IntoIterator>) -> Self { self.args.commands = string_from_iter(commands); @@ -123,19 +123,19 @@ impl CommandAddBuilder { /// Execute the CLI command pub fn execute(self) -> anyhow::Result<()> { - command::execute(command::Args { - operation: command::Operation::Add(self.args), + task::execute(task::Args { + operation: task::Operation::Add(self.args), manifest_path: self.manifest_path, }) } } -pub struct CommandAliasBuilder { +pub struct TaskAliasBuilder { pub manifest_path: Option, - pub args: command::AliasArgs, + pub args: task::AliasArgs, } -impl CommandAliasBuilder { +impl TaskAliasBuilder { /// Depends on these commands pub fn with_depends_on(mut self, depends: impl IntoIterator>) -> Self { self.args.depends_on = string_from_iter(depends); @@ -144,8 +144,8 @@ impl CommandAliasBuilder { /// Execute the CLI command pub fn execute(self) -> anyhow::Result<()> { - command::execute(command::Args { - operation: command::Operation::Alias(self.args), + task::execute(task::Args { + operation: task::Operation::Alias(self.args), manifest_path: self.manifest_path, }) } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 2e840f774..2c8a92821 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -3,11 +3,11 @@ pub mod builders; pub mod package_database; -use crate::common::builders::{AddBuilder, CommandAddBuilder, CommandAliasBuilder, InitBuilder}; -use pixi::cli::command::{AddArgs, AliasArgs}; +use crate::common::builders::{AddBuilder, InitBuilder, TaskAddBuilder, TaskAliasBuilder}; use pixi::cli::install::Args; -use pixi::cli::run::{create_command, get_command_env, order_commands}; -use pixi::cli::{add, command, init, run}; +use pixi::cli::run::{create_task, get_task_env, order_tasks}; +use pixi::cli::task::{AddArgs, AliasArgs}; +use pixi::cli::{add, init, run, task}; use pixi::{consts, Project}; use rattler_conda_types::conda_lock::CondaLock; use rattler_conda_types::{MatchSpec, Version}; @@ -117,30 +117,30 @@ impl PixiControl { } } - /// Access the command control, which allows to add and remove commands - pub fn command(&self) -> CommandControl { - CommandControl { pixi: self } + /// Access the tasks control, which allows to add and remove tasks + pub fn tasks(&self) -> TasksControl { + TasksControl { pixi: self } } - /// Run a command + /// Run a tasks pub async fn run(&self, mut args: run::Args) -> anyhow::Result> { args.manifest_path = args.manifest_path.or_else(|| Some(self.manifest_path())); - let mut commands = order_commands(args.command, &self.project().unwrap())?; + let mut tasks = order_tasks(args.task, &self.project().unwrap())?; let project = self.project().unwrap(); - let command_env = get_command_env(&project).await.unwrap(); + let task_env = get_task_env(&project).await.unwrap(); let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); tokio::spawn(async move { - while let Some(command) = commands.pop_back() { - let command = create_command(command, &project, &command_env) + while let Some(task) = tasks.pop_back() { + let task = create_task(task, &project, &task_env) .await .expect("could not create command"); - if let Some(mut command) = command { + if let Some(mut task) = task { let tx = tx.clone(); spawn_blocking(move || { - let output = command + let output = task .stdout(Stdio::piped()) .spawn() .expect("could not spawn task") @@ -172,15 +172,15 @@ impl PixiControl { } } -pub struct CommandControl<'a> { +pub struct TasksControl<'a> { /// Reference to the pixi control pixi: &'a PixiControl, } -impl CommandControl<'_> { - /// Add a command - pub fn add(&self, name: impl ToString) -> CommandAddBuilder { - CommandAddBuilder { +impl TasksControl<'_> { + /// Add a task + pub fn add(&self, name: impl ToString) -> TaskAddBuilder { + TaskAddBuilder { manifest_path: Some(self.pixi.manifest_path()), args: AddArgs { name: name.to_string(), @@ -190,19 +190,19 @@ impl CommandControl<'_> { } } - /// Remove a command + /// Remove a task pub async fn remove(&self, name: impl ToString) -> anyhow::Result<()> { - command::execute(command::Args { + task::execute(task::Args { manifest_path: Some(self.pixi.manifest_path()), - operation: command::Operation::Remove(command::RemoveArgs { + operation: task::Operation::Remove(task::RemoveArgs { name: name.to_string(), }), }) } - /// Alias a command - pub fn alias(&self, name: impl ToString) -> CommandAliasBuilder { - CommandAliasBuilder { + /// Alias one or multiple tasks + pub fn alias(&self, name: impl ToString) -> TaskAliasBuilder { + TaskAliasBuilder { manifest_path: Some(self.pixi.manifest_path()), args: AliasArgs { alias: name.to_string(), diff --git a/tests/install_tests.rs b/tests/install_tests.rs index 2d0693f6f..fcfa580fc 100644 --- a/tests/install_tests.rs +++ b/tests/install_tests.rs @@ -22,7 +22,7 @@ async fn install_run_python() { // Check if python is installed and can be run let mut result = pixi .run(run::Args { - command: string_from_iter(["python", "--version"]), + task: string_from_iter(["python", "--version"]), ..Default::default() }) .await diff --git a/tests/command_tests.rs b/tests/task_tests.rs similarity index 50% rename from tests/command_tests.rs rename to tests/task_tests.rs index 9d7b504e2..d73058324 100644 --- a/tests/command_tests.rs +++ b/tests/task_tests.rs @@ -1,26 +1,27 @@ use crate::common::PixiControl; +use pixi::task::{CmdArgs, Task}; mod common; #[tokio::test] -pub async fn add_remove_command() { +pub async fn add_remove_task() { let pixi = PixiControl::new().unwrap(); pixi.init().await.unwrap(); - // Simple command - pixi.command() + // Simple task + pixi.tasks() .add("test") .with_commands(["echo hello"]) .execute() .unwrap(); let project = pixi.project().unwrap(); - let cmd = project.manifest.commands.get("test").unwrap(); - assert!(matches!(cmd, pixi::command::Command::Plain(s) if s == "echo hello")); + let task = project.manifest.tasks.get("test").unwrap(); + assert!(matches!(task, Task::Plain(s) if s == "echo hello")); - // Remove the command - pixi.command().remove("test").await.unwrap(); - assert_eq!(pixi.project().unwrap().manifest.commands.len(), 0); + // Remove the task + pixi.tasks().remove("test").await.unwrap(); + assert_eq!(pixi.project().unwrap().manifest.tasks.len(), 0); } #[tokio::test] @@ -29,12 +30,12 @@ pub async fn add_command_types() { pixi.init().await.unwrap(); // Add a command with dependencies - pixi.command() + pixi.tasks() .add("test") .with_commands(["echo hello"]) .execute() .unwrap(); - pixi.command() + pixi.tasks() .add("test2") .with_commands(["echo hello", "echo bonjour"]) .with_depends_on(["test"]) @@ -42,21 +43,17 @@ pub async fn add_command_types() { .unwrap(); let project = pixi.project().unwrap(); - let cmd = project.manifest.commands.get("test2").unwrap(); - assert!( - matches!(cmd, pixi::command::Command::Process(cmd) if matches!(cmd.cmd, pixi::command::CmdArgs::Single(_))) - ); - assert!(matches!(cmd, pixi::command::Command::Process(cmd) if !cmd.depends_on.is_empty())); + let task = project.manifest.tasks.get("test2").unwrap(); + assert!(matches!(task, Task::Execute(cmd) if matches!(cmd.cmd, CmdArgs::Single(_)))); + assert!(matches!(task, Task::Execute(cmd) if !cmd.depends_on.is_empty())); // Create an alias - pixi.command() + pixi.tasks() .alias("testing") .with_depends_on(["test"]) .execute() .unwrap(); let project = pixi.project().unwrap(); - let cmd = project.manifest.commands.get("testing").unwrap(); - assert!( - matches!(cmd, pixi::command::Command::Alias(a) if a.depends_on.get(0).unwrap() == "test") - ); + let task = project.manifest.tasks.get("testing").unwrap(); + assert!(matches!(task, Task::Alias(a) if a.depends_on.get(0).unwrap() == "test")); } From ac6ea8629fb85ab9fa7f5a4407c57949cf9fc433 Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Thu, 6 Jul 2023 15:20:20 +0200 Subject: [PATCH 6/8] feat: accepts 1..n args for specific arguments --- src/cli/init.rs | 2 +- src/cli/mod.rs | 5 ++--- src/cli/task.rs | 12 +++++++++--- src/project/mod.rs | 4 ++-- src/task/mod.rs | 4 ++-- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/cli/init.rs b/src/cli/init.rs index c5dfc6fac..f8460fad2 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -31,7 +31,7 @@ authors = ["{{ author[0] }} <{{ author[1] }}>"] channels = ["{{ channels|join("\", \"") }}"] platforms = ["{{ platform }}"] -[commands] +[tasks] [dependencies] "#; diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 2643162d2..5a0643edb 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -52,8 +52,7 @@ pub enum Command { Auth(auth::Args), #[clap(alias = "i")] Install(install::Args), - #[clap(alias = "c")] - Command(task::Args), + Task(task::Args), } fn completion(args: CompletionCommand) -> Result<(), Error> { @@ -108,6 +107,6 @@ pub async fn execute_command(command: Command) -> Result<(), Error> { Command::Auth(cmd) => auth::execute(cmd).await, Command::Install(cmd) => install::execute(cmd).await, Command::Shell(cmd) => shell::execute(cmd).await, - Command::Command(cmd) => task::execute(cmd), + Command::Task(cmd) => task::execute(cmd), } } diff --git a/src/cli/task.rs b/src/cli/task.rs index 3aa1918c9..6602e7c2f 100644 --- a/src/cli/task.rs +++ b/src/cli/task.rs @@ -17,30 +17,36 @@ pub enum Operation { } #[derive(Parser, Debug)] +#[clap(arg_required_else_help = true)] pub struct RemoveArgs { - /// Command name + /// Task name pub name: String, } #[derive(Parser, Debug)] +#[clap(arg_required_else_help = true)] pub struct AddArgs { - /// Command name + /// Task name pub name: String, /// One or more commands to actually execute + #[clap(required = true, num_args = 1..)] pub commands: Vec, /// Depends on these other commands #[clap(long)] + #[clap(num_args = 1..)] pub depends_on: Option>, } #[derive(Parser, Debug)] +#[clap(arg_required_else_help = true)] pub struct AliasArgs { /// Alias name pub alias: String, - /// Depends on these commands to execute + /// Depends on these tasks to execute + #[clap(required = true, num_args = 1..)] pub depends_on: Vec, } diff --git a/src/project/mod.rs b/src/project/mod.rs index bc4001e80..7a624db38 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -126,9 +126,9 @@ impl Project { for depends in depends_on { if !self.manifest.tasks.contains_key(depends) { anyhow::bail!( - "depends_on '{}' for '{}' does not exist", + "task '{}' for the depends on for '{}' does not exist", + depends, name.as_ref(), - depends ); } } diff --git a/src/task/mod.rs b/src/task/mod.rs index 0575acef7..85eb9b30f 100644 --- a/src/task/mod.rs +++ b/src/task/mod.rs @@ -26,8 +26,8 @@ impl Task { #[serde_as] #[derive(Debug, Clone, Deserialize)] pub struct Execute { - // A list of arguments, the first argument denotes the command to run. When deserializing both - // an array of strings and a single string are supported. + /// A list of arguments, the first argument denotes the command to run. When deserializing both + /// an array of strings and a single string are supported. pub cmd: CmdArgs, /// A list of commands that should be run before this one From 8fec93da5f127046cc14ca5c6abd83a0972db304 Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Thu, 6 Jul 2023 17:02:30 +0200 Subject: [PATCH 7/8] feat: added to docs --- docs/cli.md | 1 + examples/cpp-sdl/pixi.toml | 2 +- examples/flask-hello-world/pixi.toml | 2 +- examples/opencv/pixi.toml | 2 +- examples/turtlesim/pixi.toml | 2 +- src/cli/task.rs | 57 ++++++++++++++++++---------- tests/common/mod.rs | 2 +- 7 files changed, 44 insertions(+), 24 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 3cf5d99a1..110a18270 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -10,6 +10,7 @@ With `pixi` you can install packages in global space or local to the environment | `install` | Installs all dependencies of the project in its environment | | `run` | Runs the given command in a project's environment | | `shell` | Starts a shell in the project's environment | +| `tasks` | Manage tasks in your `pixi.toml` file | ### Initialize a new project This command is used to create a new project. diff --git a/examples/cpp-sdl/pixi.toml b/examples/cpp-sdl/pixi.toml index 35948a32f..8c4cb9ab0 100644 --- a/examples/cpp-sdl/pixi.toml +++ b/examples/cpp-sdl/pixi.toml @@ -6,7 +6,7 @@ authors = ["Bas Zalmstra "] channels = ["conda-forge"] platforms = ["win-64", "linux-64", "osx-64", "osx-arm64"] -[commands] +[tasks] # Configures CMake configure = { cmd = [ "cmake", diff --git a/examples/flask-hello-world/pixi.toml b/examples/flask-hello-world/pixi.toml index 149ea1a3d..f395d6b3a 100644 --- a/examples/flask-hello-world/pixi.toml +++ b/examples/flask-hello-world/pixi.toml @@ -6,7 +6,7 @@ authors = ["Wolf Vollprecht "] channels = ["conda-forge"] platforms = ["linux-64", "win-64", "osx-64", "osx-arm64"] -[commands] +[tasks] start = "python -m flask run --port=5050" [dependencies] diff --git a/examples/opencv/pixi.toml b/examples/opencv/pixi.toml index bead7f6b8..21298dbe4 100644 --- a/examples/opencv/pixi.toml +++ b/examples/opencv/pixi.toml @@ -6,7 +6,7 @@ authors = ["Ruben Arts "] channels = ["conda-forge"] platforms = ["linux-64", "win-64", "osx-64", "osx-arm64"] -[commands] +[tasks] start = "python webcam_capture.py" calibrate = "python calibrate.py" diff --git a/examples/turtlesim/pixi.toml b/examples/turtlesim/pixi.toml index bad79b489..11a83c728 100644 --- a/examples/turtlesim/pixi.toml +++ b/examples/turtlesim/pixi.toml @@ -6,7 +6,7 @@ authors = ["Ruben Arts "] channels = ["conda-forge", "robostack-staging"] platforms = ["linux-64", "win-64", "osx-64", "osx-arm64"] -[commands] +[tasks] start = "ros2 run turtlesim turtlesim_node" teleop = "ros2 run turtlesim turtle_teleop_key" diff --git a/src/cli/task.rs b/src/cli/task.rs index 6602e7c2f..352c7b583 100644 --- a/src/cli/task.rs +++ b/src/cli/task.rs @@ -7,20 +7,23 @@ use std::path::PathBuf; #[derive(Parser, Debug)] pub enum Operation { /// Add a command to the project + #[clap(alias = "a")] Add(AddArgs), /// Remove a command from the project + #[clap(alias = "r")] Remove(RemoveArgs), /// Alias another specific command + #[clap(alias = "@")] Alias(AliasArgs), } #[derive(Parser, Debug)] #[clap(arg_required_else_help = true)] pub struct RemoveArgs { - /// Task name - pub name: String, + /// Task names to remove + pub names: Vec, } #[derive(Parser, Debug)] @@ -109,26 +112,42 @@ pub fn execute(args: Args) -> anyhow::Result<()> { ); } Operation::Remove(args) => { - let name = args.name; - project.remove_task(&name)?; - let depends_on = project.task_depends_on(&name); - if !depends_on.is_empty() { - eprintln!( - "{}: {}", - console::style("Warning, the following task/s depend on this task").yellow(), - console::style(depends_on.iter().to_owned().join(", ")).bold() - ); + let mut to_remove = Vec::new(); + for name in args.names.iter() { + if project.task_opt(name).is_none() { + eprintln!( + "{}Task {} does not exist", + console::style(console::Emoji("❌ ", "X")).red(), + console::style(&name).bold(), + ); + continue; + } + // Check if task has dependencies + let depends_on = project.task_depends_on(name); + if !depends_on.is_empty() && !args.names.contains(name) { + eprintln!( + "{}: {}", + console::style("Warning, the following task/s depend on this task") + .yellow(), + console::style(depends_on.iter().to_owned().join(", ")).bold() + ); + eprintln!( + "{}", + console::style("Be sure to modify these after the removal\n").yellow() + ); + } + // Safe to remove + to_remove.push(name); + } + + for name in to_remove { + project.remove_task(name)?; eprintln!( - "{}", - console::style("Be sure to modify these after the removal\n").yellow() + "{}Removed task {} ", + console::style(console::Emoji("❌ ", "X")).yellow(), + console::style(&name).bold(), ); } - - eprintln!( - "{}Removed task {} ", - console::style(console::Emoji("❌ ", "X")).yellow(), - console::style(&name).bold(), - ); } Operation::Alias(args) => { let name = args.alias.clone(); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 2c8a92821..be61e145c 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -195,7 +195,7 @@ impl TasksControl<'_> { task::execute(task::Args { manifest_path: Some(self.pixi.manifest_path()), operation: task::Operation::Remove(task::RemoveArgs { - name: name.to_string(), + names: vec![name.to_string()], }), }) } From 451ead666b354e4d655e64d3886e264f74c51fe7 Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Fri, 7 Jul 2023 10:16:10 +0200 Subject: [PATCH 8/8] feat: task add remove docs --- docs/cli.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/cli.md b/docs/cli.md index 110a18270..9bbc30e23 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -60,6 +60,30 @@ pixi run --manifest-path ~/myproject python pixi run build ``` +### Create a task from a command +If you want to make a shorthand for a specific command you can add a task for it +```bash +pixi task add cow cowpy "Hello User" +``` + +This adds the following to the `pixi.toml`: + +```toml +[tasks] +cow = "cowpy \"Hello User\"" +``` +Which you can then run with the `run` command: + +```bash +pixi run cow +``` + +To remove a task you can use the `task remove` command: + +```bash +pixi task remove cow +``` + ### Start a shell in the environment This command starts a new shell in the project's environment. To exit the pixi shell, simply run exit