Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions crates/pixi/tests/integration_rust/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ use pixi_core::{
use pixi_manifest::{EnvironmentName, FeatureName};
use pixi_progress::global_multi_progress;
use pixi_task::{
ExecutableTask, RunOutput, SearchEnvironments, TaskExecutionError, TaskGraph, TaskGraphError,
TaskName, get_task_env,
ExecutableTask, PreferExecutable, RunOutput, SearchEnvironments, TaskExecutionError, TaskGraph,
TaskGraphError, TaskName, get_task_env,
};
use rattler_conda_types::{MatchSpec, ParseStrictness::Lenient, Platform};
use rattler_lock::{LockFile, LockedPackageRef, UrlOrPath};
Expand Down Expand Up @@ -591,8 +591,18 @@ impl PixiControl {
.map(|e| e.best_platform())
.or(Some(Platform::current())),
);
let task_graph = TaskGraph::from_cmd_args(&project, &search_env, args.task, false)
.map_err(RunError::TaskGraphError)?;
let task_graph = TaskGraph::from_cmd_args(
&project,
&search_env,
args.task,
false,
if args.executable {
PreferExecutable::Always
} else {
PreferExecutable::TaskFirst
},
)
.map_err(RunError::TaskGraphError)?;

// Iterate over all tasks in the graph and execute them.
let mut task_env = None;
Expand Down
22 changes: 18 additions & 4 deletions crates/pixi_cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use pixi_manifest::{FeaturesExt, TaskName};
use pixi_progress::global_multi_progress;
use pixi_task::{
AmbiguousTask, CanSkip, ExecutableTask, FailedToParseShellScript, InvalidWorkingDirectory,
SearchEnvironments, TaskAndEnvironment, TaskGraph, get_task_env,
PreferExecutable, SearchEnvironments, TaskAndEnvironment, TaskGraph, get_task_env,
};
use rattler_conda_types::Platform;
use thiserror::Error;
Expand All @@ -50,6 +50,12 @@ pub struct Args {
/// environment, which can be an executable in the environment's PATH.
pub task: Vec<String>,

/// Execute the command as an executable without resolving Pixi tasks.
///
/// Useful when a task name and an executable have the same name.
#[arg(long = "executable", short = 'x')]
pub executable: bool,

#[clap(flatten)]
pub workspace_config: WorkspaceConfig,

Expand Down Expand Up @@ -178,9 +184,17 @@ pub async fn execute(args: Args) -> miette::Result<()> {
SearchEnvironments::from_opt_env(&workspace, explicit_environment.clone(), search_platform)
.with_disambiguate_fn(disambiguate_task_interactive);

let task_graph =
TaskGraph::from_cmd_args(&workspace, &search_environment, args.task, args.skip_deps)?;

let task_graph = TaskGraph::from_cmd_args(
&workspace,
&search_environment,
args.task,
args.skip_deps,
if args.executable {
PreferExecutable::Always
} else {
PreferExecutable::TaskFirst
},
)?;
tracing::debug!("Task graph: {}", task_graph);

// Print dry-run message if dry-run mode is enabled
Expand Down
2 changes: 1 addition & 1 deletion crates/pixi_task/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ pub use task_environment::{
AmbiguousTask, FindTaskError, FindTaskSource, SearchEnvironments, TaskAndEnvironment,
TaskDisambiguation,
};
pub use task_graph::{TaskGraph, TaskGraphError, TaskId, TaskNode};
pub use task_graph::{PreferExecutable, TaskGraph, TaskGraphError, TaskId, TaskNode};
102 changes: 90 additions & 12 deletions crates/pixi_task/src/task_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ fn join_args_with_single_quotes<'a>(args: impl IntoIterator<Item = &'a str>) ->
.join(" ")
}

/// Controls whether to prefer resolving commands as executables over Pixi tasks
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PreferExecutable {
/// Try to resolve as a Pixi task first, fall back to executable if no task found
#[default]
TaskFirst,
/// Always treat as an executable, skip task resolution
Always,
}

/// A task ID is a unique identifier for a [`TaskNode`] in a [`TaskGraph`].
///
/// To get a task from a [`TaskGraph`], you can use the [`TaskId`] as an index.
Expand Down Expand Up @@ -189,11 +199,18 @@ impl<'p> TaskGraph<'p> {
}

/// Constructs a new [`TaskGraph`] from a list of command line arguments.
///
/// When `prefer_executable` is [`PreferExecutable::Always`], the first
/// argument will not be resolved as a Pixi task even if a task with that
/// name exists. Instead, the arguments are treated as a custom command to
/// execute within the environment, allowing running executables that
/// collide with task names.
pub fn from_cmd_args<D: TaskDisambiguation<'p>>(
project: &'p Workspace,
search_envs: &SearchEnvironments<'p, D>,
args: Vec<String>,
skip_deps: bool,
prefer_executable: PreferExecutable,
) -> Result<Self, TaskGraphError> {
// Split 'args' into arguments if it's a single string, supporting commands
// like: `"test 1 == 0 || echo failed"` or `"echo foo && echo bar"` or
Expand All @@ -208,7 +225,9 @@ impl<'p> TaskGraph<'p> {
(args, true)
};

if let Some(name) = args.first() {
if prefer_executable == PreferExecutable::TaskFirst
&& let Some(name) = args.first()
{
match search_envs.find_task(TaskName::from(name.clone()), FindTaskSource::CmdArgs, None)
{
Err(FindTaskError::MissingTask(_)) => {}
Expand Down Expand Up @@ -613,7 +632,7 @@ mod test {

use crate::{
task_environment::SearchEnvironments,
task_graph::{TaskGraph, join_args_with_single_quotes},
task_graph::{PreferExecutable, TaskGraph, join_args_with_single_quotes},
};

fn commands_in_order(
Expand All @@ -622,6 +641,7 @@ mod test {
platform: Option<Platform>,
environment_name: Option<EnvironmentName>,
skip_deps: bool,
prefer_executable: PreferExecutable,
) -> Vec<String> {
let project = Workspace::from_str(Path::new("pixi.toml"), project_str).unwrap();

Expand All @@ -633,6 +653,7 @@ mod test {
&search_envs,
run_args.iter().map(|arg| arg.to_string()).collect(),
skip_deps,
prefer_executable,
)
.unwrap();

Expand Down Expand Up @@ -670,7 +691,8 @@ mod test {
&["top", "--test"],
None,
None,
false
false,
PreferExecutable::TaskFirst,
),
vec!["echo root", "echo task1", "echo task2", "echo top '--test'"]
);
Expand All @@ -694,7 +716,8 @@ mod test {
&["top"],
None,
None,
false
false,
PreferExecutable::TaskFirst,
),
vec!["echo root", "echo task1", "echo task2", "echo top"]
);
Expand All @@ -720,7 +743,8 @@ mod test {
&["top"],
Some(Platform::Linux64),
None,
false
false,
PreferExecutable::TaskFirst,
),
vec!["echo linux", "echo task1", "echo task2", "echo top",]
);
Expand All @@ -739,7 +763,8 @@ mod test {
&["echo bla"],
None,
None,
false
false,
PreferExecutable::TaskFirst,
),
vec![r#"echo bla"#]
);
Expand All @@ -764,7 +789,8 @@ mod test {
&["build"],
None,
None,
false
false,
PreferExecutable::TaskFirst,
),
vec![r#"echo build"#]
);
Expand Down Expand Up @@ -792,7 +818,8 @@ mod test {
&["start"],
None,
None,
false
false,
PreferExecutable::TaskFirst,
),
vec![r#"hello world"#]
);
Expand Down Expand Up @@ -824,7 +851,8 @@ mod test {
&["start"],
None,
Some(EnvironmentName::Named("cuda".to_string())),
false
false,
PreferExecutable::TaskFirst,
),
vec![r#"python train.py --cuda"#, r#"python test.py --cuda"#]
);
Expand Down Expand Up @@ -854,7 +882,8 @@ mod test {
&["foobar"],
None,
None,
false
false,
PreferExecutable::TaskFirst,
),
vec![r#"echo foo"#, r#"echo bar"#]
);
Expand Down Expand Up @@ -886,6 +915,7 @@ mod test {
None,
None,
false,
PreferExecutable::TaskFirst,
);
}

Expand All @@ -902,11 +932,25 @@ mod test {
bar = { cmd = "echo bar", depends-on = ["foo"] }
"#;
assert_eq!(
commands_in_order(project, &["bar"], None, None, true),
commands_in_order(
project,
&["bar"],
None,
None,
true,
PreferExecutable::TaskFirst
),
vec![r#"echo bar"#]
);
assert_eq!(
commands_in_order(project, &["bar"], None, None, false),
commands_in_order(
project,
&["bar"],
None,
None,
false,
PreferExecutable::TaskFirst
),
vec!["echo foo", "echo bar"]
);
}
Expand All @@ -931,4 +975,38 @@ mod test {
let quote_result = join_args_with_single_quotes(quote_args.iter().copied());
assert_eq!(quote_result, r#"'echo' 'it'"'"'s'"#);
}

#[test]
fn test_prefer_executable_always() {
let project = r#"
[project]
name = "pixi"
channels = []
platforms = ["linux-64"]

[tasks]
foo = "echo from-task"
"#;

let project =
Workspace::from_str(Path::new("pixi.toml"), project).expect("valid workspace");

let search_envs = SearchEnvironments::from_opt_env(&project, None, None);

let graph = TaskGraph::from_cmd_args(
&project,
&search_envs,
vec!["foo".to_string()],
false,
PreferExecutable::Always,
)
.expect("graph should be created");

// When PreferExecutable::Always is used, we should not resolve the command
// as a task, even if a task with that name exists.
let order = graph.topological_order();
assert_eq!(order.len(), 1);
let task = &graph[order[0]];
assert!(task.name.is_none());
}
}
2 changes: 2 additions & 0 deletions docs/reference/cli/pixi/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pixi run [OPTIONS] [TASK]...
<br>May be provided more than once.

## Options
- <a id="arg---executable" href="#arg---executable">`--executable (-x)`</a>
: Execute the command as an executable without resolving Pixi tasks
- <a id="arg---environment" href="#arg---environment">`--environment (-e) <ENVIRONMENT>`</a>
: The environment to run the task in
- <a id="arg---clean-env" href="#arg---clean-env">`--clean-env`</a>
Expand Down
Loading