Update target parsing

This makes several changes to target parsing:
- Rename "packages" to "targets".
- Assume providers are locally namespaced unless explicitly specified as
  global.
- Implicitly create a provider for each project.
- Allow projects to define targets within their own manifests as well as
  through external provider files.
- Make target names globally unique.
- Allow dependency-only targets.

Change-Id: I9b28dc3415837e82835ce3e1374b866cd2bce14d
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/qg/+/124891
Commit-Queue: Alexei Frolov <frolv@google.com>
Reviewed-by: Erik Gilling <konkers@google.com>
diff --git a/qg-cli/src/main.rs b/qg-cli/src/main.rs
index 67a82a0..ea1a6d8 100644
--- a/qg-cli/src/main.rs
+++ b/qg-cli/src/main.rs
@@ -17,8 +17,8 @@
 #![warn(clippy::pedantic)]
 
 mod hello;
-mod packages;
 mod subcommands;
+mod target;
 
 #[cfg(feature = "new_command")]
 mod new;
diff --git a/qg-cli/src/subcommands.rs b/qg-cli/src/subcommands.rs
index 43b00c4..39bdf66 100644
--- a/qg-cli/src/subcommands.rs
+++ b/qg-cli/src/subcommands.rs
@@ -14,7 +14,7 @@
 
 use anyhow::Result;
 
-use crate::{hello, packages};
+use crate::{hello, target};
 
 #[cfg(feature = "new_command")]
 use crate::new;
@@ -29,10 +29,10 @@
 #[derive(clap::Parser, Debug)]
 pub enum Subcommands {
     Hello(hello::Command),
+    Target(target::Command),
 
     #[cfg(feature = "new_command")]
     New(new::Command),
-    Package(packages::Command),
 
     #[cfg(feature = "python")]
     PyDemo(py_demo::Command),
@@ -45,8 +45,8 @@
             Self::Hello(args) => {
                 hello::run(args);
             }
-            Self::Package(args) => {
-                packages::run(args).await?;
+            Self::Target(args) => {
+                target::run(args).await?;
             }
             #[cfg(feature = "new_command")]
             Self::New(args) => {
diff --git a/qg-cli/src/packages.rs b/qg-cli/src/target.rs
similarity index 72%
rename from qg-cli/src/packages.rs
rename to qg-cli/src/target.rs
index 18d1c65..7fd8bef 100644
--- a/qg-cli/src/packages.rs
+++ b/qg-cli/src/target.rs
@@ -23,16 +23,18 @@
 // TODO(frolv): Actually use async.
 #[allow(clippy::unused_async)]
 pub async fn run(_args: &Command) -> Result<()> {
-    let registry = Project::locate()?.parse_manifests()?;
+    let project = Project::locate()?;
+    let registry = project.parse_manifests()?;
 
-    for (name, packages) in registry.packages_by_name() {
-        if packages.len() == 1 {
-            println!("{name}");
-        } else {
-            for package in packages {
-                println!("{}", package.canonical_slug());
-            }
-        }
+    println!(
+        "Project {}, {} total targets",
+        project.name(),
+        registry.target_count()
+    );
+
+    for (name, target) in registry.targets() {
+        println!("* {name}");
+        println!("  type: {}", target.type_string());
     }
 
     Ok(())
diff --git a/qg/src/lib.rs b/qg/src/lib.rs
index 91e3893..8c7c509 100644
--- a/qg/src/lib.rs
+++ b/qg/src/lib.rs
@@ -18,12 +18,12 @@
 
 use std::path::{Path, PathBuf};
 
-pub mod package;
 pub mod project;
 pub mod registry;
+pub mod target;
 
 #[doc(inline)]
-pub use package::Package;
+pub use target::Target;
 
 #[doc(inline)]
 pub use project::Project;
diff --git a/qg/src/package.rs b/qg/src/package.rs
deleted file mode 100644
index 44bf754..0000000
--- a/qg/src/package.rs
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright 2022 The Pigweed Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-//     https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-use crate::project::manifest;
-
-/// An installable `qg` package.
-#[derive(Debug)]
-pub struct Package {
-    name: String,
-    provider: String,
-    metadata: Metadata,
-}
-
-#[derive(Debug)]
-enum Metadata {
-    Cipd(manifest::CipdPackage),
-}
-
-impl From<manifest::PackageType> for Metadata {
-    fn from(pt: manifest::PackageType) -> Self {
-        match pt {
-            manifest::PackageType::Cipd(data) => Self::Cipd(data),
-        }
-    }
-}
-
-impl Package {
-    pub(crate) fn from_manifest(name: &str, provider: &str, package: manifest::Package) -> Self {
-        Self {
-            name: name.to_owned(),
-            provider: provider.to_owned(),
-            metadata: package.desc.into(),
-        }
-    }
-
-    /// Returns the name of the package.
-    #[must_use]
-    pub fn name(&self) -> &str {
-        &self.name
-    }
-
-    /// Returns the name of the package's provider.
-    #[must_use]
-    pub fn provider(&self) -> &str {
-        &self.provider
-    }
-
-    /// Returns the type of the package as a string.
-    #[must_use]
-    pub fn type_string(&self) -> &str {
-        match self.metadata {
-            Metadata::Cipd(_) => "cipd",
-        }
-    }
-
-    /// Returns the globally-unique identifier for the package.
-    #[must_use]
-    pub fn canonical_slug(&self) -> String {
-        if self.provider.is_empty() {
-            self.name.clone()
-        } else {
-            format!("{}:{}", self.provider, self.name)
-        }
-    }
-}
diff --git a/qg/src/project/manifest.rs b/qg/src/project/manifest.rs
index 271130f..5e6e003 100644
--- a/qg/src/project/manifest.rs
+++ b/qg/src/project/manifest.rs
@@ -16,13 +16,19 @@
 
 use serde::{Deserialize, Serialize};
 
+/// Top-level manifest file for a project.
 #[derive(Debug, Deserialize, Serialize)]
 pub struct Manifest {
     /// Information about the project to which the manifest belongs.
     pub project: Project,
 
+    /// Additional target providers defined by the project.
     #[serde(default)]
     pub providers: HashMap<String, ProviderDescriptor>,
+
+    /// Targets provided directly by the project.
+    #[serde(default)]
+    pub targets: HashMap<String, Target>,
 }
 
 /// A qg-based project.
@@ -32,29 +38,59 @@
     pub name: String,
 }
 
+/// Scoping of targets in a provider.
+#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum ProviderNamespace {
+    /// Targets are namespaced under the provider.
+    Local,
+
+    /// Targets appear in the global namespace.
+    Global,
+}
+
+impl Default for ProviderNamespace {
+    fn default() -> Self {
+        Self::Local
+    }
+}
+
+/// Information about a target provider.
 #[derive(Debug, Deserialize, Serialize)]
 pub struct ProviderDescriptor {
+    /// Path to a file in which the provider is defined.
     pub manifest: Option<PathBuf>,
+
+    /// Default scoping of the provider's targets.
+    #[serde(default)]
+    pub namespace: ProviderNamespace,
 }
 
+/// Manifest file defining a target provider.
 #[derive(Debug, Deserialize, Serialize)]
 pub struct ProviderFile {
+    /// Metadata about the provider.
+    pub provider: Option<ProviderDescriptor>,
+
+    /// Targets defined by the provider.
     #[serde(default)]
-    pub packages: HashMap<String, Package>,
+    pub targets: HashMap<String, Target>,
 }
 
+/// A buildable target.
 #[derive(Debug, Deserialize, Serialize)]
-pub struct Package {
+pub struct Target {
     #[serde(flatten)]
-    pub desc: PackageType,
+    pub desc: Option<TargetType>,
 }
 
 #[derive(Debug, Deserialize, Serialize)]
 #[serde(tag = "type", rename_all = "snake_case")]
-pub enum PackageType {
+pub enum TargetType {
     Cipd(CipdPackage),
 }
 
+/// A downloadable CIPD package.
 #[derive(Debug, Deserialize, Serialize)]
 pub struct CipdPackage {
     /// The location on of the package in CIPD's repositories.
@@ -80,6 +116,7 @@
                 name: name.to_owned(),
             },
             providers: HashMap::new(),
+            targets: HashMap::new(),
         }
     }
 }
diff --git a/qg/src/project/mod.rs b/qg/src/project/mod.rs
index 32183a0..bbd2de8 100644
--- a/qg/src/project/mod.rs
+++ b/qg/src/project/mod.rs
@@ -17,7 +17,7 @@
     path::{Path, PathBuf},
 };
 
-use crate::registry::Registry;
+use crate::{registry::Registry, target::Provider};
 use crate::{Error, Result};
 use manifest::Manifest;
 
@@ -157,16 +157,36 @@
         let root_manifest = root_manifest_file.deserialize_toml::<Manifest>()?;
         let mut registry = Registry::new();
 
+        let project_provider = Provider::new(
+            &root_manifest.project.name,
+            root_manifest_file.path(),
+            false,
+        );
+        registry.add_provider(project_provider);
+
+        for (name, target) in root_manifest.targets {
+            registry.add_target(crate::Target::from_manifest(
+                &name,
+                &root_manifest.project.name,
+                target,
+            ));
+        }
+
         for (provider, desc) in &root_manifest.providers {
-            registry.add_provider(provider);
-
             if let Some(manifest) = &desc.manifest {
-                let provider_file = root_manifest_file
-                    .relative_file(manifest)
-                    .deserialize_toml::<manifest::ProviderFile>()?;
+                let provider_file = root_manifest_file.relative_file(manifest);
 
-                for (name, package) in provider_file.packages {
-                    registry.add_package(crate::Package::from_manifest(&name, provider, package));
+                let provider_data = provider_file.deserialize_toml::<manifest::ProviderFile>()?;
+                let is_global = if let Some(desc) = &provider_data.provider {
+                    desc.namespace == manifest::ProviderNamespace::Global
+                } else {
+                    false
+                };
+
+                registry.add_provider(Provider::new(provider, provider_file.path(), is_global));
+
+                for (name, target) in provider_data.targets {
+                    registry.add_target(crate::Target::from_manifest(&name, provider, target));
                 }
             }
         }
diff --git a/qg/src/registry.rs b/qg/src/registry.rs
index ada1f20..dbad628 100644
--- a/qg/src/registry.rs
+++ b/qg/src/registry.rs
@@ -12,26 +12,18 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-use std::{
-    collections::{HashMap, HashSet},
-    sync::Arc,
-};
+use std::{collections::HashMap, sync::Arc};
 
-use crate::Package;
+use crate::target::{Provider, Target};
 
 /// A database of packages known to `qg`.
 #[derive(Debug)]
 pub struct Registry {
     /// Mapping of package slug to package. Slugs are globally unique.
-    packages: HashMap<String, Arc<Package>>,
-
-    /// Mapping of package name to packages. The same package can come form
-    /// multiple providers.
-    packages_by_name: HashMap<String, Vec<Arc<Package>>>,
+    targets: HashMap<String, Arc<Target>>,
 
     /// All known package providers by name.
-    /// TODO(frolv): Make this a map storing provider metadata.
-    providers: HashSet<String>,
+    providers: HashMap<String, Provider>,
 }
 
 impl Registry {
@@ -39,36 +31,43 @@
     #[must_use]
     pub fn new() -> Self {
         Self {
-            packages: HashMap::new(),
-            packages_by_name: HashMap::new(),
-            providers: HashSet::new(),
+            targets: HashMap::new(),
+            providers: HashMap::new(),
         }
     }
 
-    /// Returns an iterator over all known packages grouped by package name.
-    pub fn packages_by_name(&self) -> impl Iterator<Item = (&str, Vec<&Package>)> {
-        self.packages_by_name
+    /// Returns the number of registered targets.
+    #[must_use]
+    pub fn target_count(&self) -> usize {
+        self.targets.len()
+    }
+
+    /// Returns an iterator over all known targets, in arbitrary order.
+    pub fn targets(&self) -> impl Iterator<Item = (&str, &Target)> {
+        self.targets
             .iter()
-            .map(|(k, v)| (k.as_str(), v.iter().map(Arc::as_ref).collect()))
+            .map(|(k, v)| (k.as_str(), Arc::as_ref(v)))
     }
 
-    pub(crate) fn add_provider(&mut self, name: &str) -> bool {
-        self.providers.insert(name.to_owned())
+    pub(crate) fn add_provider(&mut self, provider: Provider) -> bool {
+        self.providers
+            .insert(provider.name.clone(), provider)
+            .is_none()
     }
 
-    pub(crate) fn add_package(&mut self, package: Package) -> bool {
-        if !self.providers.contains(package.provider()) {
+    pub(crate) fn add_target(&mut self, package: Target) -> bool {
+        let Some(provider) = self.providers.get(package.provider()) else {
             return false;
-        }
+        };
 
-        let package = Arc::new(package);
-        self.packages
-            .insert(package.canonical_slug(), package.clone());
+        let slug = if provider.global {
+            package.name().to_string()
+        } else {
+            format!("{}:{}", provider.name, package.name())
+        };
 
-        self.packages_by_name
-            .entry(package.name().into())
-            .or_default()
-            .push(package);
+        let target = Arc::new(package);
+        self.targets.insert(slug, target);
 
         true
     }
diff --git a/qg/src/target.rs b/qg/src/target.rs
new file mode 100644
index 0000000..a72a877
--- /dev/null
+++ b/qg/src/target.rs
@@ -0,0 +1,97 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+use std::path::{Path, PathBuf};
+
+use crate::project::manifest;
+
+/// A source of targets.
+#[derive(Debug)]
+pub struct Provider {
+    /// The globally-unique name of the provider.
+    pub name: String,
+
+    /// Manifest file in which the provider is defined.
+    pub file: PathBuf,
+
+    /// If true, targets provided by the provider appear in the global namespace
+    /// instead of nested under the provider's name.
+    pub global: bool,
+}
+
+impl Provider {
+    #[must_use]
+    pub(crate) fn new(name: &str, file: &Path, global: bool) -> Self {
+        Self {
+            name: name.into(),
+            file: file.into(),
+            global,
+        }
+    }
+}
+
+/// An installable `qg` package.
+#[derive(Debug)]
+pub struct Target {
+    name: String,
+    provider: String,
+    metadata: Metadata,
+}
+
+/// Additional information about how to build a target specific to a type
+/// of provider.
+#[derive(Debug)]
+enum Metadata {
+    DepOnly,
+    Cipd(manifest::CipdPackage),
+}
+
+impl From<manifest::TargetType> for Metadata {
+    fn from(tt: manifest::TargetType) -> Self {
+        match tt {
+            manifest::TargetType::Cipd(data) => Self::Cipd(data),
+        }
+    }
+}
+
+impl Target {
+    pub(crate) fn from_manifest(name: &str, provider: &str, target: manifest::Target) -> Self {
+        Self {
+            name: name.to_owned(),
+            provider: provider.to_owned(),
+            metadata: target.desc.map_or(Metadata::DepOnly, Metadata::from),
+        }
+    }
+
+    /// Returns the name of the package.
+    #[must_use]
+    pub fn name(&self) -> &str {
+        &self.name
+    }
+
+    /// Returns the name of the package's provider.
+    #[must_use]
+    pub fn provider(&self) -> &str {
+        &self.provider
+    }
+
+    /// Returns the type of the package as a string.
+    #[must_use]
+    pub fn type_string(&self) -> &str {
+        match self.metadata {
+            Metadata::DepOnly => "dep_only",
+            Metadata::Cipd(_) => "cipd",
+        }
+    }
+}