Add support for canonical output and work directories

Change-Id: Ie8038805822fca12d387f577f39c654e4949b526
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/qg/+/126668
Pigweed-Auto-Submit: Erik Gilling <konkers@google.com>
Reviewed-by: Alexei Frolov <frolv@google.com>
Commit-Queue: Erik Gilling <konkers@google.com>
diff --git a/qg/src/executor.rs b/qg/src/executor.rs
index 0efc07d..ac3887a 100644
--- a/qg/src/executor.rs
+++ b/qg/src/executor.rs
@@ -244,14 +244,16 @@
     async fn dispatch_target(&mut self, target: Arc<Target>) -> Result<()> {
         self.pending_targets
             .insert(target.full_name(), target.clone());
+        let output_dir = self.project.target_output_directory(&target)?;
+        let work_dir = self.project.target_work_directory(&target)?;
+
         self.dispatch_tx
             .send(ExecutionContext {
                 target,
                 // TODO(frolv): Allow setting platforms dynamically.
                 target_platform: Platform::current(),
-                // TODO(konkers): Add canonical location for `output_dir` and `work_dir`.
-                output_dir: "".into(),
-                work_dir: "".into(),
+                output_dir,
+                work_dir,
             })
             .await
             .map_err(|e| Error::StringErrorPlaceholder(format!("Error dispatching target: {e}")))
diff --git a/qg/src/project/mod.rs b/qg/src/project/mod.rs
index 35efbbf..dc22fbf 100644
--- a/qg/src/project/mod.rs
+++ b/qg/src/project/mod.rs
@@ -14,11 +14,12 @@
 
 use std::{
     ffi::OsStr,
+    fs,
     path::{Path, PathBuf},
     sync::Arc,
 };
 
-use crate::registry::Registry;
+use crate::{registry::Registry, Target};
 use crate::{Error, Result};
 use manifest::Manifest;
 
@@ -32,14 +33,14 @@
 #[derive(Debug)]
 pub struct Project {
     root: PathBuf,
-    cache_dir: PathBuf,
+    qg_dir: PathBuf,
     name: String,
     registry: Arc<Registry>,
 }
 
 impl Project {
     const MANIFEST_FILE: &str = "qg.toml";
-    const CACHE_DIRECTORY: &str = "qg-cache";
+    const QG_DIRECTORY: &str = "qg";
 
     /// Checks if a project name is valid.
     ///
@@ -103,7 +104,7 @@
 
         Ok(Self {
             root: project_root.to_owned(),
-            cache_dir: project_root.join(Self::CACHE_DIRECTORY),
+            qg_dir: project_root.join(Self::QG_DIRECTORY),
             name: manifest.project.name,
             registry: Arc::new(registry),
         })
@@ -159,13 +160,46 @@
         Self::relative_file(&self.root, Path::new(path.as_ref()))
     }
 
-    /// Returns a [`File`](self::File) representing some file located at `path`
-    /// within the project's cache directory.
+    /// Returns a `PathBuf` pointing to the directory located at `path` within
+    /// the project root.  Will create the directory if it does not exist.
     ///
     /// # Errors
-    /// Returns an [`Error::InvalidPath`] if the provided path is absolute.
-    pub fn cache_file(&self, path: impl AsRef<OsStr>) -> Result<self::File> {
-        Self::relative_file(&self.cache_dir, Path::new(path.as_ref()))
+    /// Returns an error if the directory does not exist and can not be
+    /// created.
+    fn directory(&self, path: impl AsRef<Path>) -> Result<PathBuf> {
+        let dir_path = self.root.join(path.as_ref());
+        fs::create_dir_all(&dir_path)?;
+        Ok(dir_path)
+    }
+
+    /// Returns a `PathBuf` pointing to the output directory for a given target.
+    /// Will create the directory if it does not exist.
+    ///
+    /// # Errors
+    /// Returns an error if the directory does not exist and can not be
+    /// created.
+    pub fn target_output_directory(&self, target: &Target) -> Result<PathBuf> {
+        self.directory(
+            self.qg_dir
+                .join("out")
+                .join(target.provider())
+                .join(target.name()),
+        )
+    }
+
+    /// Returns a `PathBuf` pointing to the output directory for a given target.
+    /// Will create the directory if it does not exist.
+    ///
+    /// # Errors
+    /// Returns an error if the directory does not exist and can not be
+    /// created.
+    pub fn target_work_directory(&self, target: &Target) -> Result<PathBuf> {
+        self.directory(
+            self.qg_dir
+                .join("work")
+                .join(target.provider())
+                .join(target.name()),
+        )
     }
 
     /// Returns the target registry for this project.
@@ -187,7 +221,7 @@
 mod tests {
     use std::collections::HashSet;
 
-    use crate::target::Metadata;
+    use crate::{target::Metadata, test_util::TestProject};
 
     use super::*;
 
@@ -210,7 +244,7 @@
     fn file_valid_path() {
         let root = PathBuf::from("/qg");
         let project = Project {
-            cache_dir: root.join("qg-cache"),
+            qg_dir: root.join("qg"),
             root,
             name: "qg2".into(),
             registry: Arc::new(Registry::new()),
@@ -224,17 +258,13 @@
             project.file("secrets/nuclear_codes.toml").unwrap().path(),
             Path::new("/qg/secrets/nuclear_codes.toml")
         );
-        assert_eq!(
-            project.cache_file("downloads.xml").unwrap().path(),
-            Path::new("/qg/qg-cache/downloads.xml")
-        );
     }
 
     #[test]
     fn file_invalid_path() {
         let root = PathBuf::from("/qg");
         let project = Project {
-            cache_dir: root.join("qg-cache"),
+            qg_dir: root.join("qg"),
             root,
             name: "qg2".into(),
             registry: Arc::new(Registry::new()),
@@ -244,15 +274,7 @@
             project.file("/bin").unwrap_err(),
             Error::InvalidPath,
         ));
-        assert!(matches!(
-            project.cache_file("/").unwrap_err(),
-            Error::InvalidPath,
-        ));
         assert!(matches!(project.file("").unwrap_err(), Error::InvalidPath));
-        assert!(matches!(
-            project.cache_file("").unwrap_err(),
-            Error::InvalidPath,
-        ));
     }
 
     #[test]
@@ -274,8 +296,29 @@
     }
 
     #[test]
+    fn out_and_work_directories() {
+        let test_project = TestProject::new("./src/test_projects/dependency_test").unwrap();
+        let project = &test_project.project;
+        assert_eq!(project.name, "dep-test");
+
+        // Neither dep-test:a's out or work directories should exist before we
+        // ask for their paths.
+        assert!(!test_project.relative_path("qg/out/dep-test/a").exists());
+        assert!(!test_project.relative_path("qg/work/dep-test/a").exists());
+
+        let target = project.registry().get_target("dep-test:a").unwrap();
+
+        let _ = project.target_output_directory(&target).unwrap();
+        assert!(test_project.relative_path("qg/out/dep-test/a").exists());
+
+        let _ = project.target_work_directory(&target).unwrap();
+        assert!(test_project.relative_path("qg/work/dep-test/a").exists());
+    }
+
+    #[test]
     fn simple_dependencies() {
-        let project = Project::load("./src/test_projects/dependency_test").unwrap();
+        let test_project = TestProject::new("./src/test_projects/dependency_test").unwrap();
+        let project = &test_project.project;
         assert_eq!(project.name, "dep-test");
 
         let registry = project.registry();
diff --git a/qg/src/test_util.rs b/qg/src/test_util.rs
index e4db038..156a629 100644
--- a/qg/src/test_util.rs
+++ b/qg/src/test_util.rs
@@ -47,7 +47,6 @@
 
     /// Returns a `PathBuf` to the file at the relative path `path` within
     /// the `TestProject` temporary copy.
-    #[allow(unused)]
     pub fn relative_path(&self, path: impl AsRef<Path>) -> PathBuf {
         self.project_root.path().join(path)
     }