mirror of
https://github.com/denoland/deno.git
synced 2024-12-25 08:39:09 -05:00
fix(task): ensure root config always looks up dependencies in root (#26959)
We were accidentally looking up dependencies in the member.
This commit is contained in:
parent
d1b086b05a
commit
cb188cab7a
10 changed files with 205 additions and 70 deletions
|
@ -12,6 +12,7 @@ use deno_config::workspace::FolderConfigs;
|
||||||
use deno_config::workspace::TaskDefinition;
|
use deno_config::workspace::TaskDefinition;
|
||||||
use deno_config::workspace::TaskOrScript;
|
use deno_config::workspace::TaskOrScript;
|
||||||
use deno_config::workspace::WorkspaceDirectory;
|
use deno_config::workspace::WorkspaceDirectory;
|
||||||
|
use deno_config::workspace::WorkspaceMemberTasksConfig;
|
||||||
use deno_config::workspace::WorkspaceTasksConfig;
|
use deno_config::workspace::WorkspaceTasksConfig;
|
||||||
use deno_core::anyhow::anyhow;
|
use deno_core::anyhow::anyhow;
|
||||||
use deno_core::anyhow::bail;
|
use deno_core::anyhow::bail;
|
||||||
|
@ -270,11 +271,7 @@ impl<'a> TaskRunner<'a> {
|
||||||
pkg_tasks_config: &PackageTaskInfo,
|
pkg_tasks_config: &PackageTaskInfo,
|
||||||
) -> Result<i32, deno_core::anyhow::Error> {
|
) -> Result<i32, deno_core::anyhow::Error> {
|
||||||
match sort_tasks_topo(pkg_tasks_config) {
|
match sort_tasks_topo(pkg_tasks_config) {
|
||||||
Ok(sorted) => {
|
Ok(sorted) => self.run_tasks_in_parallel(sorted).await,
|
||||||
self
|
|
||||||
.run_tasks_in_parallel(&pkg_tasks_config.tasks_config, sorted)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
Err(err) => match err {
|
Err(err) => match err {
|
||||||
TaskError::NotFound(name) => {
|
TaskError::NotFound(name) => {
|
||||||
if self.task_flags.is_run {
|
if self.task_flags.is_run {
|
||||||
|
@ -308,64 +305,62 @@ impl<'a> TaskRunner<'a> {
|
||||||
|
|
||||||
async fn run_tasks_in_parallel(
|
async fn run_tasks_in_parallel(
|
||||||
&self,
|
&self,
|
||||||
tasks_config: &WorkspaceTasksConfig,
|
tasks: Vec<ResolvedTask<'a>>,
|
||||||
task_names: Vec<String>,
|
|
||||||
) -> Result<i32, deno_core::anyhow::Error> {
|
) -> Result<i32, deno_core::anyhow::Error> {
|
||||||
struct PendingTasksContext {
|
struct PendingTasksContext<'a> {
|
||||||
completed: HashSet<String>,
|
completed: HashSet<usize>,
|
||||||
running: HashSet<String>,
|
running: HashSet<usize>,
|
||||||
task_names: Vec<String>,
|
tasks: &'a [ResolvedTask<'a>],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PendingTasksContext {
|
impl<'a> PendingTasksContext<'a> {
|
||||||
fn has_remaining_tasks(&self) -> bool {
|
fn has_remaining_tasks(&self) -> bool {
|
||||||
self.completed.len() < self.task_names.len()
|
self.completed.len() < self.tasks.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mark_complete(&mut self, task_name: String) {
|
fn mark_complete(&mut self, task: &ResolvedTask) {
|
||||||
self.running.remove(&task_name);
|
self.running.remove(&task.id);
|
||||||
self.completed.insert(task_name);
|
self.completed.insert(task.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_next_task<'a>(
|
fn get_next_task<'b>(
|
||||||
&mut self,
|
&mut self,
|
||||||
runner: &'a TaskRunner<'a>,
|
runner: &'b TaskRunner<'b>,
|
||||||
tasks_config: &'a WorkspaceTasksConfig,
|
) -> Option<
|
||||||
) -> Option<LocalBoxFuture<'a, Result<(i32, String), AnyError>>> {
|
LocalBoxFuture<'b, Result<(i32, &'a ResolvedTask<'a>), AnyError>>,
|
||||||
for name in &self.task_names {
|
>
|
||||||
if self.completed.contains(name) || self.running.contains(name) {
|
where
|
||||||
|
'a: 'b,
|
||||||
|
{
|
||||||
|
for task in self.tasks.iter() {
|
||||||
|
if self.completed.contains(&task.id)
|
||||||
|
|| self.running.contains(&task.id)
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some((folder_url, task_or_script)) = tasks_config.task(name)
|
let should_run = task
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let should_run = match task_or_script {
|
|
||||||
TaskOrScript::Task(_, def) => def
|
|
||||||
.dependencies
|
.dependencies
|
||||||
.iter()
|
.iter()
|
||||||
.all(|dep| self.completed.contains(dep)),
|
.all(|dep_id| self.completed.contains(dep_id));
|
||||||
TaskOrScript::Script(_, _) => true,
|
|
||||||
};
|
|
||||||
|
|
||||||
if !should_run {
|
if !should_run {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.running.insert(name.clone());
|
self.running.insert(task.id);
|
||||||
let name = name.clone();
|
|
||||||
return Some(
|
return Some(
|
||||||
async move {
|
async move {
|
||||||
match task_or_script {
|
match task.task_or_script {
|
||||||
TaskOrScript::Task(_, def) => {
|
TaskOrScript::Task(_, def) => {
|
||||||
runner.run_deno_task(folder_url, &name, def).await
|
runner.run_deno_task(task.folder_url, task.name, def).await
|
||||||
}
|
}
|
||||||
TaskOrScript::Script(scripts, _) => {
|
TaskOrScript::Script(scripts, _) => {
|
||||||
runner.run_npm_script(folder_url, &name, scripts).await
|
runner
|
||||||
|
.run_npm_script(task.folder_url, task.name, scripts)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map(|exit_code| (exit_code, name))
|
.map(|exit_code| (exit_code, task))
|
||||||
}
|
}
|
||||||
.boxed_local(),
|
.boxed_local(),
|
||||||
);
|
);
|
||||||
|
@ -375,16 +370,16 @@ impl<'a> TaskRunner<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut context = PendingTasksContext {
|
let mut context = PendingTasksContext {
|
||||||
completed: HashSet::with_capacity(task_names.len()),
|
completed: HashSet::with_capacity(tasks.len()),
|
||||||
running: HashSet::with_capacity(self.concurrency),
|
running: HashSet::with_capacity(self.concurrency),
|
||||||
task_names,
|
tasks: &tasks,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut queue = futures_unordered::FuturesUnordered::new();
|
let mut queue = futures_unordered::FuturesUnordered::new();
|
||||||
|
|
||||||
while context.has_remaining_tasks() {
|
while context.has_remaining_tasks() {
|
||||||
while queue.len() < self.concurrency {
|
while queue.len() < self.concurrency {
|
||||||
if let Some(task) = context.get_next_task(self, tasks_config) {
|
if let Some(task) = context.get_next_task(self) {
|
||||||
queue.push(task);
|
queue.push(task);
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
|
@ -393,7 +388,7 @@ impl<'a> TaskRunner<'a> {
|
||||||
|
|
||||||
// If queue is empty at this point, then there are no more tasks in the queue.
|
// If queue is empty at this point, then there are no more tasks in the queue.
|
||||||
let Some(result) = queue.next().await else {
|
let Some(result) = queue.next().await else {
|
||||||
debug_assert_eq!(context.task_names.len(), 0);
|
debug_assert_eq!(context.tasks.len(), 0);
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -521,46 +516,105 @@ enum TaskError {
|
||||||
TaskDepCycle { path: Vec<String> },
|
TaskDepCycle { path: Vec<String> },
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sort_tasks_topo(
|
struct ResolvedTask<'a> {
|
||||||
pkg_task_config: &PackageTaskInfo,
|
id: usize,
|
||||||
) -> Result<Vec<String>, TaskError> {
|
name: &'a str,
|
||||||
|
folder_url: &'a Url,
|
||||||
|
task_or_script: TaskOrScript<'a>,
|
||||||
|
dependencies: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sort_tasks_topo<'a>(
|
||||||
|
pkg_task_config: &'a PackageTaskInfo,
|
||||||
|
) -> Result<Vec<ResolvedTask<'a>>, TaskError> {
|
||||||
|
trait TasksConfig {
|
||||||
|
fn task(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
) -> Option<(&Url, TaskOrScript, &dyn TasksConfig)>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TasksConfig for WorkspaceTasksConfig {
|
||||||
|
fn task(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
) -> Option<(&Url, TaskOrScript, &dyn TasksConfig)> {
|
||||||
|
if let Some(member) = &self.member {
|
||||||
|
if let Some((dir_url, task_or_script)) = member.task(name) {
|
||||||
|
return Some((dir_url, task_or_script, self as &dyn TasksConfig));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(root) = &self.root {
|
||||||
|
if let Some((dir_url, task_or_script)) = root.task(name) {
|
||||||
|
// switch to only using the root tasks for the dependencies
|
||||||
|
return Some((dir_url, task_or_script, root as &dyn TasksConfig));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TasksConfig for WorkspaceMemberTasksConfig {
|
||||||
|
fn task(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
) -> Option<(&Url, TaskOrScript, &dyn TasksConfig)> {
|
||||||
|
self.task(name).map(|(dir_url, task_or_script)| {
|
||||||
|
(dir_url, task_or_script, self as &dyn TasksConfig)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn sort_visit<'a>(
|
fn sort_visit<'a>(
|
||||||
name: &'a str,
|
name: &'a str,
|
||||||
sorted: &mut Vec<String>,
|
sorted: &mut Vec<ResolvedTask<'a>>,
|
||||||
mut path: Vec<&'a str>,
|
mut path: Vec<(&'a Url, &'a str)>,
|
||||||
tasks_config: &'a WorkspaceTasksConfig,
|
tasks_config: &'a dyn TasksConfig,
|
||||||
) -> Result<(), TaskError> {
|
) -> Result<usize, TaskError> {
|
||||||
// Already sorted
|
let Some((folder_url, task_or_script, tasks_config)) =
|
||||||
if sorted.iter().any(|sorted_name| sorted_name == name) {
|
tasks_config.task(name)
|
||||||
return Ok(());
|
else {
|
||||||
}
|
|
||||||
|
|
||||||
// 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((_, task_or_script)) = tasks_config.task(name) else {
|
|
||||||
return Err(TaskError::NotFound(name.to_string()));
|
return Err(TaskError::NotFound(name.to_string()));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(existing_task) = sorted
|
||||||
|
.iter()
|
||||||
|
.find(|task| task.name == name && task.folder_url == folder_url)
|
||||||
|
{
|
||||||
|
// already exists
|
||||||
|
return Ok(existing_task.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if path.contains(&(folder_url, name)) {
|
||||||
|
path.push((folder_url, name));
|
||||||
|
return Err(TaskError::TaskDepCycle {
|
||||||
|
path: path.iter().map(|(_, s)| s.to_string()).collect(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dependencies: Vec<usize> = Vec::new();
|
||||||
if let TaskOrScript::Task(_, task) = task_or_script {
|
if let TaskOrScript::Task(_, task) = task_or_script {
|
||||||
|
dependencies.reserve(task.dependencies.len());
|
||||||
for dep in &task.dependencies {
|
for dep in &task.dependencies {
|
||||||
let mut path = path.clone();
|
let mut path = path.clone();
|
||||||
path.push(name);
|
path.push((folder_url, name));
|
||||||
sort_visit(dep, sorted, path, tasks_config)?
|
dependencies.push(sort_visit(dep, sorted, path, tasks_config)?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sorted.push(name.to_string());
|
let id = sorted.len();
|
||||||
|
sorted.push(ResolvedTask {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
folder_url,
|
||||||
|
task_or_script,
|
||||||
|
dependencies,
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut sorted: Vec<String> = vec![];
|
let mut sorted: Vec<ResolvedTask<'a>> = vec![];
|
||||||
|
|
||||||
for name in &pkg_task_config.matched_tasks {
|
for name in &pkg_task_config.matched_tasks {
|
||||||
sort_visit(name, &mut sorted, Vec::new(), &pkg_task_config.tasks_config)?;
|
sort_visit(name, &mut sorted, Vec::new(), &pkg_task_config.tasks_config)?;
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"args": "task a",
|
||||||
|
"cwd": "member",
|
||||||
|
"output": "task.out"
|
||||||
|
}
|
10
tests/specs/task/dependencies_root_not_cycle/deno.json
Normal file
10
tests/specs/task/dependencies_root_not_cycle/deno.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"tasks": {
|
||||||
|
"a": "echo root-a",
|
||||||
|
"b": {
|
||||||
|
"dependencies": ["a"],
|
||||||
|
"command": "echo b"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workspace": ["./member"]
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"tasks": {
|
||||||
|
"a": {
|
||||||
|
"dependencies": ["b"],
|
||||||
|
"command": "echo a"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
tests/specs/task/dependencies_root_not_cycle/task.out
Normal file
6
tests/specs/task/dependencies_root_not_cycle/task.out
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
Task a echo root-a
|
||||||
|
root-a
|
||||||
|
Task b echo b
|
||||||
|
b
|
||||||
|
Task a echo a
|
||||||
|
a
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"tests": {
|
||||||
|
"root_dependending_root": {
|
||||||
|
"args": "task root-depending-root",
|
||||||
|
"cwd": "member",
|
||||||
|
"output": "root_dependending_root.out"
|
||||||
|
},
|
||||||
|
"member_depending_root_and_member": {
|
||||||
|
"args": "task member-dependending-root-and-member",
|
||||||
|
"cwd": "member",
|
||||||
|
"output": "member_depending_root_and_member.out"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
tests/specs/task/dependencies_shadowed_root_name/deno.jsonc
Normal file
12
tests/specs/task/dependencies_shadowed_root_name/deno.jsonc
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"tasks": {
|
||||||
|
"build": "echo root",
|
||||||
|
"root-depending-root": {
|
||||||
|
"dependencies": [
|
||||||
|
"build"
|
||||||
|
],
|
||||||
|
"command": "echo test"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workspace": ["./member"]
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"tasks": {
|
||||||
|
"build": "echo member",
|
||||||
|
"member-dependending-root-and-member": {
|
||||||
|
"dependencies": [
|
||||||
|
"build",
|
||||||
|
"root-depending-root"
|
||||||
|
],
|
||||||
|
"command": "echo member-test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
[UNORDERED_START]
|
||||||
|
Task build echo member
|
||||||
|
member
|
||||||
|
Task build echo root
|
||||||
|
root
|
||||||
|
Task root-depending-root echo test
|
||||||
|
test
|
||||||
|
[UNORDERED_END]
|
||||||
|
Task member-dependending-root-and-member echo member-test
|
||||||
|
member-test
|
|
@ -0,0 +1,4 @@
|
||||||
|
Task build echo root
|
||||||
|
root
|
||||||
|
Task root-depending-root echo test
|
||||||
|
test
|
Loading…
Reference in a new issue