Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add the current working directory in tasks #380

Merged
merged 2 commits into from
Oct 6, 2023
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
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());
}
Loading