Bail and signal error when the cwd of a resolved task doesn't exist (#32777)

Cole Miller created

Closes #32688

Release Notes:

- Fixed tasks (including build tasks for debug configurations) silently
using `/` as a working directory when the specified `cwd` didn't exist.

Change summary

Cargo.lock                                 |  1 +
crates/debugger_ui/src/session/running.rs  | 11 ++++++++++-
crates/task/Cargo.toml                     |  1 +
crates/task/src/task_template.rs           | 24 +++++++++++++++++++++++-
crates/terminal_view/src/terminal_panel.rs | 19 ++++++++++++++++++-
crates/workspace/src/workspace.rs          |  2 +-
6 files changed, 54 insertions(+), 4 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -15732,6 +15732,7 @@ dependencies = [
  "serde_json_lenient",
  "sha2",
  "shellexpand 2.1.2",
+ "smol",
  "util",
  "workspace-hack",
  "zed_actions",

crates/debugger_ui/src/session/running.rs 🔗

@@ -850,9 +850,18 @@ impl RunningState {
                         (task, None)
                     }
                 };
-                let Some(task) = task_template.resolve_task("debug-build-task", &task_context) else {
+                let Some(task) = task_template.resolve_task_and_check_cwd("debug-build-task", &task_context, cx.background_executor().clone()) else {
                     anyhow::bail!("Could not resolve task variables within a debug scenario");
                 };
+                let task = match task.await {
+                    Ok(task) => task,
+                    Err(e) => {
+                        workspace.update(cx, |workspace, cx| {
+                            workspace.show_error(&e, cx);
+                        }).ok();
+                        return Err(e)
+                    }
+                };
 
                 let locator_name = if let Some(locator_name) = locator_name {
                     debug_assert!(!config_is_valid);

crates/task/Cargo.toml 🔗

@@ -29,6 +29,7 @@ serde_json.workspace = true
 serde_json_lenient.workspace = true
 sha2.workspace = true
 shellexpand.workspace = true
+smol.workspace = true
 util.workspace = true
 workspace-hack.workspace = true
 zed_actions.workspace = true

crates/task/src/task_template.rs 🔗

@@ -1,5 +1,6 @@
-use anyhow::{Context as _, bail};
+use anyhow::{Context as _, Result, anyhow, bail};
 use collections::{HashMap, HashSet};
+use gpui::{BackgroundExecutor, Task};
 use schemars::{JsonSchema, r#gen::SchemaSettings};
 use serde::{Deserialize, Serialize};
 use sha2::{Digest, Sha256};
@@ -270,6 +271,27 @@ impl TaskTemplate {
             },
         })
     }
+
+    pub fn resolve_task_and_check_cwd(
+        &self,
+        id_base: &str,
+        task_context: &TaskContext,
+        executor: BackgroundExecutor,
+    ) -> Option<Task<Result<ResolvedTask>>> {
+        let resolved_task = self.resolve_task(id_base, task_context)?;
+        let task = executor.spawn(async move {
+            if let Some(cwd) = resolved_task.resolved.cwd.as_deref() {
+                match smol::fs::metadata(cwd).await {
+                    Ok(metadata) if metadata.is_dir() => Ok(resolved_task),
+                    Ok(_) => Err(anyhow!("cwd for resolved task is not a directory: {cwd:?}")),
+                    Err(e) => Err(e).context(format!("reading cwd of resolved task: {cwd:?}")),
+                }
+            } else {
+                Ok(resolved_task)
+            }
+        });
+        Some(task)
+    }
 }
 
 const MAX_DISPLAY_VARIABLE_LENGTH: usize = 15;

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -1461,10 +1461,27 @@ impl workspace::TerminalProvider for TerminalProvider {
         &self,
         task: SpawnInTerminal,
         window: &mut Window,
-        cx: &mut App,
+        cx: &mut Context<Workspace>,
     ) -> Task<Option<Result<ExitStatus>>> {
         let terminal_panel = self.0.clone();
+        let workspace = cx.weak_entity();
         window.spawn(cx, async move |cx| {
+            if let Some(cwd) = task.cwd.as_deref() {
+                let result = match smol::fs::metadata(cwd).await {
+                    Ok(metadata) if metadata.is_dir() => Ok(()),
+                    Ok(_) => Err(anyhow!("cwd for resolved task is not a directory: {cwd:?}")),
+                    Err(e) => Err(e).context(format!("reading cwd of resolved task: {cwd:?}")),
+                };
+                if let Err(e) = result {
+                    workspace
+                        .update(cx, |workspace, cx| {
+                            workspace.show_error(&e, cx);
+                        })
+                        .ok();
+                    return None;
+                }
+            }
+
             let terminal = terminal_panel
                 .update_in(cx, |terminal_panel, window, cx| {
                     terminal_panel.spawn_task(&task, window, cx)

crates/workspace/src/workspace.rs 🔗

@@ -135,7 +135,7 @@ pub trait TerminalProvider {
         &self,
         task: SpawnInTerminal,
         window: &mut Window,
-        cx: &mut App,
+        cx: &mut Context<Workspace>,
     ) -> Task<Option<Result<ExitStatus>>>;
 }