From 76c6159340ce543d30c5ae4f1cae6a759adf7fbf Mon Sep 17 00:00:00 2001 From: Ryan Bottriell Date: Fri, 13 Dec 2024 08:57:28 -0800 Subject: [PATCH 01/11] Introduce a workspace type with recipe globs Signed-off-by: Ryan Bottriell --- Cargo.lock | 11 ++++++ Cargo.toml | 1 + crates/spk-workspace/Cargo.toml | 25 +++++++++++++ crates/spk-workspace/src/lib.rs | 1 + crates/spk-workspace/src/spec.rs | 52 +++++++++++++++++++++++++++ crates/spk-workspace/src/spec_test.rs | 18 ++++++++++ 6 files changed, 108 insertions(+) create mode 100644 crates/spk-workspace/Cargo.toml create mode 100644 crates/spk-workspace/src/lib.rs create mode 100644 crates/spk-workspace/src/spec.rs create mode 100644 crates/spk-workspace/src/spec_test.rs diff --git a/Cargo.lock b/Cargo.lock index 3ad68678b..1a016c49f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4651,6 +4651,17 @@ dependencies = [ "variantly", ] +[[package]] +name = "spk-workspace" +version = "0.42.0" +dependencies = [ + "glob", + "rstest 0.18.2", + "serde", + "serde_json", + "spk-solve", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index ad01238ae..fca9b084e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "crates/spk-solve", "crates/spk-solve/crates/*", "crates/spk-storage", + "crates/spk-workspace", "crates/spk", ] resolver = "2" diff --git a/crates/spk-workspace/Cargo.toml b/crates/spk-workspace/Cargo.toml new file mode 100644 index 000000000..052877405 --- /dev/null +++ b/crates/spk-workspace/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "spk-workspace" +authors = { workspace = true } +edition = { workspace = true } +version = { workspace = true } +license-file = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +readme = { workspace = true } +description = { workspace = true } + +[lints] +workspace = true + +[features] +sentry = ["spk-solve/sentry"] + +[dependencies] +serde = { workspace = true, features = ["derive"] } +glob = { workspace = true } +spk-solve = { workspace = true } + +[dev-dependencies] +rstest = { workspace = true } +serde_json = { workspace = true } diff --git a/crates/spk-workspace/src/lib.rs b/crates/spk-workspace/src/lib.rs new file mode 100644 index 000000000..9e3d3cbc2 --- /dev/null +++ b/crates/spk-workspace/src/lib.rs @@ -0,0 +1 @@ +mod spec; diff --git a/crates/spk-workspace/src/spec.rs b/crates/spk-workspace/src/spec.rs new file mode 100644 index 000000000..70272223f --- /dev/null +++ b/crates/spk-workspace/src/spec.rs @@ -0,0 +1,52 @@ +use serde::{Deserialize, Serialize}; + +#[cfg(test)] +#[path = "spec_test.rs"] +mod spec_test; + +#[derive(Debug, Clone, Hash, PartialEq, Eq, Ord, PartialOrd, Deserialize, Serialize)] +pub struct Workspace { + #[serde(default, skip_serializing_if = "Vec::is_empty", with = "glob_from_str")] + pub recipes: Vec, +} + +mod glob_from_str { + use serde::{Deserializer, Serialize, Serializer}; + + pub fn serialize(patterns: &Vec, serializer: S) -> Result + where + S: Serializer, + { + let patterns: Vec<_> = patterns.iter().map(|p| p.as_str()).collect(); + patterns.serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + /// Visits a serialized string, decoding it as a digest + struct PatternVisitor; + + impl<'de> serde::de::Visitor<'de> for PatternVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a glob pattern") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut patterns = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(pattern) = seq.next_element()? { + let pattern = glob::Pattern::new(pattern).map_err(serde::de::Error::custom)?; + patterns.push(pattern); + } + Ok(patterns) + } + } + deserializer.deserialize_seq(PatternVisitor) + } +} diff --git a/crates/spk-workspace/src/spec_test.rs b/crates/spk-workspace/src/spec_test.rs new file mode 100644 index 000000000..ff1ee8f77 --- /dev/null +++ b/crates/spk-workspace/src/spec_test.rs @@ -0,0 +1,18 @@ +use rstest::rstest; + +use super::Workspace; + +#[rstest] +fn test_workspace_roundtrip() { + let workspace = Workspace { + recipes: vec![ + glob::Pattern::new("packages/*/*.spk.yml").unwrap(), + glob::Pattern::new("platforms/*/*.spk.yml").unwrap(), + ], + }; + + let serialized = serde_json::to_string(&workspace).unwrap(); + let deserialized: Workspace = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(workspace, deserialized); +} From 50010a2e3068160e8d5d255148562316a47b74ed Mon Sep 17 00:00:00 2001 From: Ryan Bottriell Date: Fri, 13 Dec 2024 09:37:41 -0800 Subject: [PATCH 02/11] Add logic to discover workspaces in parent dirs Signed-off-by: Ryan Bottriell --- Cargo.lock | 6 +++ Cargo.toml | 1 + crates/spk-schema/Cargo.toml | 2 +- .../spk-schema/crates/foundation/Cargo.toml | 2 +- crates/spk-schema/crates/ident/Cargo.toml | 2 +- crates/spk-storage/Cargo.toml | 2 +- crates/spk-workspace/Cargo.toml | 6 +++ crates/spk-workspace/src/error.rs | 16 ++++++ crates/spk-workspace/src/lib.rs | 3 ++ crates/spk-workspace/src/spec.rs | 52 ++++++++++++++++++ crates/spk-workspace/src/spec_test.rs | 53 ++++++++++++++++++- workspace.spk.yml | 4 ++ 12 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 crates/spk-workspace/src/error.rs create mode 100644 workspace.spk.yml diff --git a/Cargo.lock b/Cargo.lock index 1a016c49f..b0509860d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4655,11 +4655,17 @@ dependencies = [ name = "spk-workspace" version = "0.42.0" dependencies = [ + "format_serde_error", "glob", + "miette", "rstest 0.18.2", "serde", "serde_json", + "serde_yaml 0.9.27", + "spk-schema-foundation", "spk-solve", + "tempfile", + "thiserror", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fca9b084e..1ca8b2365 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ dunce = "1.0.4" dyn-clone = "1.0" enum_dispatch = "0.3.13" flatbuffers = "23.5.26" +format_serde_error = { version = "0.3", default-features = false } fuser = "0.14.0" futures = "0.3.28" futures-core = "0.3.28" diff --git a/crates/spk-schema/Cargo.toml b/crates/spk-schema/Cargo.toml index b83acc60d..17db94761 100644 --- a/crates/spk-schema/Cargo.toml +++ b/crates/spk-schema/Cargo.toml @@ -20,7 +20,7 @@ config = { workspace = true } data-encoding = "2.3" dunce = { workspace = true } enum_dispatch = { workspace = true } -format_serde_error = { version = "0.3", default-features = false, features = [ +format_serde_error = { workspace = true, default-features = false, features = [ "serde_yaml", "colored", ] } diff --git a/crates/spk-schema/crates/foundation/Cargo.toml b/crates/spk-schema/crates/foundation/Cargo.toml index a1c785bb5..6524bb0d8 100644 --- a/crates/spk-schema/crates/foundation/Cargo.toml +++ b/crates/spk-schema/crates/foundation/Cargo.toml @@ -24,7 +24,7 @@ async-trait = { workspace = true } colored = { workspace = true } data-encoding = "2.3" enum_dispatch = { workspace = true } -format_serde_error = { version = "0.3", default-features = false, features = [ +format_serde_error = { workspace = true, default-features = false, features = [ "serde_yaml", "colored", ] } diff --git a/crates/spk-schema/crates/ident/Cargo.toml b/crates/spk-schema/crates/ident/Cargo.toml index 5ceafa9a6..44e72b5e4 100644 --- a/crates/spk-schema/crates/ident/Cargo.toml +++ b/crates/spk-schema/crates/ident/Cargo.toml @@ -17,7 +17,7 @@ migration-to-components = ["spk-schema-foundation/migration-to-components"] [dependencies] colored = { workspace = true } -format_serde_error = { version = "0.3", default-features = false, features = [ +format_serde_error = { workspace = true, default-features = false, features = [ "serde_yaml", "colored", ] } diff --git a/crates/spk-storage/Cargo.toml b/crates/spk-storage/Cargo.toml index e34b78ddc..04803882b 100644 --- a/crates/spk-storage/Cargo.toml +++ b/crates/spk-storage/Cargo.toml @@ -23,7 +23,7 @@ colored = { workspace = true } dashmap = "5.4.0" data-encoding = "2.3.0" enum_dispatch = { workspace = true } -format_serde_error = { version = "0.3", default-features = false, features = [ +format_serde_error = { workspace = true, default-features = false, features = [ "serde_yaml", "colored", ] } diff --git a/crates/spk-workspace/Cargo.toml b/crates/spk-workspace/Cargo.toml index 052877405..ad5f125bc 100644 --- a/crates/spk-workspace/Cargo.toml +++ b/crates/spk-workspace/Cargo.toml @@ -19,7 +19,13 @@ sentry = ["spk-solve/sentry"] serde = { workspace = true, features = ["derive"] } glob = { workspace = true } spk-solve = { workspace = true } +thiserror = { workspace = true } +miette = { workspace = true } +serde_yaml = { workspace = true } +spk-schema-foundation = { workspace = true } +format_serde_error = { workspace = true } [dev-dependencies] rstest = { workspace = true } serde_json = { workspace = true } +tempfile = { workspace = true } diff --git a/crates/spk-workspace/src/error.rs b/crates/spk-workspace/src/error.rs new file mode 100644 index 000000000..80aa8d156 --- /dev/null +++ b/crates/spk-workspace/src/error.rs @@ -0,0 +1,16 @@ +use std::path::PathBuf; + +#[derive(thiserror::Error, miette::Diagnostic, Debug)] +pub enum LoadWorkspaceError { + #[error( + "workspace not found, no {} in {0:?} or any parent", + crate::Workspace::FILE_NAME + )] + WorkspaceNotFound(PathBuf), + #[error("'{}' not found in {0:?}", crate::Workspace::FILE_NAME)] + NoWorkspaceFile(PathBuf), + #[error(transparent)] + ReadFailed(std::io::Error), + #[error(transparent)] + InvalidYaml(format_serde_error::SerdeError), +} diff --git a/crates/spk-workspace/src/lib.rs b/crates/spk-workspace/src/lib.rs index 9e3d3cbc2..30e6a8e47 100644 --- a/crates/spk-workspace/src/lib.rs +++ b/crates/spk-workspace/src/lib.rs @@ -1 +1,4 @@ +pub mod error; mod spec; + +pub use spec::Workspace; diff --git a/crates/spk-workspace/src/spec.rs b/crates/spk-workspace/src/spec.rs index 70272223f..1aa4f9b0d 100644 --- a/crates/spk-workspace/src/spec.rs +++ b/crates/spk-workspace/src/spec.rs @@ -1,4 +1,9 @@ +use std::path::Path; + use serde::{Deserialize, Serialize}; +use spk_schema_foundation::FromYaml; + +use crate::error::LoadWorkspaceError; #[cfg(test)] #[path = "spec_test.rs"] @@ -10,6 +15,53 @@ pub struct Workspace { pub recipes: Vec, } +impl Workspace { + pub const FILE_NAME: &str = "workspace.spk.yaml"; + + /// Load a workspace from its root directory on disk + pub fn load>(root: P) -> Result { + let root = root + .as_ref() + .canonicalize() + .map_err(|_| LoadWorkspaceError::NoWorkspaceFile(root.as_ref().into()))?; + + let workspace_file = std::fs::read_to_string(root.join(Workspace::FILE_NAME)) + .map_err(LoadWorkspaceError::ReadFailed)?; + Workspace::from_yaml(workspace_file).map_err(LoadWorkspaceError::InvalidYaml) + } + + /// Load the workspace for a given dir, looking at parent directories + /// as necessary to find the workspace root + pub fn discover>(cwd: P) -> Result { + let cwd = if cwd.as_ref().is_absolute() { + cwd.as_ref().to_owned() + } else { + // prefer PWD if available, since it may be more representative of + // how the user arrived at the current dir and avoids dereferencing + // symlinks that could otherwise make error messages harder to understand + match std::env::var("PWD").ok() { + Some(pwd) => Path::new(&pwd).join(cwd), + None => std::env::current_dir().unwrap_or_default().join(cwd), + } + }; + let mut candidate = cwd.clone(); + let mut last_found = None; + + loop { + if candidate.join(Workspace::FILE_NAME).is_file() { + last_found = Some(candidate.clone()); + } + if !candidate.pop() { + break; + } + } + match last_found { + Some(path) => Self::load(path), + None => Err(LoadWorkspaceError::WorkspaceNotFound(cwd)), + } + } +} + mod glob_from_str { use serde::{Deserializer, Serialize, Serializer}; diff --git a/crates/spk-workspace/src/spec_test.rs b/crates/spk-workspace/src/spec_test.rs index ff1ee8f77..7a075ab14 100644 --- a/crates/spk-workspace/src/spec_test.rs +++ b/crates/spk-workspace/src/spec_test.rs @@ -1,7 +1,20 @@ -use rstest::rstest; +use rstest::{fixture, rstest}; use super::Workspace; +#[fixture] +pub fn tmpdir() -> tempfile::TempDir { + tempfile::Builder::new() + .prefix("spk-test-") + .tempdir() + .expect("create a temp directory for test files") +} + +const EMPTY_WORKSPACE: &str = r#" +api: v0/workspace +recipes: [] +"#; + #[rstest] fn test_workspace_roundtrip() { let workspace = Workspace { @@ -16,3 +29,41 @@ fn test_workspace_roundtrip() { assert_eq!(workspace, deserialized); } + +#[rstest] +fn test_empty_workspace_loading(tmpdir: tempfile::TempDir) { + let root = tmpdir.path(); + std::fs::write(root.join(Workspace::FILE_NAME), EMPTY_WORKSPACE).unwrap(); + let _workspace = Workspace::load(root).expect("failed to load empty workspace"); +} + +#[rstest] +fn test_must_have_file(tmpdir: tempfile::TempDir) { + let root = tmpdir.path(); + Workspace::load(root).expect_err("workspace should fail to load for empty dir"); +} + +#[rstest] +#[case("", "")] +#[case("my-workspace", "my-workspace/src/packages")] +#[should_panic] +#[case("my-workspace", "other-dir")] +#[case("", "")] +#[case("my-workspace", "my-workspace/src/packages")] +#[should_panic] +#[case("my-workspace", "other-dir/src/packages")] +fn test_workspace_discovery( + tmpdir: tempfile::TempDir, + #[case] workspace_root: &str, + #[case] discovery_start: &str, +) { + let dir = tmpdir.path(); + let root = dir.join(workspace_root); + let cwd = dir.join(discovery_start); + + std::fs::create_dir_all(&cwd).unwrap(); + std::fs::create_dir_all(&root).unwrap(); + std::fs::write(root.join(Workspace::FILE_NAME), EMPTY_WORKSPACE).unwrap(); + + Workspace::discover(&cwd).expect("failed to load workspace"); +} diff --git a/workspace.spk.yml b/workspace.spk.yml new file mode 100644 index 000000000..cc340e414 --- /dev/null +++ b/workspace.spk.yml @@ -0,0 +1,4 @@ +api: v0/workspace + +recipes: + - packages/**.spk.yml From f775b5cd8d25b3a7038a47ca319c3ef5e7134fbf Mon Sep 17 00:00:00 2001 From: Ryan Bottriell Date: Fri, 13 Dec 2024 11:14:01 -0800 Subject: [PATCH 03/11] Claify the naming and purpose of the workspace file Signed-off-by: Ryan Bottriell --- crates/spk-workspace/src/error.rs | 6 ++-- crates/spk-workspace/src/{spec.rs => file.rs} | 32 +++++++++++-------- .../src/{spec_test.rs => file_test.rs} | 16 +++++----- crates/spk-workspace/src/lib.rs | 4 +-- 4 files changed, 32 insertions(+), 26 deletions(-) rename crates/spk-workspace/src/{spec.rs => file.rs} (78%) rename crates/spk-workspace/src/{spec_test.rs => file_test.rs} (71%) diff --git a/crates/spk-workspace/src/error.rs b/crates/spk-workspace/src/error.rs index 80aa8d156..8caea7ac0 100644 --- a/crates/spk-workspace/src/error.rs +++ b/crates/spk-workspace/src/error.rs @@ -1,13 +1,13 @@ use std::path::PathBuf; #[derive(thiserror::Error, miette::Diagnostic, Debug)] -pub enum LoadWorkspaceError { +pub enum LoadWorkspaceFileError { #[error( "workspace not found, no {} in {0:?} or any parent", - crate::Workspace::FILE_NAME + crate::WorkspaceFile::FILE_NAME )] WorkspaceNotFound(PathBuf), - #[error("'{}' not found in {0:?}", crate::Workspace::FILE_NAME)] + #[error("'{}' not found in {0:?}", crate::WorkspaceFile::FILE_NAME)] NoWorkspaceFile(PathBuf), #[error(transparent)] ReadFailed(std::io::Error), diff --git a/crates/spk-workspace/src/spec.rs b/crates/spk-workspace/src/file.rs similarity index 78% rename from crates/spk-workspace/src/spec.rs rename to crates/spk-workspace/src/file.rs index 1aa4f9b0d..1507b43c2 100644 --- a/crates/spk-workspace/src/spec.rs +++ b/crates/spk-workspace/src/file.rs @@ -3,36 +3,42 @@ use std::path::Path; use serde::{Deserialize, Serialize}; use spk_schema_foundation::FromYaml; -use crate::error::LoadWorkspaceError; +use crate::error::LoadWorkspaceFileError; #[cfg(test)] -#[path = "spec_test.rs"] -mod spec_test; +#[path = "file_test.rs"] +mod file_test; +/// Describes a workspace configuration. +/// +/// Contains information about the layout of the workspace, +/// and where to find data, usually loaded from a file on disk. +/// It must still be fully validated and loaded into a +/// [`super::Workspace`] to be operated on. #[derive(Debug, Clone, Hash, PartialEq, Eq, Ord, PartialOrd, Deserialize, Serialize)] -pub struct Workspace { +pub struct WorkspaceFile { #[serde(default, skip_serializing_if = "Vec::is_empty", with = "glob_from_str")] pub recipes: Vec, } -impl Workspace { +impl WorkspaceFile { pub const FILE_NAME: &str = "workspace.spk.yaml"; /// Load a workspace from its root directory on disk - pub fn load>(root: P) -> Result { + pub fn load>(root: P) -> Result { let root = root .as_ref() .canonicalize() - .map_err(|_| LoadWorkspaceError::NoWorkspaceFile(root.as_ref().into()))?; + .map_err(|_| LoadWorkspaceFileError::NoWorkspaceFile(root.as_ref().into()))?; - let workspace_file = std::fs::read_to_string(root.join(Workspace::FILE_NAME)) - .map_err(LoadWorkspaceError::ReadFailed)?; - Workspace::from_yaml(workspace_file).map_err(LoadWorkspaceError::InvalidYaml) + let workspace_file = std::fs::read_to_string(root.join(WorkspaceFile::FILE_NAME)) + .map_err(LoadWorkspaceFileError::ReadFailed)?; + WorkspaceFile::from_yaml(workspace_file).map_err(LoadWorkspaceFileError::InvalidYaml) } /// Load the workspace for a given dir, looking at parent directories /// as necessary to find the workspace root - pub fn discover>(cwd: P) -> Result { + pub fn discover>(cwd: P) -> Result { let cwd = if cwd.as_ref().is_absolute() { cwd.as_ref().to_owned() } else { @@ -48,7 +54,7 @@ impl Workspace { let mut last_found = None; loop { - if candidate.join(Workspace::FILE_NAME).is_file() { + if candidate.join(WorkspaceFile::FILE_NAME).is_file() { last_found = Some(candidate.clone()); } if !candidate.pop() { @@ -57,7 +63,7 @@ impl Workspace { } match last_found { Some(path) => Self::load(path), - None => Err(LoadWorkspaceError::WorkspaceNotFound(cwd)), + None => Err(LoadWorkspaceFileError::WorkspaceNotFound(cwd)), } } } diff --git a/crates/spk-workspace/src/spec_test.rs b/crates/spk-workspace/src/file_test.rs similarity index 71% rename from crates/spk-workspace/src/spec_test.rs rename to crates/spk-workspace/src/file_test.rs index 7a075ab14..282433099 100644 --- a/crates/spk-workspace/src/spec_test.rs +++ b/crates/spk-workspace/src/file_test.rs @@ -1,6 +1,6 @@ use rstest::{fixture, rstest}; -use super::Workspace; +use super::WorkspaceFile; #[fixture] pub fn tmpdir() -> tempfile::TempDir { @@ -17,7 +17,7 @@ recipes: [] #[rstest] fn test_workspace_roundtrip() { - let workspace = Workspace { + let workspace = WorkspaceFile { recipes: vec![ glob::Pattern::new("packages/*/*.spk.yml").unwrap(), glob::Pattern::new("platforms/*/*.spk.yml").unwrap(), @@ -25,7 +25,7 @@ fn test_workspace_roundtrip() { }; let serialized = serde_json::to_string(&workspace).unwrap(); - let deserialized: Workspace = serde_json::from_str(&serialized).unwrap(); + let deserialized: WorkspaceFile = serde_json::from_str(&serialized).unwrap(); assert_eq!(workspace, deserialized); } @@ -33,14 +33,14 @@ fn test_workspace_roundtrip() { #[rstest] fn test_empty_workspace_loading(tmpdir: tempfile::TempDir) { let root = tmpdir.path(); - std::fs::write(root.join(Workspace::FILE_NAME), EMPTY_WORKSPACE).unwrap(); - let _workspace = Workspace::load(root).expect("failed to load empty workspace"); + std::fs::write(root.join(WorkspaceFile::FILE_NAME), EMPTY_WORKSPACE).unwrap(); + let _workspace = WorkspaceFile::load(root).expect("failed to load empty workspace"); } #[rstest] fn test_must_have_file(tmpdir: tempfile::TempDir) { let root = tmpdir.path(); - Workspace::load(root).expect_err("workspace should fail to load for empty dir"); + WorkspaceFile::load(root).expect_err("workspace should fail to load for empty dir"); } #[rstest] @@ -63,7 +63,7 @@ fn test_workspace_discovery( std::fs::create_dir_all(&cwd).unwrap(); std::fs::create_dir_all(&root).unwrap(); - std::fs::write(root.join(Workspace::FILE_NAME), EMPTY_WORKSPACE).unwrap(); + std::fs::write(root.join(WorkspaceFile::FILE_NAME), EMPTY_WORKSPACE).unwrap(); - Workspace::discover(&cwd).expect("failed to load workspace"); + WorkspaceFile::discover(&cwd).expect("failed to load workspace"); } diff --git a/crates/spk-workspace/src/lib.rs b/crates/spk-workspace/src/lib.rs index 30e6a8e47..0f5d2f62e 100644 --- a/crates/spk-workspace/src/lib.rs +++ b/crates/spk-workspace/src/lib.rs @@ -1,4 +1,4 @@ pub mod error; -mod spec; +mod file; -pub use spec::Workspace; +pub use file::WorkspaceFile; From f9fb300562b2b818eda0ef21707a52657bc8fc04 Mon Sep 17 00:00:00 2001 From: Ryan Bottriell Date: Fri, 13 Dec 2024 11:16:09 -0800 Subject: [PATCH 04/11] Add missing copyright notices Signed-off-by: Ryan Bottriell --- crates/spk-workspace/src/error.rs | 4 ++++ crates/spk-workspace/src/file.rs | 4 ++++ crates/spk-workspace/src/file_test.rs | 4 ++++ crates/spk-workspace/src/lib.rs | 5 +++++ 4 files changed, 17 insertions(+) diff --git a/crates/spk-workspace/src/error.rs b/crates/spk-workspace/src/error.rs index 8caea7ac0..4ffeb40ed 100644 --- a/crates/spk-workspace/src/error.rs +++ b/crates/spk-workspace/src/error.rs @@ -1,3 +1,7 @@ +// Copyright (c) Contributors to the SPK project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/spkenv/spk + use std::path::PathBuf; #[derive(thiserror::Error, miette::Diagnostic, Debug)] diff --git a/crates/spk-workspace/src/file.rs b/crates/spk-workspace/src/file.rs index 1507b43c2..43fa98449 100644 --- a/crates/spk-workspace/src/file.rs +++ b/crates/spk-workspace/src/file.rs @@ -1,3 +1,7 @@ +// Copyright (c) Contributors to the SPK project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/spkenv/spk + use std::path::Path; use serde::{Deserialize, Serialize}; diff --git a/crates/spk-workspace/src/file_test.rs b/crates/spk-workspace/src/file_test.rs index 282433099..8f6b4074a 100644 --- a/crates/spk-workspace/src/file_test.rs +++ b/crates/spk-workspace/src/file_test.rs @@ -1,3 +1,7 @@ +// Copyright (c) Contributors to the SPK project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/spkenv/spk + use rstest::{fixture, rstest}; use super::WorkspaceFile; diff --git a/crates/spk-workspace/src/lib.rs b/crates/spk-workspace/src/lib.rs index 0f5d2f62e..581bc5288 100644 --- a/crates/spk-workspace/src/lib.rs +++ b/crates/spk-workspace/src/lib.rs @@ -1,4 +1,9 @@ +// Copyright (c) Contributors to the SPK project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/spkenv/spk + pub mod error; mod file; +mod workspace; pub use file::WorkspaceFile; From fb8710c47b6a973c69b9ee04186394ffe7644135 Mon Sep 17 00:00:00 2001 From: Ryan Bottriell Date: Fri, 13 Dec 2024 12:18:12 -0800 Subject: [PATCH 05/11] Add logic to load spec templates in a workspace Signed-off-by: Ryan Bottriell --- Cargo.lock | 3 +- crates/spk-workspace/Cargo.toml | 3 +- crates/spk-workspace/src/builder.rs | 98 +++++++++++++++++++++++++++ crates/spk-workspace/src/file.rs | 2 +- crates/spk-workspace/src/lib.rs | 2 + crates/spk-workspace/src/workspace.rs | 29 ++++++++ 6 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 crates/spk-workspace/src/builder.rs create mode 100644 crates/spk-workspace/src/workspace.rs diff --git a/Cargo.lock b/Cargo.lock index b0509860d..47d4dd78a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4657,12 +4657,13 @@ version = "0.42.0" dependencies = [ "format_serde_error", "glob", + "itertools 0.12.0", "miette", "rstest 0.18.2", "serde", "serde_json", "serde_yaml 0.9.27", - "spk-schema-foundation", + "spk-schema", "spk-solve", "tempfile", "thiserror", diff --git a/crates/spk-workspace/Cargo.toml b/crates/spk-workspace/Cargo.toml index ad5f125bc..dc3ebb7df 100644 --- a/crates/spk-workspace/Cargo.toml +++ b/crates/spk-workspace/Cargo.toml @@ -18,11 +18,12 @@ sentry = ["spk-solve/sentry"] [dependencies] serde = { workspace = true, features = ["derive"] } glob = { workspace = true } +itertools = { workspace = true } spk-solve = { workspace = true } thiserror = { workspace = true } miette = { workspace = true } serde_yaml = { workspace = true } -spk-schema-foundation = { workspace = true } +spk-schema = { workspace = true } format_serde_error = { workspace = true } [dev-dependencies] diff --git a/crates/spk-workspace/src/builder.rs b/crates/spk-workspace/src/builder.rs new file mode 100644 index 000000000..474dd4421 --- /dev/null +++ b/crates/spk-workspace/src/builder.rs @@ -0,0 +1,98 @@ +// Copyright (c) Contributors to the SPK project. +// 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, + }, + } +} + +#[derive(Default)] +pub struct WorkspaceBuilder { + spec_files: Vec, +} + +impl WorkspaceBuilder { + /// Load all data from a workspace file discovered using the current directory. + pub fn load_from_current_dir(self) -> Result { + self.load_from_dir(".") + } + + /// Load all data from a workspace file in the given directory. + pub fn load_from_dir( + self, + dir: impl AsRef, + ) -> Result { + let file = crate::file::WorkspaceFile::discover(dir)?; + self.load_from_file(file) + .map_err(error::FromPathError::from) + } + + /// Load all data from a workspace specification. + pub fn load_from_file( + mut self, + file: crate::file::WorkspaceFile, + ) -> Result { + let mut glob_results = file + .recipes + .iter() + .map(|pattern| glob::glob(pattern.as_str())) + .flatten_ok() + .flatten_ok(); + while let Some(path) = glob_results.next().transpose()? { + self = self.with_recipe_file(path); + } + + Ok(self) + } + + /// Add a recipe file to the workspace. + pub fn with_recipe_file(mut self, path: impl Into) -> Self { + self.spec_files.push(path.into()); + self + } + + pub fn build(self) -> Result { + let mut templates = HashMap::<_, Vec<_>>::new(); + 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); + } + Ok(crate::Workspace { templates }) + } +} diff --git a/crates/spk-workspace/src/file.rs b/crates/spk-workspace/src/file.rs index 43fa98449..c74b1472c 100644 --- a/crates/spk-workspace/src/file.rs +++ b/crates/spk-workspace/src/file.rs @@ -5,7 +5,7 @@ use std::path::Path; use serde::{Deserialize, Serialize}; -use spk_schema_foundation::FromYaml; +use spk_schema::foundation::FromYaml; use crate::error::LoadWorkspaceFileError; diff --git a/crates/spk-workspace/src/lib.rs b/crates/spk-workspace/src/lib.rs index 581bc5288..d9e1aa137 100644 --- a/crates/spk-workspace/src/lib.rs +++ b/crates/spk-workspace/src/lib.rs @@ -2,8 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 // https://github.com/spkenv/spk +pub mod builder; pub mod error; mod file; mod workspace; pub use file::WorkspaceFile; +pub use workspace::Workspace; diff --git a/crates/spk-workspace/src/workspace.rs b/crates/spk-workspace/src/workspace.rs new file mode 100644 index 000000000..653456109 --- /dev/null +++ b/crates/spk-workspace/src/workspace.rs @@ -0,0 +1,29 @@ +// Copyright (c) Contributors to the SPK project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/spkenv/spk + +use std::collections::HashMap; + +/// A collection of recipes and build targets. +/// +/// Workspaces are used to define and build many recipes +/// together, helping to produce complete environments +/// with shared compatibility requirements. Workspaces +/// 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. +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>, +} + +impl Workspace { + pub fn builder() -> crate::builder::WorkspaceBuilder { + crate::builder::WorkspaceBuilder::default() + } +} From c46d0f3ee247d0764be2985700a91a2aa88cac8c Mon Sep 17 00:00:00 2001 From: Ryan Bottriell Date: Fri, 13 Dec 2024 15:17:51 -0800 Subject: [PATCH 06/11] Support string or mapping in recipes list Signed-off-by: Ryan Bottriell --- crates/spk-workspace/src/builder.rs | 2 +- crates/spk-workspace/src/file.rs | 61 +++++++++++++++------------ crates/spk-workspace/src/file_test.rs | 28 ++++++------ 3 files changed, 50 insertions(+), 41 deletions(-) diff --git a/crates/spk-workspace/src/builder.rs b/crates/spk-workspace/src/builder.rs index 474dd4421..d434abb4e 100644 --- a/crates/spk-workspace/src/builder.rs +++ b/crates/spk-workspace/src/builder.rs @@ -67,7 +67,7 @@ impl WorkspaceBuilder { let mut glob_results = file .recipes .iter() - .map(|pattern| glob::glob(pattern.as_str())) + .map(|pattern| glob::glob(pattern.path.as_str())) .flatten_ok() .flatten_ok(); while let Some(path) = glob_results.next().transpose()? { diff --git a/crates/spk-workspace/src/file.rs b/crates/spk-workspace/src/file.rs index c74b1472c..9b14f67c7 100644 --- a/crates/spk-workspace/src/file.rs +++ b/crates/spk-workspace/src/file.rs @@ -4,7 +4,7 @@ use std::path::Path; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use spk_schema::foundation::FromYaml; use crate::error::LoadWorkspaceFileError; @@ -19,10 +19,10 @@ mod file_test; /// and where to find data, usually loaded from a file on disk. /// It must still be fully validated and loaded into a /// [`super::Workspace`] to be operated on. -#[derive(Debug, Clone, Hash, PartialEq, Eq, Ord, PartialOrd, Deserialize, Serialize)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Ord, PartialOrd, Deserialize)] pub struct WorkspaceFile { - #[serde(default, skip_serializing_if = "Vec::is_empty", with = "glob_from_str")] - pub recipes: Vec, + #[serde(default)] + pub recipes: Vec, } impl WorkspaceFile { @@ -72,43 +72,48 @@ impl WorkspaceFile { } } -mod glob_from_str { - use serde::{Deserializer, Serialize, Serializer}; - - pub fn serialize(patterns: &Vec, serializer: S) -> Result - where - S: Serializer, - { - let patterns: Vec<_> = patterns.iter().map(|p| p.as_str()).collect(); - patterns.serialize(serializer) - } +#[derive(Debug, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)] +pub struct RecipesItem { + pub path: glob::Pattern, +} - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> +impl<'de> serde::de::Deserialize<'de> for RecipesItem { + fn deserialize(deserializer: D) -> Result where - D: Deserializer<'de>, + D: serde::de::Deserializer<'de>, { - /// Visits a serialized string, decoding it as a digest - struct PatternVisitor; + struct RecipeCollectorVisitor; - impl<'de> serde::de::Visitor<'de> for PatternVisitor { - type Value = Vec; + impl<'de> serde::de::Visitor<'de> for RecipeCollectorVisitor { + type Value = RecipesItem; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a glob pattern") } - fn visit_seq(self, mut seq: A) -> Result + fn visit_str(self, v: &str) -> Result where - A: serde::de::SeqAccess<'de>, + E: serde::de::Error, { - let mut patterns = Vec::with_capacity(seq.size_hint().unwrap_or(0)); - while let Some(pattern) = seq.next_element()? { - let pattern = glob::Pattern::new(pattern).map_err(serde::de::Error::custom)?; - patterns.push(pattern); + let path = glob::Pattern::new(v).map_err(serde::de::Error::custom)?; + Ok(RecipesItem { path }) + } + + fn visit_map(self, map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + #[derive(Deserialize)] + struct RawRecipeItem { + path: String, } - Ok(patterns) + + let raw_recipe = + RawRecipeItem::deserialize(serde::de::value::MapAccessDeserializer::new(map))?; + self.visit_str(&raw_recipe.path) } } - deserializer.deserialize_seq(PatternVisitor) + + deserializer.deserialize_any(RecipeCollectorVisitor) } } diff --git a/crates/spk-workspace/src/file_test.rs b/crates/spk-workspace/src/file_test.rs index 8f6b4074a..ef32214cf 100644 --- a/crates/spk-workspace/src/file_test.rs +++ b/crates/spk-workspace/src/file_test.rs @@ -20,18 +20,22 @@ recipes: [] "#; #[rstest] -fn test_workspace_roundtrip() { - let workspace = WorkspaceFile { - recipes: vec![ - glob::Pattern::new("packages/*/*.spk.yml").unwrap(), - glob::Pattern::new("platforms/*/*.spk.yml").unwrap(), - ], - }; - - let serialized = serde_json::to_string(&workspace).unwrap(); - let deserialized: WorkspaceFile = serde_json::from_str(&serialized).unwrap(); - - assert_eq!(workspace, deserialized); +#[case( + r#" +api: v0/workspace +recipes: + - packages/**/*.spk.yaml + - path: packages/python/python2.spk.yaml + versions: [2.7.18] + - path: packages/python/python3.spk.yaml + versions: + - '3.7.{0..17}' + - '3.8.{0..20}' + - '3.9.{0..21}' +"# +)] +fn test_workspace_from_yaml(#[case] yaml: &str) { + let _deserialized: WorkspaceFile = serde_yaml::from_str(yaml).unwrap(); } #[rstest] From 0980e04bb76c4045aada63ac95a382b92add93a2 Mon Sep 17 00:00:00 2001 From: Ryan Bottriell Date: Fri, 13 Dec 2024 15:38:22 -0800 Subject: [PATCH 07/11] Add support for brace-expanded version number lists Signed-off-by: Ryan Bottriell --- Cargo.lock | 7 ++++++ Cargo.toml | 1 + crates/spk-workspace/Cargo.toml | 9 ++++--- crates/spk-workspace/src/file.rs | 43 ++++++++++++++++++++++++++++++-- workspace.spk.yml | 26 ++++++++++++++++++- 5 files changed, 79 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47d4dd78a..7ecec1f72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -367,6 +367,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bracoxide" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc0bcb5424e8e1f29c21a00f2d222df5e8e9779732ff03f840315d8fbac708e" + [[package]] name = "brownstone" version = "3.0.0" @@ -4655,6 +4661,7 @@ dependencies = [ name = "spk-workspace" version = "0.42.0" dependencies = [ + "bracoxide", "format_serde_error", "glob", "itertools 0.12.0", diff --git a/Cargo.toml b/Cargo.toml index 1ca8b2365..3683aa754 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ description = "SPK is a Package Manager for high-velocity software environments, [workspace.dependencies] arc-swap = "1.6.0" async-trait = "0.1" +bracoxide = "0.1.4" bytes = "1.5" cached = "0.48.1" chrono = { version = "0.4.34", features = ["serde"] } diff --git a/crates/spk-workspace/Cargo.toml b/crates/spk-workspace/Cargo.toml index dc3ebb7df..23c07a017 100644 --- a/crates/spk-workspace/Cargo.toml +++ b/crates/spk-workspace/Cargo.toml @@ -16,15 +16,16 @@ workspace = true sentry = ["spk-solve/sentry"] [dependencies] -serde = { workspace = true, features = ["derive"] } +bracoxide = { workspace = true } +format_serde_error = { workspace = true } glob = { workspace = true } itertools = { workspace = true } -spk-solve = { workspace = true } -thiserror = { workspace = true } miette = { workspace = true } +serde = { workspace = true, features = ["derive"] } serde_yaml = { workspace = true } spk-schema = { workspace = true } -format_serde_error = { workspace = true } +spk-solve = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] rstest = { workspace = true } diff --git a/crates/spk-workspace/src/file.rs b/crates/spk-workspace/src/file.rs index 9b14f67c7..a7d7a37b6 100644 --- a/crates/spk-workspace/src/file.rs +++ b/crates/spk-workspace/src/file.rs @@ -3,9 +3,13 @@ // https://github.com/spkenv/spk use std::path::Path; +use std::str::FromStr; +use bracoxide::tokenizer::TokenizationError; +use bracoxide::OxidizationError; use serde::Deserialize; use spk_schema::foundation::FromYaml; +use spk_schema::version::Version; use crate::error::LoadWorkspaceFileError; @@ -75,6 +79,7 @@ impl WorkspaceFile { #[derive(Debug, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)] pub struct RecipesItem { pub path: glob::Pattern, + pub versions: Vec, } impl<'de> serde::de::Deserialize<'de> for RecipesItem { @@ -96,7 +101,10 @@ impl<'de> serde::de::Deserialize<'de> for RecipesItem { E: serde::de::Error, { let path = glob::Pattern::new(v).map_err(serde::de::Error::custom)?; - Ok(RecipesItem { path }) + Ok(RecipesItem { + path, + versions: Vec::new(), + }) } fn visit_map(self, map: A) -> Result @@ -106,11 +114,42 @@ impl<'de> serde::de::Deserialize<'de> for RecipesItem { #[derive(Deserialize)] struct RawRecipeItem { path: String, + #[serde(default)] + versions: Vec, } let raw_recipe = RawRecipeItem::deserialize(serde::de::value::MapAccessDeserializer::new(map))?; - self.visit_str(&raw_recipe.path) + let mut base = self.visit_str(&raw_recipe.path)?; + for (i, version_expr) in raw_recipe.versions.into_iter().enumerate() { + let expand_result = bracoxide::bracoxidize(&version_expr); + let expanded = match expand_result { + Ok(expanded) => expanded, + Err(OxidizationError::TokenizationError(TokenizationError::NoBraces)) + | Err(OxidizationError::TokenizationError( + TokenizationError::EmptyContent, + )) + | Err(OxidizationError::TokenizationError( + TokenizationError::FormatNotSupported, + )) => { + vec![version_expr] + } + Err(err) => { + return Err(serde::de::Error::custom(format!( + "invalid brace expansion in position {i}: {err:?}" + ))) + } + }; + for version in expanded { + let parsed = Version::from_str(&version).map_err(|err| { + serde::de::Error::custom(format!( + "brace expansion in position {i} produced invalid version '{version}': {err}" + )) + })?; + base.versions.push(parsed); + } + } + Ok(base) } } diff --git a/workspace.spk.yml b/workspace.spk.yml index cc340e414..46bfe1271 100644 --- a/workspace.spk.yml +++ b/workspace.spk.yml @@ -1,4 +1,28 @@ api: v0/workspace recipes: - - packages/**.spk.yml + # collect all of the recipes in the workspace + - packages/**/*.spk.yaml + + # some recipes require additional information + # which can be augmented even if they were already + # collected above + + - path: packages/python/python2.spk.yaml + # here, we define the specific versions that can + # be build from a recipe + versions: [2.7.18] + + - path: packages/python/python3.spk.yaml + # we can use bash-style brace expansion to define + # ranges of versions that are supported + versions: + - '3.7.{0..17}' + - '3.8.{0..20}' + - '3.9.{0..21}' + - '3.10.{0..16}' + - '3.11.{0..11}' + - '3.12.{0..8}' + - '3.13.{0..1}' + + From 086dad032c935e1a95512daec4fa24d59ace392f Mon Sep 17 00:00:00 2001 From: Ryan Bottriell Date: Mon, 16 Dec 2024 13:36:34 -0800 Subject: [PATCH 08/11] Move packages cli arg into shared flags struct Signed-off-by: Ryan Bottriell --- crates/spk-cli/cmd-build/src/cmd_build.rs | 36 +++---- .../cmd-make-binary/src/cmd_make_binary.rs | 57 ++--------- .../cmd-make-source/src/cmd_make_source.rs | 56 +++-------- crates/spk-cli/common/src/flags.rs | 96 ++++++++++++++++++- 4 files changed, 139 insertions(+), 106 deletions(-) diff --git a/crates/spk-cli/cmd-build/src/cmd_build.rs b/crates/spk-cli/cmd-build/src/cmd_build.rs index 2d6718f32..1e5afd5d7 100644 --- a/crates/spk-cli/cmd-build/src/cmd_build.rs +++ b/crates/spk-cli/cmd-build/src/cmd_build.rs @@ -4,8 +4,8 @@ use clap::Args; use miette::Result; -use spk_cli_common::{flags, CommandArgs, Run}; -use spk_cmd_make_binary::cmd_make_binary::PackageSpecifier; +use spk_cli_common::flags::{self, PackageSpecifier}; +use spk_cli_common::{CommandArgs, Run}; #[cfg(test)] #[path = "./cmd_build_test/mod.rs"] @@ -37,9 +37,8 @@ pub struct Build { #[clap(long, short)] env: bool, - /// The package names or yaml spec files to build - #[clap(name = "NAME|SPEC_FILE")] - packages: Vec, + #[clap(flatten)] + packages: flags::Packages, /// Build only the specified variants #[clap(flatten)] @@ -77,13 +76,13 @@ impl Run for Build { .await?; // divide our packages into one for each iteration of mks/mkb - let mut runs: Vec<_> = self.packages.iter().map(|f| vec![f.to_owned()]).collect(); + let mut runs: Vec<_> = self.packages.split(); if runs.is_empty() { - runs.push(Vec::new()); + runs.push(Default::default()); } let mut builds_for_summary = spk_cli_common::BuildResult::default(); - for packages in runs { + for mut packages in runs { let mut make_source = spk_cmd_make_source::cmd_make_source::MakeSource { options: self.options.clone(), verbose: self.verbose, @@ -94,6 +93,17 @@ impl Run for Build { let idents = make_source.make_source().await?; builds_for_summary.extend(make_source.created_src); + // add the source ident specifier from the source build to ensure that + // the binary build operates over this exact source package + packages.packages = packages + .packages + .into_iter() + .zip(idents.into_iter()) + .map(|(package, ident)| { + PackageSpecifier::WithSourceIdent((package.into_specifier(), ident.into())) + }) + .collect(); + let mut make_binary = spk_cmd_make_binary::cmd_make_binary::MakeBinary { verbose: self.verbose, runtime: self.runtime.clone(), @@ -102,13 +112,7 @@ impl Run for Build { here: self.here, interactive: self.interactive, env: self.env, - packages: packages - .into_iter() - .zip(idents.into_iter()) - .map(|(package, ident)| { - PackageSpecifier::WithSourceIdent((package, ident.into())) - }) - .collect(), + packages, variant: self.variant.clone(), formatter_settings: self.formatter_settings.clone(), allow_circular_dependencies: self.allow_circular_dependencies, @@ -139,6 +143,6 @@ impl Run for Build { impl CommandArgs for Build { // The important positional args for a build are the packages fn get_positional_args(&self) -> Vec { - self.packages.clone() + self.packages.get_positional_args() } } diff --git a/crates/spk-cli/cmd-make-binary/src/cmd_make_binary.rs b/crates/spk-cli/cmd-make-binary/src/cmd_make_binary.rs index 8bbe9d640..bde6e9a8a 100644 --- a/crates/spk-cli/cmd-make-binary/src/cmd_make_binary.rs +++ b/crates/spk-cli/cmd-make-binary/src/cmd_make_binary.rs @@ -11,7 +11,7 @@ use miette::{bail, miette, Context, IntoDiagnostic, Report, Result}; use spk_build::{BinaryPackageBuilder, BuildSource}; use spk_cli_common::{flags, spk_exe, BuildArtifact, BuildResult, CommandArgs, Run}; use spk_schema::foundation::format::FormatIdent; -use spk_schema::ident::{PkgRequest, RangeIdent, RequestedBy}; +use spk_schema::ident::{PkgRequest, RequestedBy}; use spk_schema::option_map::HOST_OPTIONS; use spk_schema::prelude::*; use spk_schema::OptionMap; @@ -21,31 +21,6 @@ use spk_storage as storage; #[path = "./cmd_make_binary_test.rs"] mod cmd_make_binary_test; -#[derive(Clone, Debug)] -pub enum PackageSpecifier { - Plain(String), - WithSourceIdent((String, RangeIdent)), -} - -impl PackageSpecifier { - // Return the package spec or filename string. - fn get_specifier(&self) -> &String { - match self { - PackageSpecifier::Plain(s) => s, - PackageSpecifier::WithSourceIdent((s, _)) => s, - } - } -} - -impl std::str::FromStr for PackageSpecifier { - type Err = clap::Error; - - fn from_str(s: &str) -> Result { - // On the command line, only `Plain` is possible. - Ok(PackageSpecifier::Plain(s.to_owned())) - } -} - /// Build a binary package from a spec file or source package. #[derive(Args)] #[clap(visible_aliases = &["mkbinary", "mkbin", "mkb"])] @@ -72,9 +47,8 @@ pub struct MakeBinary { #[clap(long, short)] pub env: bool, - /// The local yaml spec files or published package/versions to build or rebuild - #[clap(name = "SPEC_FILE|PKG/VER")] - pub packages: Vec, + #[clap(flatten)] + pub packages: flags::Packages, /// Build only the specified variants #[clap(flatten)] @@ -96,11 +70,7 @@ pub struct MakeBinary { impl CommandArgs for MakeBinary { // The important positional args for a make-binary are the packages fn get_positional_args(&self) -> Vec { - self.packages - .iter() - .map(|ps| ps.get_specifier()) - .cloned() - .collect() + self.packages.get_positional_args() } } @@ -130,21 +100,12 @@ impl Run for MakeBinary { .map(|(_, r)| Arc::new(r)) .collect::>(); - let mut packages: Vec<_> = self.packages.iter().cloned().map(Some).collect(); - if packages.is_empty() { - packages.push(None) - } - let opt_host_options = (!self.options.no_host).then(|| HOST_OPTIONS.get().unwrap_or_default()); - for package in packages { - let (spec_data, filename) = flags::find_package_recipe_from_template_or_repo( - package.as_ref().map(|p| p.get_specifier()), - &options, - &repos, - ) - .await?; + for (package, spec_data, filename) in + self.packages.find_all_recipes(&options, &repos).await? + { let recipe = spec_data.into_recipe().wrap_err_with(|| { format!( "{filename} was expected to contain a recipe", @@ -220,7 +181,9 @@ impl Run for MakeBinary { .into_diagnostic() .wrap_err("Failed to get current directory")?; builder.with_source(BuildSource::LocalPath(here)); - } else if let Some(PackageSpecifier::WithSourceIdent((_, ref ident))) = package { + } else if let Some(flags::PackageSpecifier::WithSourceIdent((_, ref ident))) = + package + { // Use the source package `AnyIdent` if the caller supplied one. builder.with_source(BuildSource::SourcePackage(ident.clone())); } diff --git a/crates/spk-cli/cmd-make-source/src/cmd_make_source.rs b/crates/spk-cli/cmd-make-source/src/cmd_make_source.rs index 6b7629cc6..50668c926 100644 --- a/crates/spk-cli/cmd-make-source/src/cmd_make_source.rs +++ b/crates/spk-cli/cmd-make-source/src/cmd_make_source.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 // https://github.com/spkenv/spk -use std::path::PathBuf; use std::sync::Arc; use clap::Args; @@ -11,7 +10,7 @@ use spk_build::SourcePackageBuilder; use spk_cli_common::{flags, BuildArtifact, BuildResult, CommandArgs, Run}; use spk_schema::foundation::format::FormatIdent; use spk_schema::ident::LocatedBuildIdent; -use spk_schema::{Package, Recipe, SpecTemplate, Template, TemplateExt}; +use spk_schema::{Package, Recipe}; use spk_storage as storage; /// Build a source package from a spec file. @@ -27,9 +26,8 @@ pub struct MakeSource { #[clap(short, long, global = true, action = clap::ArgAction::Count)] pub verbose: u8, - /// The packages or yaml spec files to collect - #[clap(name = "PKG|SPEC_FILE")] - pub packages: Vec, + #[clap(flatten)] + pub packages: flags::Packages, /// Populated with the created src to generate a summary from the caller. #[clap(skip)] @@ -60,44 +58,18 @@ impl MakeSource { .runtime .ensure_active_runtime(&["make-source", "mksource", "mksrc", "mks"]) .await?; - let local: storage::RepositoryHandle = storage::local_repository().await?.into(); + let local = Arc::new(storage::local_repository().await?.into()); let options = self.options.get_options()?; - let mut packages: Vec<_> = self.packages.iter().cloned().map(Some).collect(); - if packages.is_empty() { - packages.push(None) - } - let mut idents = Vec::new(); - for package in packages.into_iter() { - let template = match flags::find_package_template(package.as_ref())? { - flags::FindPackageTemplateResult::NotFound(name) => { - // TODO:: load from given repos - Arc::new(SpecTemplate::from_file(name.as_ref())?) - } - res => { - let (_, template) = res.must_be_found(); - template - } - }; - let root = template - .file_path() - .parent() - .map(ToOwned::to_owned) - .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))); - if let Some(name) = template.name() { - tracing::info!("rendering template for {name}"); - } else { - tracing::info!("rendering template without a name"); - } - let rendered_data = template.render(&options)?; - let recipe = rendered_data.into_recipe().wrap_err_with(|| { - format!( - "{filename} was expected to contain a recipe", - filename = template.file_path().to_string_lossy() - ) - })?; + for (_package, spec_data, path) in self + .packages + .find_all_recipes(&options, &[Arc::clone(&local)]) + .await? + { + let root = path.parent().unwrap_or_else(|| std::path::Path::new(".")); + let recipe = spec_data.into_recipe()?; let ident = recipe.ident(); tracing::info!("saving package recipe for {}", ident.format_ident()); @@ -106,12 +78,12 @@ impl MakeSource { tracing::info!("collecting sources for {}", ident.format_ident()); let (out, _components) = SourcePackageBuilder::from_recipe(Arc::unwrap_or_clone(recipe)) - .build_and_publish(root, &local) + .build_and_publish(root, &*local) .await .wrap_err("Failed to collect sources")?; tracing::info!("created {}", out.ident().format_ident()); self.created_src.push( - template.file_path().display().to_string(), + path.display().to_string(), BuildArtifact::Source(out.ident().clone()), ); idents.push(out.ident().clone().into_located(local.name().to_owned())); @@ -123,6 +95,6 @@ impl MakeSource { impl CommandArgs for MakeSource { fn get_positional_args(&self) -> Vec { // The important positional args for a make-source are the packages - self.packages.clone() + self.packages.get_positional_args() } } diff --git a/crates/spk-cli/common/src/flags.rs b/crates/spk-cli/common/src/flags.rs index 4f61712f8..8fd476d71 100644 --- a/crates/spk-cli/common/src/flags.rs +++ b/crates/spk-cli/common/src/flags.rs @@ -27,6 +27,7 @@ use spk_schema::ident::{ AnyIdent, AsVersionIdent, PkgRequest, + RangeIdent, Request, RequestedBy, VarRequest, @@ -48,7 +49,7 @@ pub use variant::{Variant, VariantBuildStatus, VariantLocation}; use {spk_solve as solve, spk_storage as storage}; use crate::parsing::{stage_specifier, VariantIndex}; -use crate::Error; +use crate::{CommandArgs, Error}; #[cfg(test)] #[path = "./flags_test.rs"] @@ -662,6 +663,99 @@ pub async fn parse_stage_specifier( Ok((recipe, filename, stage, build_variant)) } +/// Specifies a package, allowing for more details when being invoked +/// programmatically instead of by a user on the command line. +#[derive(Clone, Debug)] +pub enum PackageSpecifier { + Plain(String), + WithSourceIdent((String, RangeIdent)), +} + +impl PackageSpecifier { + // Return the package spec or filename string. + pub fn get_specifier(&self) -> &String { + match self { + PackageSpecifier::Plain(s) => s, + PackageSpecifier::WithSourceIdent((s, _)) => s, + } + } + + // Extract the package spec or filename string. + pub fn into_specifier(self) -> String { + match self { + PackageSpecifier::Plain(s) => s, + PackageSpecifier::WithSourceIdent((s, _)) => s, + } + } +} + +impl std::str::FromStr for PackageSpecifier { + type Err = clap::Error; + + fn from_str(s: &str) -> Result { + // On the command line, only `Plain` is possible. + Ok(PackageSpecifier::Plain(s.to_owned())) + } +} + +#[derive(Args, Default, Clone)] +pub struct Packages { + /// The package names or yaml spec files to operate on + #[clap(name = "PKG|SPEC_FILE")] + pub packages: Vec, +} + +impl CommandArgs for Packages { + fn get_positional_args(&self) -> Vec { + self.packages + .iter() + .map(|ps| ps.get_specifier()) + .cloned() + .collect() + } +} + +impl Packages { + /// Create clones of these arguments where each instance + /// has only one package specified. + /// + /// Useful for running multiple package operations for each + /// entry in order. + pub fn split(&self) -> Vec { + self.packages + .iter() + .cloned() + .map(|p| Self { + packages: vec![p], + ..self.clone() + }) + .collect() + } + + pub async fn find_all_recipes( + &self, + options: &OptionMap, + repos: &[Arc], + ) -> Result, SpecFileData, std::path::PathBuf)>> { + let mut packages: Vec<_> = self.packages.iter().cloned().map(Some).collect(); + if packages.is_empty() { + packages.push(None) + } + + let mut results = Vec::with_capacity(packages.len()); + for package in packages { + let (file_data, path) = find_package_recipe_from_template_or_repo( + package.as_ref().map(|p| p.get_specifier()), + options, + repos, + ) + .await?; + results.push((package, file_data, path)); + } + Ok(results) + } +} + /// 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 From 4ff5b281d37e2060be551b61a99c420e4c0c5fa4 Mon Sep 17 00:00:00 2001 From: Ryan Bottriell Date: Tue, 17 Dec 2024 10:39:02 -0800 Subject: [PATCH 09/11] 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 7ecec1f72..5d7910e7e 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 3683aa754..48fe62d87 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 7b664eb28..0c3b58c1d 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 cf7ccfcdb..678967557 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 f13b3d473..9ff3010d1 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 b3cbce8f9..2f6e76bf0 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 cb4192388..64f238de2 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 a9cdb674b..2dd873b56 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 8fd476d71..2bc895fae 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 c47214589..a594ab1be 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 0663a73f9..c79f179da 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 76f52143b..29e0be848 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 23c07a017..4cf10d0b7 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 d434abb4e..42cb3c683 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 4ffeb40ed..93e03fb3b 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 a7d7a37b6..380d40602 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 d9e1aa137..4fc1b5604 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 653456109..d40576a45 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 From 9ced96e7c87d008e3b88b104a6cb1b0c44533237 Mon Sep 17 00:00:00 2001 From: Ryan Bottriell Date: Tue, 17 Dec 2024 14:46:53 -0800 Subject: [PATCH 10/11] Update workspace builder to deduplicate paths Signed-off-by: Ryan Bottriell --- crates/spk-workspace/src/builder.rs | 6 ++++-- crates/spk-workspace/src/workspace.rs | 2 +- cspell.json | 17 ++++++++++------- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/crates/spk-workspace/src/builder.rs b/crates/spk-workspace/src/builder.rs index 42cb3c683..74279647b 100644 --- a/crates/spk-workspace/src/builder.rs +++ b/crates/spk-workspace/src/builder.rs @@ -2,11 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 // https://github.com/spkenv/spk +use std::collections::HashSet; + use crate::error; #[derive(Default)] pub struct WorkspaceBuilder { - spec_files: Vec, + spec_files: HashSet, } impl WorkspaceBuilder { @@ -53,7 +55,7 @@ impl WorkspaceBuilder { /// Add a recipe file to the workspace. pub fn with_recipe_file(mut self, path: impl Into) -> Self { - self.spec_files.push(path.into()); + self.spec_files.insert(path.into()); self } diff --git a/crates/spk-workspace/src/workspace.rs b/crates/spk-workspace/src/workspace.rs index d40576a45..cbf365687 100644 --- a/crates/spk-workspace/src/workspace.rs +++ b/crates/spk-workspace/src/workspace.rs @@ -116,7 +116,7 @@ impl Workspace { ); let by_name = self.templates.entry(template.name().cloned()).or_default(); by_name.push(template); - Ok(&by_name.last().expect("just pushed something")) + Ok(by_name.last().expect("just pushed something")) } } diff --git a/cspell.json b/cspell.json index 630cb5303..19ed72fce 100644 --- a/cspell.json +++ b/cspell.json @@ -11,6 +11,7 @@ "language": "en", "version": "0.2", "words": [ + "A7USTIBXPXHMD5CYEIIOBMFLM3X77ESVR3WAUXQ7XQQGTHKH7DMQ", "ABCA", "ABEA", "abouts", @@ -19,7 +20,6 @@ "aclocal", "acyclical", "addrs", - "A7USTIBXPXHMD5CYEIIOBMFLM3X77ESVR3WAUXQ7XQQGTHKH7DMQ", "AEMKIJQ", "AGVXA", "alib", @@ -31,6 +31,7 @@ "ASWF", "atexit", "autogen", + "automake", "automounter", "autopoint", "autoreconf", @@ -55,9 +56,11 @@ "Blosc", "BLOSC", "BMLX", - "bools", "Boolish", + "bools", "Bottriell", + "bracoxide", + "bracoxidize", "bufread", "buildable", "builddep", @@ -272,8 +275,8 @@ "hasattr", "hashable", "hashalgo", - "hashset", "Hasher", + "hashset", "helmignore", "HFSBQEYR", "HHMMSS", @@ -495,7 +498,6 @@ "OWYWHJKCVRKGSBL", "OXFB", "OYMIQUY", - "PJDUUENJYFFDKZWRKHDUXK4FGGZ7FHDYMZ7P4CXORUG6TUMDJ7A", "parsedbuf", "parsewords", "partio", @@ -512,6 +514,7 @@ "pids", "Pinnable", "pixbuf", + "PJDUUENJYFFDKZWRKHDUXK4FGGZ7FHDYMZ7P4CXORUG6TUMDJ7A", "pkga", "pkgb", "pkgconfig", @@ -638,18 +641,18 @@ "SIMD", "SNAPPROCESS", "somedata", + "SOMEDIGEST", "somedir", "somename", "someotherdata", + "SOMEOTHERDIGEST", "somepkg", "someproject", "sometag", + "SOMETAG", "somethingelse", "somevalue", "somevar", - "SOMEDIGEST", - "SOMEOTHERDIGEST", - "SOMETAG", "SOVERSION", "spdev", "SPFS", From 3ffc6ac003f3d57d2152d26874c3258523b7f410 Mon Sep 17 00:00:00 2001 From: Ryan Bottriell Date: Tue, 14 Jan 2025 14:08:07 -0800 Subject: [PATCH 11/11] Remove duplicate unit test case Co-authored-by: jrray Signed-off-by: Ryan Bottriell --- crates/spk-workspace/src/file_test.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/spk-workspace/src/file_test.rs b/crates/spk-workspace/src/file_test.rs index ef32214cf..2c84833a3 100644 --- a/crates/spk-workspace/src/file_test.rs +++ b/crates/spk-workspace/src/file_test.rs @@ -56,8 +56,6 @@ fn test_must_have_file(tmpdir: tempfile::TempDir) { #[case("my-workspace", "my-workspace/src/packages")] #[should_panic] #[case("my-workspace", "other-dir")] -#[case("", "")] -#[case("my-workspace", "my-workspace/src/packages")] #[should_panic] #[case("my-workspace", "other-dir/src/packages")] fn test_workspace_discovery(