Skip to content

Commit

Permalink
feat: Add the current working directory in tasks (#380)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ruben-arts authored Oct 6, 2023
1 parent d91069f commit 766b61c
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 14 deletions.
13 changes: 13 additions & 0 deletions docs/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,28 @@ 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`:

```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:
Expand Down
35 changes: 23 additions & 12 deletions src/cli/run.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -149,25 +151,33 @@ pub async fn create_script(task: Task, args: Vec<String>) -> miette::Result<Sequ
deno_task_shell::parser::parse(full_script.trim()).map_err(|e| miette!("{e}"))
}

/// Select a working directory based on a given path or the project.
pub fn select_cwd(path: Option<&Path>, project: &Project) -> miette::Result<PathBuf> {
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<String, String>,
cwd: &Path,
) -> miette::Result<i32> {
// 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<String, String>,
input: Option<&[u8]>,
) -> RunOutput {
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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") }
};
Expand Down
7 changes: 6 additions & 1 deletion src/cli/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ pub struct AddArgs {
/// The platform for which the task should be added
#[arg(long, short)]
pub platform: Option<Platform>,

/// The working directory relative to the root of the project
#[arg(long)]
pub cwd: Option<PathBuf>,
}

#[derive(Parser, Debug, Clone)]
Expand Down Expand Up @@ -88,12 +92,13 @@ impl From<AddArgs> 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,
})
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
13 changes: 13 additions & 0 deletions src/task/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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
Expand All @@ -86,6 +96,9 @@ pub struct Execute {
#[serde(default)]
#[serde_as(deserialize_as = "OneOrMany<_, PreferMany>")]
pub depends_on: Vec<String>,

/// The working directory for the command relative to the root of the project.
pub cwd: Option<PathBuf>,
}

impl From<Execute> for Task {
Expand Down
6 changes: 6 additions & 0 deletions tests/common/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 4 additions & 1 deletion tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -238,6 +240,7 @@ impl TasksControl<'_> {
commands: vec![],
depends_on: None,
platform,
cwd: None,
},
}
}
Expand Down
49 changes: 49 additions & 0 deletions tests/task_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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());
}

0 comments on commit 766b61c

Please sign in to comment.