From 4ff5b281d37e2060be551b61a99c420e4c0c5fa4 Mon Sep 17 00:00:00 2001 From: Ryan Bottriell Date: Tue, 17 Dec 2024 10:39:02 -0800 Subject: [PATCH] Update spk cli to use workspaces to find spec files Although the concept is largely hidden (aside from the new flags) this adds the ability to define a workspace in the current directory and modify the default behavior of only loading spk.yaml files from the current directory. Signed-off-by: Ryan Bottriell --- Cargo.lock | 4 + Cargo.toml | 1 + Makefile | 2 +- README.md | 2 + crates/spk-cli/cmd-make-recipe/Cargo.toml | 1 + .../cmd-make-recipe/src/cmd_make_recipe.rs | 22 +- crates/spk-cli/cmd-test/src/cmd_test.rs | 13 +- crates/spk-cli/common/Cargo.toml | 1 + crates/spk-cli/common/src/flags.rs | 299 ++++++------------ crates/spk-cli/group2/src/cmd_num_variants.rs | 6 +- crates/spk-cli/group4/Cargo.toml | 1 + crates/spk-cli/group4/src/cmd_view.rs | 16 +- crates/spk-workspace/Cargo.toml | 1 + crates/spk-workspace/src/builder.rs | 69 ++-- crates/spk-workspace/src/error.rs | 27 ++ crates/spk-workspace/src/file.rs | 10 +- crates/spk-workspace/src/lib.rs | 2 +- crates/spk-workspace/src/workspace.rs | 141 ++++++++- workspace.spk.yml => workspace.spk.yaml | 0 19 files changed, 335 insertions(+), 283 deletions(-) rename workspace.spk.yml => workspace.spk.yaml (100%) diff --git a/Cargo.lock b/Cargo.lock index 7ecec1f72e..5d7910e7eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3993,6 +3993,7 @@ dependencies = [ "spk-schema", "spk-solve", "spk-storage", + "spk-workspace", "statsd 0.15.0", "strip-ansi-escapes 0.1.1", "thiserror", @@ -4095,6 +4096,7 @@ dependencies = [ "spk-schema", "spk-solve", "spk-storage", + "spk-workspace", "strum", "tokio", "tracing", @@ -4247,6 +4249,7 @@ dependencies = [ "spk-cli-common", "spk-schema", "spk-schema-tera", + "spk-workspace", "tracing", ] @@ -4674,6 +4677,7 @@ dependencies = [ "spk-solve", "tempfile", "thiserror", + "tracing", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3683aa7545..48fe62d87c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,6 +138,7 @@ spk-solve-package-iterator = { path = "crates/spk-solve/crates/package-iterator" spk-solve-solution = { path = "crates/spk-solve/crates/solution" } spk-solve-validation = { path = "crates/spk-solve/crates/validation" } spk-storage = { path = "crates/spk-storage" } +spk-workspace = { path = "crates/spk-workspace" } static_assertions = "1.1" strip-ansi-escapes = "0.2.0" strum = { version = "0.26.3", features = ["derive"] } diff --git a/Makefile b/Makefile index 7b664eb280..0c3b58c1de 100644 --- a/Makefile +++ b/Makefile @@ -86,7 +86,7 @@ test: .PHONY: converters converters: - $(MAKE) -C packages spk-convert-pip/spk-convert-pip.spk + spk build spk-convert-pip .PHONY: rpms rpms: spk-rpm spfs-rpm diff --git a/README.md b/README.md index cf7ccfcdb0..6789675577 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ SPK/SPFS are distributed using the [Apache-2.0 license](LICENSE). `spk` is the software packaging system built on top of SPFS. +The `packages` directory contains a collection of recipes that can be used as examples or to build out common open source software packages. + ## Contributing Please read [Contributing to SPK](CONTRIBUTING.md). diff --git a/crates/spk-cli/cmd-make-recipe/Cargo.toml b/crates/spk-cli/cmd-make-recipe/Cargo.toml index f13b3d4738..9ff3010d10 100644 --- a/crates/spk-cli/cmd-make-recipe/Cargo.toml +++ b/crates/spk-cli/cmd-make-recipe/Cargo.toml @@ -20,4 +20,5 @@ clap = { workspace = true, features = ["derive", "env"] } spk-cli-common = { workspace = true } spk-schema = { workspace = true } spk-schema-tera = { workspace = true } +spk-workspace = { workspace = true } tracing = "0.1.35" diff --git a/crates/spk-cli/cmd-make-recipe/src/cmd_make_recipe.rs b/crates/spk-cli/cmd-make-recipe/src/cmd_make_recipe.rs index b3cbce8f9d..2f6e76bf0d 100644 --- a/crates/spk-cli/cmd-make-recipe/src/cmd_make_recipe.rs +++ b/crates/spk-cli/cmd-make-recipe/src/cmd_make_recipe.rs @@ -2,13 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 // https://github.com/spkenv/spk -use std::sync::Arc; - use clap::Args; use miette::{Context, IntoDiagnostic, Result}; use spk_cli_common::{flags, CommandArgs, Run}; use spk_schema::foundation::format::FormatOptionMap; -use spk_schema::{SpecFileData, SpecTemplate, Template, TemplateExt}; +use spk_schema::{SpecFileData, Template}; /// Render a package spec template into a recipe /// @@ -20,6 +18,9 @@ pub struct MakeRecipe { #[clap(flatten)] pub options: flags::Options, + #[clap(flatten)] + pub workspace: flags::Workspace, + #[clap(short, long, global = true, action = clap::ArgAction::Count)] pub verbose: u8, @@ -44,16 +45,13 @@ impl Run for MakeRecipe { async fn run(&mut self) -> Result { let options = self.options.get_options()?; + let workspace = self.workspace.load_or_default()?; - let template = match flags::find_package_template(self.package.as_ref())? { - flags::FindPackageTemplateResult::NotFound(name) => { - Arc::new(SpecTemplate::from_file(name.as_ref())?) - } - res => { - let (_, template) = res.must_be_found(); - template - } - }; + let template = match self.package.as_ref() { + None => workspace.default_package_template(), + Some(p) => workspace.find_package_template(p), + } + .must_be_found(); if let Some(name) = template.name() { tracing::info!("rendering template for {name}"); diff --git a/crates/spk-cli/cmd-test/src/cmd_test.rs b/crates/spk-cli/cmd-test/src/cmd_test.rs index cb4192388a..64f238de21 100644 --- a/crates/spk-cli/cmd-test/src/cmd_test.rs +++ b/crates/spk-cli/cmd-test/src/cmd_test.rs @@ -33,6 +33,8 @@ pub struct CmdTest { pub runtime: flags::Runtime, #[clap(flatten)] pub repos: flags::Repositories, + #[clap(flatten)] + pub workspace: flags::Workspace, #[clap(short, long, global = true, action = clap::ArgAction::Count)] pub verbose: u8, @@ -73,6 +75,7 @@ impl Run for CmdTest { .into_iter() .map(|(_, r)| Arc::new(r)) .collect::>(); + let mut workspace = self.workspace.load_or_default()?; let source = if self.here { Some(".".into()) } else { None }; @@ -100,9 +103,13 @@ impl Run for CmdTest { } }; - let (spec_data, filename) = - flags::find_package_recipe_from_template_or_repo(Some(&name), &options, &repos) - .await?; + let (spec_data, filename) = flags::find_package_recipe_from_workspace_or_repo( + Some(&name), + &options, + &mut workspace, + &repos, + ) + .await?; let recipe = spec_data.into_recipe().wrap_err_with(|| { format!( "{filename} was expected to contain a recipe", diff --git a/crates/spk-cli/common/Cargo.toml b/crates/spk-cli/common/Cargo.toml index a9cdb674b7..2dd873b567 100644 --- a/crates/spk-cli/common/Cargo.toml +++ b/crates/spk-cli/common/Cargo.toml @@ -53,6 +53,7 @@ spk-exec = { workspace = true } spk-solve = { workspace = true } spk-schema = { workspace = true } spk-storage = { workspace = true } +spk-workspace = { workspace = true } statsd = { version = "0.15.0", optional = true } strip-ansi-escapes = { version = "0.1.1", optional = true } thiserror = { workspace = true } diff --git a/crates/spk-cli/common/src/flags.rs b/crates/spk-cli/common/src/flags.rs index 8fd476d71f..2bc895faec 100644 --- a/crates/spk-cli/common/src/flags.rs +++ b/crates/spk-cli/common/src/flags.rs @@ -33,18 +33,10 @@ use spk_schema::ident::{ VarRequest, }; use spk_schema::option_map::HOST_OPTIONS; -use spk_schema::{ - Recipe, - SpecFileData, - SpecRecipe, - SpecTemplate, - Template, - TemplateExt, - TestStage, - VariantExt, -}; +use spk_schema::{Recipe, SpecFileData, SpecRecipe, Template, TestStage, VariantExt}; #[cfg(feature = "statsd")] use spk_solve::{get_metrics_client, SPK_RUN_TIME_METRIC}; +use spk_workspace::FindPackageTemplateResult; pub use variant::{Variant, VariantBuildStatus, VariantLocation}; use {spk_solve as solve, spk_storage as storage}; @@ -350,6 +342,9 @@ pub struct Requests { /// Allow pre-releases for all command line package requests #[clap(long)] pub pre: bool, + + #[clap(flatten)] + pub workspace: Workspace, } impl Requests { @@ -361,9 +356,11 @@ impl Requests { repos: &[Arc], ) -> Result> { let mut idents = Vec::new(); + let mut workspace = self.workspace.load_or_default()?; for package in packages { if package.contains('@') { - let (recipe, _, stage, _) = parse_stage_specifier(package, options, repos).await?; + let (recipe, _, stage, _) = + parse_stage_specifier(package, options, &mut workspace, repos).await?; match stage { TestStage::Sources => { @@ -381,12 +378,13 @@ impl Requests { let path = std::path::Path::new(package); if path.is_file() { - let (filename, template) = find_package_template(Some(&package))?.must_be_found(); + let workspace = self.workspace.load_or_default()?; + let template = workspace.find_package_template(&package).must_be_found(); let rendered_data = template.render(options)?; let recipe = rendered_data.into_recipe().wrap_err_with(|| { format!( "{filename} was expected to contain a recipe", - filename = filename.to_string_lossy() + filename = template.file_path().to_string_lossy() ) })?; idents.push(recipe.ident().to_any_ident(None)); @@ -434,6 +432,7 @@ impl Requests { let override_options = options.get_options()?; let mut templating_options = override_options.clone(); let mut extra_options = OptionMap::default(); + let mut workspace = self.workspace.load_or_default()?; // From the positional REQUESTS arg for r in requests.into_iter() { @@ -441,10 +440,14 @@ impl Requests { // Is it a filepath to a package requests yaml file? if r.ends_with(".spk.yaml") { - let (spec, filename) = - find_package_recipe_from_template_or_repo(Some(&r), &templating_options, repos) - .await - .wrap_err_with(|| format!("finding requests file {r}"))?; + let (spec, filename) = find_package_recipe_from_workspace_or_repo( + Some(&r), + &templating_options, + &mut workspace, + repos, + ) + .await + .wrap_err_with(|| format!("finding requests file {r}"))?; let requests_from_file = spec.into_requests().wrap_err_with(|| { format!( "{filename} was expected to contain a list of requests", @@ -470,7 +473,7 @@ impl Requests { } let reqs = self - .parse_cli_or_pkg_file_request(r, &templating_options, repos) + .parse_cli_or_pkg_file_request(r, &templating_options, &mut workspace, repos) .await?; out.extend(reqs); } @@ -489,6 +492,7 @@ impl Requests { &self, request: &str, options: &OptionMap, + workspace: &mut spk_workspace::Workspace, repos: &[Arc], ) -> Result> { // Parses a command line request into one or more requests. @@ -496,11 +500,12 @@ impl Requests { let mut out = Vec::::new(); if request.contains('@') { - let (recipe, _, stage, build_variant) = parse_stage_specifier(request, options, repos) - .await - .wrap_err_with(|| { - format!("parsing {request} as a filename with stage specifier") - })?; + let (recipe, _, stage, build_variant) = + parse_stage_specifier(request, options, workspace, repos) + .await + .wrap_err_with(|| { + format!("parsing {request} as a filename with stage specifier") + })?; match stage { TestStage::Sources => { @@ -639,6 +644,7 @@ fn parse_package_stage_and_variant( pub async fn parse_stage_specifier( specifier: &str, options: &OptionMap, + workspace: &mut spk_workspace::Workspace, repos: &[Arc], ) -> Result<( Arc, @@ -649,7 +655,7 @@ pub async fn parse_stage_specifier( let (package, stage, build_variant) = parse_package_stage_and_variant(specifier) .wrap_err_with(|| format!("parsing {specifier} as a stage name and optional variant"))?; let (spec, filename) = - find_package_recipe_from_template_or_repo(Some(&package), options, repos) + find_package_recipe_from_workspace_or_repo(Some(&package), options, workspace, repos) .await .wrap_err_with(|| format!("finding package recipe for {package}"))?; @@ -663,6 +669,37 @@ pub async fn parse_stage_specifier( Ok((recipe, filename, stage, build_variant)) } +#[derive(Args, Default, Clone)] +pub struct Workspace { + /// The location of the spk workspace to find spec files in + #[clap(long, default_value = ".")] + pub workspace: std::path::PathBuf, +} + +impl Workspace { + pub fn load_or_default(&self) -> Result { + match spk_workspace::Workspace::builder().load_from_dir(&self.workspace) { + Ok(w) => { + tracing::debug!(workspace = ?self.workspace, "Loading workspace"); + w.build().into_diagnostic() + } + Err(spk_workspace::error::FromPathError::LoadWorkspaceFileError( + spk_workspace::error::LoadWorkspaceFileError::NoWorkspaceFile(_), + )) + | Err(spk_workspace::error::FromPathError::LoadWorkspaceFileError( + spk_workspace::error::LoadWorkspaceFileError::WorkspaceNotFound(_), + )) => { + tracing::debug!("Using virtual workspace in current dir"); + spk_workspace::Workspace::builder() + .with_glob_pattern("*.spk.yaml")? + .build() + .into_diagnostic() + } + Err(err) => Err(err.into()), + } + } +} + /// Specifies a package, allowing for more details when being invoked /// programmatically instead of by a user on the command line. #[derive(Clone, Debug)] @@ -703,6 +740,9 @@ pub struct Packages { /// The package names or yaml spec files to operate on #[clap(name = "PKG|SPEC_FILE")] pub packages: Vec, + + #[clap(flatten)] + pub workspace: Workspace, } impl CommandArgs for Packages { @@ -742,11 +782,14 @@ impl Packages { packages.push(None) } + let mut workspace = self.workspace.load_or_default()?; + let mut results = Vec::with_capacity(packages.len()); for package in packages { - let (file_data, path) = find_package_recipe_from_template_or_repo( + let (file_data, path) = find_package_recipe_from_workspace_or_repo( package.as_ref().map(|p| p.get_specifier()), options, + &mut workspace, repos, ) .await?; @@ -756,150 +799,6 @@ impl Packages { } } -/// The result of the [`find_package_template`] function. -// We are okay with the large variant here because it's specifically -// used as the positive result of the function, with the others simply -// denoting unique error cases. -#[allow(clippy::large_enum_variant)] -pub enum FindPackageTemplateResult { - /// A non-ambiguous package template file was found - Found { - path: std::path::PathBuf, - template: Arc, - }, - /// No package was specifically requested, and there are multiple - /// files in the current repository. - MultipleTemplateFiles(Vec), - /// No package was specifically requested, and there no template - /// files in the current repository. - NoTemplateFiles, - NotFound(String), -} - -impl FindPackageTemplateResult { - pub fn is_found(&self) -> bool { - matches!(self, Self::Found { .. }) - } - - /// Prints error messages and exits if no template file was found - pub fn must_be_found(self) -> (std::path::PathBuf, Arc) { - match self { - Self::Found { path, template } => return (path, template), - Self::MultipleTemplateFiles(files) => { - tracing::error!("Multiple package specs in current directory:"); - for file in files { - tracing::error!("- {}", file.into_os_string().to_string_lossy()); - } - tracing::error!(" > please specify a package name or filepath"); - } - Self::NoTemplateFiles => { - tracing::error!("No package specs found in current directory"); - tracing::error!(" > please specify a filepath"); - } - Self::NotFound(request) => { - tracing::error!("Spec file not found for '{request}', or the file does not exist"); - } - } - std::process::exit(1); - } -} - -/// Find a package template file for the requested package, if any. -/// -/// This function will use the current directory and the provided -/// package name or filename to try and discover the matching -/// yaml template file. -pub fn find_package_template(package: Option<&S>) -> Result -where - S: AsRef, -{ - use FindPackageTemplateResult::*; - - // Lazily process the glob. This closure is expected to be called at - // most once, but there are two code paths that might need to call it. - let find_packages = || { - glob::glob("*.spk.yaml") - .into_diagnostic()? - .collect::, _>>() - .into_diagnostic() - .wrap_err("Failed to discover spec files in current directory") - }; - - // This must catch and convert all the errors into the appropriate - // FindPackageTemplateResult, e.g. NotFound(error_message), so - // that find_package_recipe_from_template_or_repo() can operate - // correctly. - let package = match package { - None => { - let mut packages = find_packages()?; - return match packages.len() { - 1 => { - let path = packages.pop().unwrap(); - let template = match SpecTemplate::from_file(&path) { - Ok(t) => t, - Err(spk_schema::Error::InvalidPath(_, err)) => { - return Ok(NotFound(format!("{err}"))); - } - Err(spk_schema::Error::FileOpenError(_, err)) => { - return Ok(NotFound(format!("{err}"))); - } - Err(err) => { - return Err(err.into()); - } - }; - Ok(Found { - path, - template: Arc::new(template), - }) - } - 2.. => Ok(MultipleTemplateFiles(packages)), - _ => Ok(NoTemplateFiles), - }; - } - Some(package) => package, - }; - - match SpecTemplate::from_file(package.as_ref().as_ref()) { - Err(spk_schema::Error::InvalidPath(_, _err)) => {} - Err(spk_schema::Error::FileOpenError(_, err)) - if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) => { - return Err(err.into()); - } - Ok(res) => { - return Ok(Found { - path: package.as_ref().into(), - template: Arc::new(res), - }); - } - } - - for path in find_packages()? { - let template = match SpecTemplate::from_file(&path) { - Ok(t) => t, - Err(spk_schema::Error::InvalidPath(_, _)) => { - continue; - } - Err(spk_schema::Error::FileOpenError(_, _)) => { - continue; - } - Err(err) => { - return Err(err.into()); - } - }; - if let Some(name) = template.name() { - if name.as_str() == package.as_ref() { - return Ok(Found { - path, - template: Arc::new(template), - }); - } - } - } - - Ok(NotFound(package.as_ref().to_owned())) -} - // TODO: rename this because it is more than package recipe spec now? /// Find a package recipe either from a template file in the current /// directory, or published version of the requested package, if any. @@ -909,48 +808,38 @@ where /// will try to find the matching package/version in the repo and use /// the recipe published for that. /// -pub async fn find_package_recipe_from_template_or_repo( +pub async fn find_package_recipe_from_workspace_or_repo( package_name: Option<&S>, options: &OptionMap, + workspace: &mut spk_workspace::Workspace, repos: &[Arc], ) -> Result<(SpecFileData, std::path::PathBuf)> where S: AsRef, { - match find_package_template(package_name).wrap_err_with(|| { - format!( - "finding package template for {package_name}", - package_name = { - match &package_name { - Some(package_name) => package_name.as_ref(), - None => "something named *.spk.yaml in the current directory", - } - } - ) - })? { - FindPackageTemplateResult::Found { path, template } => { - let found = template.render(options).wrap_err_with(|| { - format!( - "{filename} was expected to contain a valid spk yaml data file", - filename = path.to_string_lossy() - ) - })?; - tracing::debug!("Rendered template from the data in {path:?}"); - Ok((found, path)) - } - FindPackageTemplateResult::MultipleTemplateFiles(files) => { + let from_workspace = match package_name { + None => workspace.default_package_template(), + Some(package_name) => workspace.find_package_template(package_name), + }; + let template = match from_workspace { + FindPackageTemplateResult::Found(template) => template, + res @ FindPackageTemplateResult::MultipleTemplateFiles(_) => { // must_be_found() will exit the program when called on MultipleTemplateFiles - FindPackageTemplateResult::MultipleTemplateFiles(files).must_be_found(); + res.must_be_found(); unreachable!() } FindPackageTemplateResult::NoTemplateFiles | FindPackageTemplateResult::NotFound(_) => { // If couldn't find a template file, maybe there's an // existing package/version that's been published - match package_name { + match package_name.map(AsRef::as_ref) { + Some(name) if std::path::Path::new(name).is_file() => { + tracing::debug!(?name, "Loading anonymous template file into workspace..."); + workspace.load_template_file(name)?.clone() + } Some(name) => { - tracing::debug!("Unable to find package file: {}", name.as_ref()); + tracing::debug!("Unable to find package file: {}", name); // there will be at least one item for any string - let name_version = name.as_ref().split('@').next().unwrap(); + let name_version = name.split('@').next().unwrap(); // If the package name can't be parsed as a valid name, // don't return the parse error. It's possible that the @@ -972,7 +861,7 @@ where ); return Ok(( SpecFileData::Recipe(recipe), - std::path::PathBuf::from(&name.as_ref()), + std::path::PathBuf::from(name), )); } @@ -984,8 +873,7 @@ where miette::bail!( help = "Check that file path, or package/version request, is correct", - "Unable to find {:?} as a file, or existing package/version recipe in any repo", - name.as_ref() + "Unable to find {name:?} as a file, or existing package/version recipe in any repo", ); } None => { @@ -996,7 +884,18 @@ where } } } - } + }; + let found = template.render(options).wrap_err_with(|| { + format!( + "{filename} was expected to contain a valid spk yaml data file", + filename = template.file_path().to_string_lossy() + ) + })?; + tracing::debug!( + "Rendered template from the data in {:?}", + template.file_path() + ); + Ok((found, template.file_path().to_owned())) } #[derive(Args, Clone)] diff --git a/crates/spk-cli/group2/src/cmd_num_variants.rs b/crates/spk-cli/group2/src/cmd_num_variants.rs index c472145899..a594ab1be7 100644 --- a/crates/spk-cli/group2/src/cmd_num_variants.rs +++ b/crates/spk-cli/group2/src/cmd_num_variants.rs @@ -16,6 +16,8 @@ pub struct NumVariants { pub repos: flags::Repositories, #[clap(flatten)] pub options: flags::Options, + #[clap(flatten)] + pub workspace: flags::Workspace, /// The local package yaml spec file or published packages/version to report on #[clap(name = "SPEC_FILE|PKG/VER")] @@ -33,10 +35,12 @@ impl Run for NumVariants { .into_iter() .map(|(_, r)| Arc::new(r)) .collect::>(); + let mut workspace = self.workspace.load_or_default()?; - let (spec_data, filename) = flags::find_package_recipe_from_template_or_repo( + let (spec_data, filename) = flags::find_package_recipe_from_workspace_or_repo( self.package.as_ref(), &options, + &mut workspace, &repos, ) .await?; diff --git a/crates/spk-cli/group4/Cargo.toml b/crates/spk-cli/group4/Cargo.toml index 0663a73f98..c79f179da3 100644 --- a/crates/spk-cli/group4/Cargo.toml +++ b/crates/spk-cli/group4/Cargo.toml @@ -28,6 +28,7 @@ spk-cli-common = { workspace = true } spk-schema = { workspace = true } spk-solve = { workspace = true } spk-storage = { workspace = true } +spk-workspace = { workspace = true } strum = { workspace = true } tokio = { workspace = true, features = ["rt"] } tracing = { workspace = true } diff --git a/crates/spk-cli/group4/src/cmd_view.rs b/crates/spk-cli/group4/src/cmd_view.rs index 76f52143b0..29e0be8481 100644 --- a/crates/spk-cli/group4/src/cmd_view.rs +++ b/crates/spk-cli/group4/src/cmd_view.rs @@ -26,7 +26,6 @@ use spk_schema::version::Version; use spk_schema::{ AnyIdent, BuildIdent, - Recipe, RequirementsList, Spec, Template, @@ -35,6 +34,7 @@ use spk_schema::{ VersionIdent, }; use spk_solve::solution::{get_spfs_layers_to_packages, LayerPackageAndComponents}; +use spk_solve::Recipe; use spk_storage; use strum::{Display, EnumString, IntoEnumIterator, VariantNames}; @@ -123,7 +123,8 @@ impl Run for View { async fn run(&mut self) -> Result { if self.variants || self.variants_with_tests { let options = self.options.get_options()?; - return self.print_variants_info(&options, self.variants_with_tests); + let workspace = self.requests.workspace.load_or_default()?; + return self.print_variants_info(&options, &workspace, self.variants_with_tests); } let package = match (&self.package, &self.filepath, &self.pkg) { @@ -224,16 +225,19 @@ impl View { fn print_variants_info( &self, options: &OptionMap, + workspace: &spk_workspace::Workspace, show_variants_with_tests: bool, ) -> Result { - let (filename, template) = flags::find_package_template(self.package.as_ref()) - .wrap_err("find package template")? - .must_be_found(); + let template = match self.package.as_ref() { + None => workspace.default_package_template(), + Some(name) => workspace.find_package_template(name), + } + .must_be_found(); let rendered_data = template.render(options)?; let recipe = rendered_data.into_recipe().wrap_err_with(|| { format!( "{filename} was expected to contain a recipe", - filename = filename.to_string_lossy() + filename = template.file_path().to_string_lossy() ) })?; diff --git a/crates/spk-workspace/Cargo.toml b/crates/spk-workspace/Cargo.toml index 23c07a017e..4cf10d0b73 100644 --- a/crates/spk-workspace/Cargo.toml +++ b/crates/spk-workspace/Cargo.toml @@ -26,6 +26,7 @@ serde_yaml = { workspace = true } spk-schema = { workspace = true } spk-solve = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true } [dev-dependencies] rstest = { workspace = true } diff --git a/crates/spk-workspace/src/builder.rs b/crates/spk-workspace/src/builder.rs index d434abb4e6..42cb3c6835 100644 --- a/crates/spk-workspace/src/builder.rs +++ b/crates/spk-workspace/src/builder.rs @@ -2,41 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // https://github.com/spkenv/spk -use std::collections::HashMap; - -use itertools::Itertools; -use spk_schema::TemplateExt; - -mod error { - pub use crate::error::LoadWorkspaceFileError; - - #[derive(thiserror::Error, miette::Diagnostic, Debug)] - pub enum FromPathError { - #[error(transparent)] - #[diagnostic(forward(0))] - LoadWorkspaceFileError(#[from] LoadWorkspaceFileError), - #[error(transparent)] - #[diagnostic(forward(0))] - FromFileError(#[from] FromFileError), - } - - #[derive(thiserror::Error, miette::Diagnostic, Debug)] - pub enum FromFileError { - #[error("Invalid glob pattern")] - PatternError(#[from] glob::PatternError), - #[error("Failed to process glob pattern")] - GlobError(#[from] glob::GlobError), - } - - #[derive(thiserror::Error, miette::Diagnostic, Debug)] - pub enum BuildError { - #[error("Failed to load spec from workspace: {file:?}")] - TemplateLoadError { - file: std::path::PathBuf, - source: spk_schema::Error, - }, - } -} +use crate::error; #[derive(Default)] pub struct WorkspaceBuilder { @@ -61,15 +27,23 @@ impl WorkspaceBuilder { /// Load all data from a workspace specification. pub fn load_from_file( - mut self, + self, file: crate::file::WorkspaceFile, ) -> Result { - let mut glob_results = file - .recipes - .iter() - .map(|pattern| glob::glob(pattern.path.as_str())) - .flatten_ok() - .flatten_ok(); + file.recipes.iter().try_fold(self, |builder, pattern| { + builder.with_glob_pattern(pattern.path.as_str()) + }) + } + + /// Add all recipe files matching a glob pattern to the workspace. + /// + /// If the provided pattern is relative, it will be relative to the + /// current working directory. + pub fn with_glob_pattern>( + mut self, + pattern: S, + ) -> Result { + let mut glob_results = glob::glob(pattern.as_ref())?; while let Some(path) = glob_results.next().transpose()? { self = self.with_recipe_file(path); } @@ -84,15 +58,10 @@ impl WorkspaceBuilder { } pub fn build(self) -> Result { - let mut templates = HashMap::<_, Vec<_>>::new(); + let mut workspace = super::Workspace::default(); for file in self.spec_files { - let template = spk_schema::SpecTemplate::from_file(&file) - .map_err(|source| error::BuildError::TemplateLoadError { file, source })?; - templates - .entry(template.name().cloned()) - .or_default() - .push(template); + workspace.load_template_file(file)?; } - Ok(crate::Workspace { templates }) + Ok(workspace) } } diff --git a/crates/spk-workspace/src/error.rs b/crates/spk-workspace/src/error.rs index 4ffeb40ed4..93e03fb3bd 100644 --- a/crates/spk-workspace/src/error.rs +++ b/crates/spk-workspace/src/error.rs @@ -4,6 +4,33 @@ use std::path::PathBuf; +#[derive(thiserror::Error, miette::Diagnostic, Debug)] +pub enum FromPathError { + #[error(transparent)] + #[diagnostic(forward(0))] + LoadWorkspaceFileError(#[from] LoadWorkspaceFileError), + #[error(transparent)] + #[diagnostic(forward(0))] + FromFileError(#[from] FromFileError), +} + +#[derive(thiserror::Error, miette::Diagnostic, Debug)] +pub enum FromFileError { + #[error("Invalid glob pattern")] + PatternError(#[from] glob::PatternError), + #[error("Failed to process glob pattern")] + GlobError(#[from] glob::GlobError), +} + +#[derive(thiserror::Error, miette::Diagnostic, Debug)] +pub enum BuildError { + #[error("Failed to load spec from workspace: {file:?}")] + TemplateLoadError { + file: std::path::PathBuf, + source: spk_schema::Error, + }, +} + #[derive(thiserror::Error, miette::Diagnostic, Debug)] pub enum LoadWorkspaceFileError { #[error( diff --git a/crates/spk-workspace/src/file.rs b/crates/spk-workspace/src/file.rs index a7d7a37b61..380d406024 100644 --- a/crates/spk-workspace/src/file.rs +++ b/crates/spk-workspace/src/file.rs @@ -58,21 +58,17 @@ impl WorkspaceFile { None => std::env::current_dir().unwrap_or_default().join(cwd), } }; - let mut candidate = cwd.clone(); - let mut last_found = None; + let mut candidate: std::path::PathBuf = cwd.clone(); loop { if candidate.join(WorkspaceFile::FILE_NAME).is_file() { - last_found = Some(candidate.clone()); + return Self::load(candidate); } if !candidate.pop() { break; } } - match last_found { - Some(path) => Self::load(path), - None => Err(LoadWorkspaceFileError::WorkspaceNotFound(cwd)), - } + Err(LoadWorkspaceFileError::WorkspaceNotFound(cwd)) } } diff --git a/crates/spk-workspace/src/lib.rs b/crates/spk-workspace/src/lib.rs index d9e1aa1376..4fc1b56041 100644 --- a/crates/spk-workspace/src/lib.rs +++ b/crates/spk-workspace/src/lib.rs @@ -8,4 +8,4 @@ mod file; mod workspace; pub use file::WorkspaceFile; -pub use workspace::Workspace; +pub use workspace::{FindPackageTemplateResult, Workspace}; diff --git a/crates/spk-workspace/src/workspace.rs b/crates/spk-workspace/src/workspace.rs index 6534561099..d40576a455 100644 --- a/crates/spk-workspace/src/workspace.rs +++ b/crates/spk-workspace/src/workspace.rs @@ -3,6 +3,12 @@ // https://github.com/spkenv/spk use std::collections::HashMap; +use std::sync::Arc; + +use spk_schema::name::PkgNameBuf; +use spk_schema::{SpecTemplate, Template, TemplateExt}; + +use crate::error; /// A collection of recipes and build targets. /// @@ -12,18 +18,149 @@ use std::collections::HashMap; /// can be used to determine the number and order of /// packages to be built in order to efficiently satisfy /// and entire set of requirements for an environment. +#[derive(Default)] pub struct Workspace { /// Spec templates available in this workspace. /// /// A workspace may contain multiple recipes for a single /// package, and templates may also not have a package name /// defined inside. - pub(crate) templates: - HashMap, Vec>, + pub(crate) templates: HashMap, Vec>>, } impl Workspace { pub fn builder() -> crate::builder::WorkspaceBuilder { crate::builder::WorkspaceBuilder::default() } + + pub fn iter(&self) -> impl Iterator, &Arc)> { + self.templates + .iter() + .flat_map(|(key, entries)| entries.iter().map(move |e| (key, e))) + } + + pub fn default_package_template(&self) -> FindPackageTemplateResult { + let mut iter = self.iter(); + // This must catch and convert all the errors into the appropriate + // FindPackageTemplateResult, e.g. NotFound(error_message), so + // that find_package_recipe_from_template_or_repo() can operate + // correctly. + let Some((_name, template)) = iter.next() else { + return FindPackageTemplateResult::NoTemplateFiles; + }; + + if iter.next().is_some() { + let files = self + .templates + .values() + .flat_map(|templates| templates.iter().map(|t| t.file_path().to_owned())) + .collect(); + return FindPackageTemplateResult::MultipleTemplateFiles(files); + }; + + FindPackageTemplateResult::Found(Arc::clone(template)) + } + + /// Find a package template file for the requested package, if any. + /// + /// This function will use the current directory and the provided + /// package name or filename to try and discover the matching + /// yaml template file. + pub fn find_package_template(&self, package: &S) -> FindPackageTemplateResult + where + S: AsRef, + { + let package = package.as_ref(); + + if let Ok(name) = spk_schema::name::PkgNameBuf::try_from(package) { + match self.templates.get(&Some(name)) { + Some(templates) if templates.len() == 1 => { + return FindPackageTemplateResult::Found(Arc::clone(&templates[0])); + } + Some(templates) => { + return FindPackageTemplateResult::MultipleTemplateFiles( + templates.iter().map(|t| t.file_path().to_owned()).collect(), + ); + } + None => {} + } + } + + for template in self.templates.values().flatten() { + if template.file_path() == std::path::Path::new(package) { + return FindPackageTemplateResult::Found(Arc::clone(template)); + } + } + + FindPackageTemplateResult::NotFound(package.to_owned()) + } + + /// Load an additional template into this workspace from an arbitrary path on disk. + /// + /// No checks are done to ensure that this template has not already been loaded + /// or that it actually appears in/logically belongs in this workspace. + pub fn load_template_file>( + &mut self, + path: P, + ) -> Result<&Arc, error::BuildError> { + let template = spk_schema::SpecTemplate::from_file(path.as_ref()) + .map(Arc::new) + .map_err(|source| error::BuildError::TemplateLoadError { + file: path.as_ref().to_owned(), + source, + })?; + tracing::trace!( + "Load template into workspace: {:?} [{}]", + path.as_ref(), + template.name().map(|n| n.as_str()).unwrap_or("") + ); + let by_name = self.templates.entry(template.name().cloned()).or_default(); + by_name.push(template); + Ok(&by_name.last().expect("just pushed something")) + } +} + +/// The result of the [`Workspace::find_package_template`] function. +// We are okay with the large variant here because it's specifically +// used as the positive result of the function, with the others simply +// denoting unique error cases. +#[allow(clippy::large_enum_variant)] +pub enum FindPackageTemplateResult { + /// A non-ambiguous package template file was found + Found(Arc), + /// No package was specifically requested, and there are multiple + /// files in the current repository. + MultipleTemplateFiles(Vec), + /// No package was specifically requested, and there no template + /// files in the current repository. + NoTemplateFiles, + NotFound(String), +} + +impl FindPackageTemplateResult { + pub fn is_found(&self) -> bool { + matches!(self, Self::Found { .. }) + } + + /// Prints error messages and exits if no template file was found + pub fn must_be_found(self) -> Arc { + match self { + Self::Found(template) => return template, + Self::MultipleTemplateFiles(files) => { + tracing::error!("Multiple package specs in current workspace:"); + for file in files { + tracing::error!("- {}", file.into_os_string().to_string_lossy()); + } + tracing::error!(" > please specify a package name or filepath"); + } + Self::NoTemplateFiles => { + tracing::error!("No package specs found in current workspace"); + tracing::error!(" > please specify a filepath"); + } + Self::NotFound(request) => { + tracing::error!("Spec file not found for '{request}', or the file does not exist"); + } + } + std::process::exit(1); + } } diff --git a/workspace.spk.yml b/workspace.spk.yaml similarity index 100% rename from workspace.spk.yml rename to workspace.spk.yaml