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",
+ }
+ }
+}