From 766b61c6813cae0f98eb7d275e7221a6a99d59ca Mon Sep 17 00:00:00 2001 From: Ruben Arts Date: Fri, 6 Oct 2023 16:51:43 +0200 Subject: [PATCH] feat: Add the current working directory in tasks (#380) This feature also fixes the fact that `pixi run` always runs in the root of the project. Now only the pixi tasks are run in the root of the project (unless specified differently), while the `pixi run` will run in the `env::current_dir()` . closes #376 --- docs/cli.mdx | 13 +++++++++++ src/cli/run.rs | 35 ++++++++++++++++++---------- src/cli/task.rs | 7 +++++- src/project/mod.rs | 3 +++ src/task/mod.rs | 13 +++++++++++ tests/common/builders.rs | 6 +++++ tests/common/mod.rs | 5 +++- tests/task_tests.rs | 49 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 117 insertions(+), 14 deletions(-) diff --git a/docs/cli.mdx b/docs/cli.mdx index c269a7462..13f807d23 100644 --- a/docs/cli.mdx +++ b/docs/cli.mdx @@ -111,8 +111,16 @@ If you want to make a shorthand for a specific command you can add a task for it Add a task to the `pixi.toml`, use `--depends-on` to add tasks you want to run before this task, e.g. build before an execute task. +#### Options +- `--platform`: the platform for which this task should be added. +- `--depends-on`: the task it depends on to be run before the one your adding. +- `--cwd`: the working directory for the task relative to the root of the project. + ```shell pixi task add cow cowpy "Hello User" +pixi task add tls ls --cwd tests +pixi task add test cargo t --depends-on build +pixi task add build-osx "METAL=1 cargo build" --platform osx-64 ``` This adds the following to the `pixi.toml`: @@ -120,6 +128,11 @@ This adds the following to the `pixi.toml`: ```toml [tasks] cow = "cowpy \"Hello User\"" +tls = { cmd = "ls", cwd = "tests" } +test = { cmd = "cargo t", depends_on = ["build"] } + +[target.osx-64.tasks] +build-osx = "METAL=1 cargo build" ``` Which you can then run with the `run` command: diff --git a/src/cli/run.rs b/src/cli/run.rs index cf2830288..3dae0712c 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -1,5 +1,6 @@ use std::collections::{HashMap, HashSet, VecDeque}; -use std::path::PathBuf; +use std::env; +use std::path::{Path, PathBuf}; use std::string::String; use clap::Parser; @@ -89,6 +90,7 @@ pub fn order_tasks( Execute { cmd: CmdArgs::from(tasks), depends_on: vec![], + cwd: Some(env::current_dir().unwrap_or(project.root().to_path_buf())), } .into(), Vec::new(), @@ -149,25 +151,33 @@ pub async fn create_script(task: Task, args: Vec) -> miette::Result, project: &Project) -> miette::Result { + Ok(match path { + Some(cwd) if cwd.is_absolute() => cwd.to_path_buf(), + Some(cwd) => { + let abs_path = project.root().join(cwd); + if !abs_path.exists() { + miette::bail!("Can't find the 'cwd': '{}'", abs_path.display()); + } + abs_path + } + None => project.root().to_path_buf(), + }) +} /// Executes the given command within the specified project and with the given environment. pub async fn execute_script( script: SequentialList, - project: &Project, command_env: &HashMap, + cwd: &Path, ) -> miette::Result { // Execute the shell command - Ok(deno_task_shell::execute( - script, - command_env.clone(), - project.root(), - Default::default(), - ) - .await) + Ok(deno_task_shell::execute(script, command_env.clone(), cwd, Default::default()).await) } pub async fn execute_script_with_output( script: SequentialList, - project: &Project, + cwd: &Path, command_env: &HashMap, input: Option<&[u8]>, ) -> RunOutput { @@ -178,7 +188,7 @@ pub async fn execute_script_with_output( drop(stdin_writer); // prevent a deadlock by dropping the writer let (stdout, stdout_handle) = get_output_writer_and_handle(); let (stderr, stderr_handle) = get_output_writer_and_handle(); - let state = ShellState::new(command_env.clone(), project.root(), Default::default()); + let state = ShellState::new(command_env.clone(), cwd, Default::default()); let code = execute_with_pipes(script, state, stdin, stdout, stderr).await; RunOutput { exit_code: code, @@ -200,6 +210,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { // Execute the commands in the correct order while let Some((command, args)) = ordered_commands.pop_back() { + let cwd = select_cwd(command.working_directory(), &project)?; // Ignore CTRL+C // Specifically so that the child is responsible for its own signal handling // NOTE: one CTRL+C is registered it will always stay registered for the rest of the runtime of the program @@ -208,7 +219,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { let ctrl_c = tokio::spawn(async { while tokio::signal::ctrl_c().await.is_ok() {} }); let script = create_script(command, args).await?; let status_code = tokio::select! { - code = execute_script(script, &project, &command_env) => code?, + code = execute_script(script, &command_env, &cwd) => code?, // This should never exit _ = ctrl_c => { unreachable!("Ctrl+C should not be triggered") } }; diff --git a/src/cli/task.rs b/src/cli/task.rs index 46817c9c6..bf277c87d 100644 --- a/src/cli/task.rs +++ b/src/cli/task.rs @@ -53,6 +53,10 @@ pub struct AddArgs { /// The platform for which the task should be added #[arg(long, short)] pub platform: Option, + + /// The working directory relative to the root of the project + #[arg(long)] + pub cwd: Option, } #[derive(Parser, Debug, Clone)] @@ -88,12 +92,13 @@ impl From for Task { // Depending on whether the task should have depends_on or not we create a Plain or complex // command. - if depends_on.is_empty() { + if depends_on.is_empty() && value.cwd.is_none() { Self::Plain(cmd_args) } else { Self::Execute(Execute { cmd: CmdArgs::Single(cmd_args), depends_on, + cwd: value.cwd, }) } } diff --git a/src/project/mod.rs b/src/project/mod.rs index c46b377c9..57c18d19a 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -72,6 +72,9 @@ fn task_as_toml(task: Task) -> Item { Value::Array(Array::from_iter(process.depends_on.into_iter())), ); } + if let Some(cwd) = process.cwd { + table.insert("cwd", cwd.to_string_lossy().to_string().into()); + } Item::Value(Value::InlineTable(table)) } Task::Alias(alias) => { diff --git a/src/task/mod.rs b/src/task/mod.rs index 275c3b9f9..bd9e70195 100644 --- a/src/task/mod.rs +++ b/src/task/mod.rs @@ -3,6 +3,7 @@ use serde::Deserialize; use serde_with::{formats::PreferMany, serde_as, OneOrMany}; use std::borrow::Cow; use std::fmt::{Display, Formatter}; +use std::path::{Path, PathBuf}; /// Represents different types of scripts #[derive(Debug, Clone, Deserialize)] @@ -72,6 +73,15 @@ impl Task { Task::Alias(_) => None, } } + + /// Returns the working directory for the task to run in. + pub fn working_directory(&self) -> Option<&Path> { + match self { + Task::Plain(_) => None, + Task::Execute(t) => t.cwd.as_deref(), + Task::Alias(_) => None, + } + } } /// A command script executes a single command from the environment @@ -86,6 +96,9 @@ pub struct Execute { #[serde(default)] #[serde_as(deserialize_as = "OneOrMany<_, PreferMany>")] pub depends_on: Vec, + + /// The working directory for the command relative to the root of the project. + pub cwd: Option, } impl From for Task { diff --git a/tests/common/builders.rs b/tests/common/builders.rs index 6e4ed5010..7369605c7 100644 --- a/tests/common/builders.rs +++ b/tests/common/builders.rs @@ -149,6 +149,12 @@ impl TaskAddBuilder { self } + /// With this working directory + pub fn with_cwd(mut self, cwd: PathBuf) -> Self { + self.args.cwd = Some(cwd); + self + } + /// Execute the CLI command pub fn execute(self) -> miette::Result<()> { task::execute(task::Args { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 14a88ab9e..4bce24d02 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -188,9 +188,11 @@ impl PixiControl { let mut result = RunOutput::default(); while let Some((command, args)) = tasks.pop_back() { + let cwd = run::select_cwd(command.working_directory(), &project)?; let script = create_script(command, args).await; if let Ok(script) = script { - let output = execute_script_with_output(script, &project, &task_env, None).await; + let output = + execute_script_with_output(script, cwd.as_path(), &task_env, None).await; result.stdout.push_str(&output.stdout); result.stderr.push_str(&output.stderr); result.exit_code = output.exit_code; @@ -238,6 +240,7 @@ impl TasksControl<'_> { commands: vec![], depends_on: None, platform, + cwd: None, }, } } diff --git a/tests/task_tests.rs b/tests/task_tests.rs index c3f4ad193..7b3625553 100644 --- a/tests/task_tests.rs +++ b/tests/task_tests.rs @@ -2,6 +2,8 @@ use crate::common::PixiControl; use pixi::cli::run::Args; use pixi::task::{CmdArgs, Task}; use rattler_conda_types::Platform; +use std::fs; +use std::path::PathBuf; mod common; @@ -142,3 +144,50 @@ pub async fn add_remove_target_specific_task() { 0 ); } + +#[tokio::test] +async fn test_cwd() { + let pixi = PixiControl::new().unwrap(); + pixi.init().without_channels().await.unwrap(); + + // Create test dir + fs::create_dir(pixi.project_path().join("test")).unwrap(); + + pixi.tasks() + .add("pwd-test", None) + .with_commands(["pwd"]) + .with_cwd(PathBuf::from("test")) + .execute() + .unwrap(); + + let result = pixi + .run(Args { + task: vec!["pwd-test".to_string()], + manifest_path: None, + locked: false, + frozen: false, + }) + .await + .unwrap(); + + assert_eq!(result.exit_code, 0); + assert!(result.stdout.contains("test")); + + // Test that an unknown cwd gives an error + pixi.tasks() + .add("unknown-cwd", None) + .with_commands(["pwd"]) + .with_cwd(PathBuf::from("tests")) + .execute() + .unwrap(); + + assert!(pixi + .run(Args { + task: vec!["unknown-cwd".to_string()], + manifest_path: None, + locked: false, + frozen: false, + }) + .await + .is_err()); +}