Skip to content

Commit 3f447f4

Browse files
authored
Feature/add feature env to task cli (#694)
Adds: ``` pixi task remove -f/--feature test pixi task list -e/--environment test ```
1 parent 5d50682 commit 3f447f4

File tree

11 files changed

+142
-48
lines changed

11 files changed

+142
-48
lines changed

src/cli/run.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use miette::{miette, Context, Diagnostic, IntoDiagnostic};
66
use rattler_conda_types::Platform;
77

88
use crate::environment::LockFileUsage;
9+
use crate::project::errors::UnsupportedPlatformError;
910
use crate::task::{
1011
ExecutableTask, FailedToParseShellScript, InvalidWorkingDirectory, TraversalError,
1112
};
@@ -90,6 +91,9 @@ enum TaskExecutionError {
9091

9192
#[error(transparent)]
9293
TraverseError(#[from] TraversalError),
94+
95+
#[error(transparent)]
96+
UnsupportedPlatformError(#[from] UnsupportedPlatformError),
9397
}
9498

9599
/// Called to execute a single command.
@@ -130,8 +134,8 @@ async fn execute_task<'p>(
130134
if status_code == 127 {
131135
let available_tasks = task
132136
.project()
133-
.manifest
134-
.tasks(Some(Platform::current()))
137+
.default_environment()
138+
.tasks(Some(Platform::current()))?
135139
.into_keys()
136140
.sorted()
137141
.collect_vec();

src/cli/task.rs

+31-9
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
use crate::project::manifest::FeatureName;
1+
use crate::project::manifest::{EnvironmentName, FeatureName};
22
use crate::task::{quote, Alias, CmdArgs, Execute, Task};
33
use crate::Project;
44
use clap::Parser;
55
use itertools::Itertools;
6+
use miette::miette;
67
use rattler_conda_types::Platform;
78
use std::path::PathBuf;
9+
use std::str::FromStr;
810
use toml_edit::{Array, Item, Table, Value};
911

1012
#[derive(Parser, Debug)]
@@ -35,6 +37,10 @@ pub struct RemoveArgs {
3537
/// The platform for which the task should be removed
3638
#[arg(long, short)]
3739
pub platform: Option<Platform>,
40+
41+
/// The feature for which the task should be removed
42+
#[arg(long, short)]
43+
pub feature: Option<String>,
3844
}
3945

4046
#[derive(Parser, Debug, Clone)]
@@ -84,6 +90,11 @@ pub struct AliasArgs {
8490
pub struct ListArgs {
8591
#[arg(long, short)]
8692
pub summary: bool,
93+
94+
/// The environment the list should be generated for
95+
/// If not specified, the default environment is used.
96+
#[arg(long, short)]
97+
pub environment: Option<String>,
8798
}
8899

89100
impl From<AddArgs> for Task {
@@ -153,19 +164,22 @@ pub fn execute(args: Args) -> miette::Result<()> {
153164
.add_task(name, task.clone(), args.platform, &feature)?;
154165
project.save()?;
155166
eprintln!(
156-
"{}Added task {}: {}",
167+
"{}Added task `{}`: {}",
157168
console::style(console::Emoji("✔ ", "+")).green(),
158169
console::style(&name).bold(),
159170
task,
160171
);
161172
}
162173
Operation::Remove(args) => {
163174
let mut to_remove = Vec::new();
175+
let feature = args
176+
.feature
177+
.map_or(FeatureName::Default, FeatureName::Named);
164178
for name in args.names.iter() {
165179
if let Some(platform) = args.platform {
166180
if !project
167181
.manifest
168-
.tasks(Some(platform))
182+
.tasks(Some(platform), &feature)?
169183
.contains_key(name.as_str())
170184
{
171185
eprintln!(
@@ -176,11 +190,16 @@ pub fn execute(args: Args) -> miette::Result<()> {
176190
);
177191
continue;
178192
}
179-
} else if !project.manifest.tasks(None).contains_key(name.as_str()) {
193+
} else if !project
194+
.manifest
195+
.tasks(None, &feature)?
196+
.contains_key(name.as_str())
197+
{
180198
eprintln!(
181-
"{}Task {} does not exist",
199+
"{}Task `{}` does not exist for the `{}` feature",
182200
console::style(console::Emoji("❌ ", "X")).red(),
183201
console::style(&name).bold(),
202+
console::style(&feature).bold(),
184203
);
185204
continue;
186205
}
@@ -207,10 +226,10 @@ pub fn execute(args: Args) -> miette::Result<()> {
207226
}
208227

209228
for (name, platform) in to_remove {
210-
project.manifest.remove_task(name, platform)?;
229+
project.manifest.remove_task(name, platform, &feature)?;
211230
project.save()?;
212231
eprintln!(
213-
"{}Removed task {} ",
232+
"{}Removed task `{}` ",
214233
console::style(console::Emoji("✔ ", "+")).green(),
215234
console::style(&name).bold(),
216235
);
@@ -224,15 +243,18 @@ pub fn execute(args: Args) -> miette::Result<()> {
224243
.add_task(name, task.clone(), args.platform, &FeatureName::Default)?;
225244
project.save()?;
226245
eprintln!(
227-
"{} Added alias {}: {}",
246+
"{} Added alias `{}`: {}",
228247
console::style("@").blue(),
229248
console::style(&name).bold(),
230249
task,
231250
);
232251
}
233252
Operation::List(args) => {
253+
let env = EnvironmentName::from_str(args.environment.as_deref().unwrap_or("default"))?;
234254
let tasks = project
235-
.tasks(Some(Platform::current()))
255+
.environment(&env)
256+
.ok_or(miette!("Environment `{}` not found in project", env))?
257+
.tasks(Some(Platform::current()))?
236258
.into_keys()
237259
.collect_vec();
238260
if tasks.is_empty() {

src/consts.rs

+2
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ pub const ENVIRONMENTS_DIR: &str = "envs";
66
pub const PYPI_DEPENDENCIES: &str = "pypi-dependencies";
77

88
pub const DEFAULT_ENVIRONMENT_NAME: &str = "default";
9+
10+
pub const DEFAULT_FEATURE_NAME: &str = DEFAULT_ENVIRONMENT_NAME;

src/project/environment.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ impl<'p> Environment<'p> {
253253
if let Some(platform) = platform {
254254
if !self.platforms().contains(&platform) {
255255
return Err(UnsupportedPlatformError {
256-
project: self.project,
256+
environments_platforms: self.platforms().into_iter().collect(),
257257
environment: self.name().clone(),
258258
platform,
259259
});

src/project/errors.rs

+7-8
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ use thiserror::Error;
1111
/// TODO: Make this error better by also explaining to the user why a certain platform was not
1212
/// supported and with suggestions as how to fix it.
1313
#[derive(Debug, Clone)]
14-
pub struct UnsupportedPlatformError<'p> {
15-
/// The project that the platform is not supported for.
16-
pub project: &'p Project,
14+
pub struct UnsupportedPlatformError {
15+
/// Platforms supported by the environment
16+
pub environments_platforms: Vec<Platform>,
1717

1818
/// The environment that the platform is not supported for.
1919
pub environment: EnvironmentName,
@@ -22,9 +22,9 @@ pub struct UnsupportedPlatformError<'p> {
2222
pub platform: Platform,
2323
}
2424

25-
impl<'p> Error for UnsupportedPlatformError<'p> {}
25+
impl Error for UnsupportedPlatformError {}
2626

27-
impl<'p> Display for UnsupportedPlatformError<'p> {
27+
impl Display for UnsupportedPlatformError {
2828
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2929
match &self.environment {
3030
EnvironmentName::Default => {
@@ -39,16 +39,15 @@ impl<'p> Display for UnsupportedPlatformError<'p> {
3939
}
4040
}
4141

42-
impl<'p> Diagnostic for UnsupportedPlatformError<'p> {
42+
impl Diagnostic for UnsupportedPlatformError {
4343
fn code(&self) -> Option<Box<dyn Display + '_>> {
4444
Some(Box::new("unsupported-platform".to_string()))
4545
}
4646

4747
fn help(&self) -> Option<Box<dyn Display + '_>> {
48-
let env = self.project.environment(&self.environment)?;
4948
Some(Box::new(format!(
5049
"supported platforms are {}",
51-
env.platforms().into_iter().format(", ")
50+
self.environments_platforms.iter().format(", ")
5251
)))
5352
}
5453

src/project/manifest/environment.rs

+11
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use miette::Diagnostic;
55
use regex::Regex;
66
use serde::{self, Deserialize, Deserializer};
77
use std::borrow::Borrow;
8+
use std::fmt;
89
use std::hash::{Hash, Hasher};
910
use std::str::FromStr;
1011
use thiserror::Error;
@@ -38,6 +39,16 @@ impl Borrow<str> for EnvironmentName {
3839
self.as_str()
3940
}
4041
}
42+
43+
impl fmt::Display for EnvironmentName {
44+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45+
match self {
46+
EnvironmentName::Default => write!(f, "{}", consts::DEFAULT_ENVIRONMENT_NAME),
47+
EnvironmentName::Named(name) => write!(f, "{}", name),
48+
}
49+
}
50+
}
51+
4152
#[derive(Debug, Clone, Error, Diagnostic, PartialEq)]
4253
#[error("Failed to parse environment name '{attempted_parse}', please use only lowercase letters, numbers and dashes")]
4354
pub struct ParseEnvironmentNameError {

src/project/manifest/feature.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ impl<'de> Deserialize<'de> for FeatureName {
2828
D: serde::Deserializer<'de>,
2929
{
3030
match String::deserialize(deserializer)?.as_str() {
31-
consts::DEFAULT_ENVIRONMENT_NAME => Err(D::Error::custom(
31+
consts::DEFAULT_FEATURE_NAME => Err(D::Error::custom(
3232
"The name 'default' is reserved for the default feature",
3333
)),
3434
name => Ok(FeatureName::Named(name.to_string())),
@@ -39,7 +39,7 @@ impl<'de> Deserialize<'de> for FeatureName {
3939
impl<'s> From<&'s str> for FeatureName {
4040
fn from(value: &'s str) -> Self {
4141
match value {
42-
consts::DEFAULT_ENVIRONMENT_NAME => FeatureName::Default,
42+
consts::DEFAULT_FEATURE_NAME => FeatureName::Default,
4343
name => FeatureName::Named(name.to_string()),
4444
}
4545
}
@@ -54,7 +54,7 @@ impl FeatureName {
5454
}
5555

5656
pub fn as_str(&self) -> &str {
57-
self.name().unwrap_or(consts::DEFAULT_ENVIRONMENT_NAME)
57+
self.name().unwrap_or(consts::DEFAULT_FEATURE_NAME)
5858
}
5959
}
6060

@@ -67,23 +67,23 @@ impl Borrow<str> for FeatureName {
6767
impl From<FeatureName> for String {
6868
fn from(name: FeatureName) -> Self {
6969
match name {
70-
FeatureName::Default => consts::DEFAULT_ENVIRONMENT_NAME.to_string(),
70+
FeatureName::Default => consts::DEFAULT_FEATURE_NAME.to_string(),
7171
FeatureName::Named(name) => name,
7272
}
7373
}
7474
}
7575
impl<'a> From<&'a FeatureName> for String {
7676
fn from(name: &'a FeatureName) -> Self {
7777
match name {
78-
FeatureName::Default => consts::DEFAULT_ENVIRONMENT_NAME.to_string(),
78+
FeatureName::Default => consts::DEFAULT_FEATURE_NAME.to_string(),
7979
FeatureName::Named(name) => name.clone(),
8080
}
8181
}
8282
}
8383
impl fmt::Display for FeatureName {
8484
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8585
match self {
86-
FeatureName::Default => write!(f, "{}", consts::DEFAULT_ENVIRONMENT_NAME),
86+
FeatureName::Default => write!(f, "{}", consts::DEFAULT_FEATURE_NAME),
8787
FeatureName::Named(name) => write!(f, "{}", name),
8888
}
8989
}

src/project/manifest/mod.rs

+37-10
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub use feature::{Feature, FeatureName};
1919
use indexmap::{Equivalent, IndexMap};
2020
use itertools::Itertools;
2121
pub use metadata::ProjectMetadata;
22-
use miette::{miette, IntoDiagnostic, LabeledSpan, NamedSource};
22+
use miette::{miette, Diagnostic, IntoDiagnostic, LabeledSpan, NamedSource};
2323
pub use python::PyPiRequirement;
2424
use rattler_conda_types::{
2525
Channel, ChannelConfig, MatchSpec, NamelessMatchSpec, PackageName, Platform, Version,
@@ -33,8 +33,16 @@ use std::{
3333
};
3434
pub use system_requirements::{LibCFamilyAndVersion, LibCSystemRequirement, SystemRequirements};
3535
pub use target::{Target, TargetSelector, Targets};
36+
use thiserror::Error;
3637
use toml_edit::{value, Array, Document, Item, Table, TomlError, Value};
3738

39+
/// Errors that can occur when getting a feature.
40+
#[derive(Debug, Clone, Error, Diagnostic)]
41+
pub enum GetFeatureError {
42+
#[error("feature `{0}` does not exist")]
43+
FeatureDoesNotExist(FeatureName),
44+
}
45+
3846
/// Handles the project's manifest file.
3947
/// This struct is responsible for reading, parsing, editing, and saving the manifest.
4048
/// It encapsulates all logic related to the manifest's TOML format and structure.
@@ -125,16 +133,23 @@ impl Manifest {
125133

126134
/// Returns a hashmap of the tasks that should run only the given platform. If the platform is
127135
/// `None`, only the default targets tasks are returned.
128-
pub fn tasks(&self, platform: Option<Platform>) -> HashMap<&str, &Task> {
129-
self.default_feature()
136+
pub fn tasks(
137+
&self,
138+
platform: Option<Platform>,
139+
feature_name: &FeatureName,
140+
) -> Result<HashMap<&str, &Task>, GetFeatureError> {
141+
Ok(self
142+
.feature(feature_name)
143+
// Return error if feature does not exist
144+
.ok_or(GetFeatureError::FeatureDoesNotExist(feature_name.clone()))?
130145
.targets
131146
.resolve(platform)
132147
.collect_vec()
133148
.into_iter()
134149
.rev()
135150
.flat_map(|target| target.tasks.iter())
136151
.map(|(name, task)| (name.as_str(), task))
137-
.collect()
152+
.collect())
138153
}
139154

140155
/// Add a task to the project
@@ -146,8 +161,10 @@ impl Manifest {
146161
feature_name: &FeatureName,
147162
) -> miette::Result<()> {
148163
// Check if the task already exists
149-
if self.tasks(platform).contains_key(name.as_ref()) {
150-
miette::bail!("task {} already exists", name.as_ref());
164+
if let Ok(tasks) = self.tasks(platform, feature_name) {
165+
if tasks.contains_key(name.as_ref()) {
166+
miette::bail!("task {} already exists", name.as_ref());
167+
}
151168
}
152169

153170
// Get the table that contains the tasks.
@@ -171,20 +188,22 @@ impl Manifest {
171188
&mut self,
172189
name: impl AsRef<str>,
173190
platform: Option<Platform>,
191+
feature_name: &FeatureName,
174192
) -> miette::Result<()> {
175-
self.tasks(platform)
193+
self.tasks(platform, feature_name)?
176194
.get(name.as_ref())
177195
.ok_or_else(|| miette::miette!("task {} does not exist", name.as_ref()))?;
178196

179197
// Get the task table either from the target platform or the default tasks.
180198
let tasks_table =
181-
get_or_insert_toml_table(&mut self.document, platform, &FeatureName::Default, "tasks")?;
199+
get_or_insert_toml_table(&mut self.document, platform, feature_name, "tasks")?;
182200

183201
// If it does not exist in toml, consider this ok as we want to remove it anyways
184202
tasks_table.remove(name.as_ref());
185203

186204
// Remove the task from the internal manifest
187-
self.default_feature_mut()
205+
self.feature_mut(feature_name)
206+
.expect("feature should exist")
188207
.targets
189208
.for_opt_target_mut(platform.map(TargetSelector::from).as_ref())
190209
.map(|target| target.tasks.remove(name.as_ref()));
@@ -499,14 +518,22 @@ impl Manifest {
499518
self.parsed.default_feature_mut()
500519
}
501520

502-
/// Returns the feature with the given name or `None` if it does not exist.
521+
/// Returns the mutable feature with the given name or `None` if it does not exist.
503522
pub fn feature_mut<Q: ?Sized>(&mut self, name: &Q) -> Option<&mut Feature>
504523
where
505524
Q: Hash + Equivalent<FeatureName>,
506525
{
507526
self.parsed.features.get_mut(name)
508527
}
509528

529+
/// Returns the feature with the given name or `None` if it does not exist.
530+
pub fn feature<Q: ?Sized>(&self, name: &Q) -> Option<&Feature>
531+
where
532+
Q: Hash + Equivalent<FeatureName>,
533+
{
534+
self.parsed.features.get(name)
535+
}
536+
510537
/// Returns the default environment
511538
///
512539
/// This is the environment that is added implicitly as the environment with only the default

0 commit comments

Comments
 (0)