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(task): dependencies #26467

Merged
merged 39 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
cb60623
feat(task): dependencies
dsherret Oct 22, 2024
526834d
Merge branch 'main' into feat_task_dependencies
bartlomieju Nov 14, 2024
7f2e2bd
support for description
bartlomieju Nov 14, 2024
8d02b99
simplify
bartlomieju Nov 14, 2024
9e16466
further refactor
bartlomieju Nov 14, 2024
ba70860
add a todo
bartlomieju Nov 14, 2024
1bddb7c
Merge branch 'main' into feat_task_dependencies
bartlomieju Nov 16, 2024
b835b45
add basic tests
bartlomieju Nov 16, 2024
9e206db
update tests
bartlomieju Nov 16, 2024
8e2cb55
add cross package test
bartlomieju Nov 16, 2024
e8a5459
fmt and lint
bartlomieju Nov 16, 2024
32b04fe
add failing test for 'diamond' dependencies
bartlomieju Nov 16, 2024
52762f9
update schema file
bartlomieju Nov 16, 2024
caedab6
Merge branch 'main' into feat_task_dependencies
bartlomieju Nov 17, 2024
ed367a2
change diamond dep test for now
bartlomieju Nov 17, 2024
05432d8
Merge branch 'main' into feat_task_dependencies
bartlomieju Nov 17, 2024
2e73474
feat: run task chains in topological order
marvinhagemeister Nov 18, 2024
efd9c80
fix: make clippy happy
marvinhagemeister Nov 18, 2024
71209f7
Merge branch 'main' into feat_task_dependencies
bartlomieju Nov 18, 2024
b54306b
fix: don't parse script value as task
marvinhagemeister Nov 18, 2024
b5978df
fix: remove accidentally committed fixture
marvinhagemeister Nov 18, 2024
89c5542
feat: parallelize tasks if possible
marvinhagemeister Nov 18, 2024
efbd227
fix: make clippy happy
marvinhagemeister Nov 18, 2024
d5c4dc6
this doesn't work
bartlomieju Nov 19, 2024
27d86a4
wip
bartlomieju Nov 19, 2024
b47c130
a
bartlomieju Nov 19, 2024
530ac3a
Merge branch 'main' into feat_task_dependencies
bartlomieju Nov 19, 2024
2317822
show which tasks we depend on
bartlomieju Nov 19, 2024
f537a2b
remove a clone
bartlomieju Nov 19, 2024
8e4e7d1
cleanup
bartlomieju Nov 19, 2024
903da22
remove TODO
bartlomieju Nov 19, 2024
52c2305
simplify a bit
bartlomieju Nov 19, 2024
9c60d6f
fix a test
bartlomieju Nov 19, 2024
245e813
Use refcell
dsherret Nov 19, 2024
b35e17a
add missing newline
bartlomieju Nov 19, 2024
c7f8c4b
Show cycle detection, move state into method call, extract function f…
dsherret Nov 19, 2024
45017a5
Remove marked now that we keep track of the path
dsherret Nov 19, 2024
b4f8593
Merge branch 'main' into feat_task_dependencies
bartlomieju Nov 19, 2024
f00edf1
remove todo
bartlomieju Nov 19, 2024
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
7 changes: 7 additions & 0 deletions cli/schemas/config-file.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,13 @@
"type": "string",
"required": true,
"description": "The task to execute"
},
"dependencies": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tasks that should be executed before this task"
}
}
}
Expand Down
229 changes: 215 additions & 14 deletions cli/tools/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use std::collections::HashMap;
use std::collections::HashSet;
use std::num::NonZeroUsize;
use std::path::Path;
use std::path::PathBuf;
use std::rc::Rc;
Expand All @@ -15,6 +16,10 @@ use deno_core::anyhow::anyhow;
use deno_core::anyhow::bail;
use deno_core::anyhow::Context;
use deno_core::error::AnyError;
use deno_core::futures::future::LocalBoxFuture;
use deno_core::futures::stream::futures_unordered;
use deno_core::futures::FutureExt;
use deno_core::futures::StreamExt;
use deno_core::url::Url;
use deno_path_util::normalize_path;
use deno_runtime::deno_node::NodeResolver;
Expand Down Expand Up @@ -68,14 +73,23 @@ pub async fn execute_script(
let node_resolver = factory.node_resolver().await?;
let env_vars = task_runner::real_env_vars();

let no_of_concurrent_tasks = if let Ok(value) = std::env::var("DENO_JOBS") {
value.parse::<NonZeroUsize>().ok()
} else {
std::thread::available_parallelism().ok()
}
.unwrap_or_else(|| NonZeroUsize::new(2).unwrap());

let task_runner = TaskRunner {
tasks_config,
task_flags: &task_flags,
npm_resolver: npm_resolver.as_ref(),
node_resolver: node_resolver.as_ref(),
env_vars,
cli_options,
concurrency: no_of_concurrent_tasks.into(),
};

task_runner.run_task(task_name).await
}

Expand All @@ -93,30 +107,156 @@ struct TaskRunner<'a> {
node_resolver: &'a NodeResolver,
env_vars: HashMap<String, String>,
cli_options: &'a CliOptions,
concurrency: usize,
}

impl<'a> TaskRunner<'a> {
async fn run_task(
pub async fn run_task(
&self,
task_name: &String,
task_name: &str,
) -> Result<i32, deno_core::anyhow::Error> {
let Some((dir_url, task_or_script)) = self.tasks_config.task(task_name)
else {
if self.task_flags.is_run {
return Err(anyhow!("Task not found: {}", task_name));
match sort_tasks_topo(task_name, &self.tasks_config) {
Ok(sorted) => self.run_tasks_in_parallel(sorted).await,
Err(err) => match err {
TaskError::NotFound(name) => {
if self.task_flags.is_run {
return Err(anyhow!("Task not found: {}", name));
}

log::error!("Task not found: {}", name);
if log::log_enabled!(log::Level::Error) {
self.print_available_tasks()?;
}
Ok(1)
}
TaskError::TaskDepCycle { path } => {
log::error!("Task cycle detected: {}", path.join(" -> "));
Ok(1)
}
},
}
}

pub fn print_available_tasks(&self) -> Result<(), std::io::Error> {
print_available_tasks(
&mut std::io::stderr(),
&self.cli_options.start_dir,
&self.tasks_config,
)
}

async fn run_tasks_in_parallel(
&self,
task_names: Vec<String>,
) -> Result<i32, deno_core::anyhow::Error> {
struct PendingTasksContext {
completed: HashSet<String>,
running: HashSet<String>,
task_names: Vec<String>,
}

impl PendingTasksContext {
fn has_remaining_tasks(&self) -> bool {
self.completed.len() < self.task_names.len()
}

log::error!("Task not found: {}", task_name);
if log::log_enabled!(log::Level::Error) {
print_available_tasks(
&mut std::io::stderr(),
&self.cli_options.start_dir,
&self.tasks_config,
)?;
fn mark_complete(&mut self, task_name: String) {
self.running.remove(&task_name);
self.completed.insert(task_name);
}
return Ok(1);

fn get_next_task<'a>(
&mut self,
runner: &'a TaskRunner<'a>,
) -> Option<LocalBoxFuture<'a, Result<(i32, String), AnyError>>> {
for name in &self.task_names {
if self.completed.contains(name) || self.running.contains(name) {
continue;
}

let should_run = if let Ok((_, def)) = runner.get_task(name) {
match def {
TaskOrScript::Task(_, def) => def
.dependencies
.iter()
.all(|dep| self.completed.contains(dep)),
TaskOrScript::Script(_, _) => true,
}
} else {
false
};

if !should_run {
continue;
}

self.running.insert(name.clone());
let name = name.clone();
return Some(
async move {
runner
.run_task_no_dependencies(&name)
.await
.map(|exit_code| (exit_code, name))
}
.boxed_local(),
);
}
None
}
}

let mut context = PendingTasksContext {
completed: HashSet::with_capacity(task_names.len()),
running: HashSet::with_capacity(self.concurrency),
task_names,
};

let mut queue = futures_unordered::FuturesUnordered::new();

while context.has_remaining_tasks() {
while queue.len() < self.concurrency {
if let Some(task) = context.get_next_task(self) {
queue.push(task);
} else {
break;
}
}

// If queue is empty at this point, then there are no more tasks in the queue.
let Some(result) = queue.next().await else {
debug_assert_eq!(context.task_names.len(), 0);
break;
};

let (exit_code, name) = result?;
if exit_code > 0 {
return Ok(exit_code);
}

context.mark_complete(name);
}

Ok(0)
}

fn get_task(
&self,
task_name: &str,
) -> Result<(&Url, TaskOrScript), TaskError> {
let Some(result) = self.tasks_config.task(task_name) else {
return Err(TaskError::NotFound(task_name.to_string()));
};

Ok(result)
}

async fn run_task_no_dependencies(
&self,
task_name: &String,
) -> Result<i32, deno_core::anyhow::Error> {
let (dir_url, task_or_script) = self.get_task(task_name.as_str()).unwrap();

match task_or_script {
TaskOrScript::Task(_tasks, definition) => {
self.run_deno_task(dir_url, task_name, definition).await
Expand Down Expand Up @@ -234,6 +374,59 @@ impl<'a> TaskRunner<'a> {
}
}

#[derive(Debug)]
enum TaskError {
NotFound(String),
TaskDepCycle { path: Vec<String> },
}

fn sort_tasks_topo(
name: &str,
task_config: &WorkspaceTasksConfig,
) -> Result<Vec<String>, TaskError> {
fn sort_visit<'a>(
name: &'a str,
sorted: &mut Vec<String>,
mut path: Vec<&'a str>,
tasks_config: &'a WorkspaceTasksConfig,
) -> Result<(), TaskError> {
// Already sorted
if sorted.iter().any(|sorted_name| sorted_name == name) {
return Ok(());
}

// Graph has a cycle
if path.contains(&name) {
path.push(name);
return Err(TaskError::TaskDepCycle {
path: path.iter().map(|s| s.to_string()).collect(),
});
}

let Some(def) = tasks_config.task(name) else {
return Err(TaskError::NotFound(name.to_string()));
};

if let TaskOrScript::Task(_, actual_def) = def.1 {
for dep in &actual_def.dependencies {
let mut path = path.clone();
path.push(name);
sort_visit(dep, sorted, path, tasks_config)?
}
}

sorted.push(name.to_string());

Ok(())
}

let mut sorted: Vec<String> = vec![];

sort_visit(name, &mut sorted, Vec::new(), task_config)?;

Ok(sorted)
}

fn output_task(task_name: &str, script: &str) {
log::info!(
"{} {} {}",
Expand Down Expand Up @@ -339,6 +532,14 @@ fn print_available_tasks(
)?;
}
writeln!(writer, " {}", desc.task.command)?;
if !desc.task.dependencies.is_empty() {
writeln!(
writer,
" {} {}",
colors::gray("depends on:"),
colors::cyan(desc.task.dependencies.join(", "))
)?;
}
}

Ok(())
Expand Down
61 changes: 61 additions & 0 deletions tests/specs/task/dependencies/__test__.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"tests": {
"basic1": {
"cwd": "basic1",
"tempDir": true,
"args": "task run",
"output": "./basic1.out"
},
"basic2": {
"cwd": "basic2",
"tempDir": true,
"args": "task run",
"output": "./basic2.out"
},
"cross_package": {
"cwd": "cross_package/package1",
"tempDir": true,
"args": "task run",
"output": "./cross_package.out",
"exitCode": 1
},
"diamond": {
"cwd": "diamond",
"tempDir": true,
"args": "task a",
"output": "./diamond.out"
},
"diamond_list": {
"cwd": "diamond",
"tempDir": true,
"args": "task",
"output": "./diamond_list.out"
},
"diamond_big": {
"cwd": "diamond_big",
"tempDir": true,
"args": "task a",
"output": "./diamond_big.out"
},
"diamond_big_list": {
"cwd": "diamond_big",
"tempDir": true,
"args": "task",
"output": "./diamond_big_list.out"
},
"cycle": {
"cwd": "cycle",
"tempDir": true,
"output": "./cycle.out",
"args": "task a",
"exitCode": 1
},
"cycle_2": {
"cwd": "cycle_2",
"tempDir": true,
"args": "task a",
"output": "./cycle_2.out",
"exitCode": 1
}
}
}
12 changes: 12 additions & 0 deletions tests/specs/task/dependencies/basic1.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Task build1 deno run ../build1.js
Task build2 deno run ../build2.js
[UNORDERED_START]
Starting build1
build1 performing more work...
build1 finished
Starting build2
build2 performing more work...
build2 finished
[UNORDERED_END]
Task run deno run ../run.js
run finished
10 changes: 10 additions & 0 deletions tests/specs/task/dependencies/basic1/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"tasks": {
"build1": "deno run ../build1.js",
"build2": "deno run ../build2.js",
"run": {
"command": "deno run ../run.js",
"dependencies": ["build1", "build2"]
}
}
}
10 changes: 10 additions & 0 deletions tests/specs/task/dependencies/basic2.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Task build1 deno run ../build1.js
Starting build1
build1 performing more work...
build1 finished
Task build2 deno run ../build2.js
Starting build2
build2 performing more work...
build2 finished
Task run deno run ../run.js
run finished
Loading