From 5f9b5ac781586b64ce8d16b825fb6d37668e1007 Mon Sep 17 00:00:00 2001 From: rvcas Date: Sun, 23 Mar 2025 20:24:11 -0400 Subject: [PATCH] feat: basic ability to have many projects in one repo --- Cargo.lock | 1 + crates/aiken-lsp/src/lib.rs | 4 +- crates/aiken-lsp/src/server.rs | 8 +- crates/aiken-lsp/src/server/lsp_project.rs | 4 +- crates/aiken-project/Cargo.toml | 1 + crates/aiken-project/src/blueprint/mod.rs | 8 +- crates/aiken-project/src/config.rs | 108 +++++++++++++++--- crates/aiken-project/src/deps.rs | 4 +- crates/aiken-project/src/deps/manifest.rs | 6 +- crates/aiken-project/src/docs.rs | 8 +- crates/aiken-project/src/docs/source_links.rs | 4 +- crates/aiken-project/src/error.rs | 2 + crates/aiken-project/src/lib.rs | 8 +- crates/aiken-project/src/watch.rs | 81 +++++++++---- crates/aiken/src/cmd/new.rs | 4 +- crates/aiken/src/cmd/packages/add.rs | 4 +- 16 files changed, 187 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c5dc624..05c61b6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,6 +144,7 @@ dependencies = [ "dirs", "fslock", "futures", + "glob", "hex", "ignore", "indexmap 1.9.3", diff --git a/crates/aiken-lsp/src/lib.rs b/crates/aiken-lsp/src/lib.rs index 640585e6..a57264d9 100644 --- a/crates/aiken-lsp/src/lib.rs +++ b/crates/aiken-lsp/src/lib.rs @@ -1,5 +1,5 @@ use crate::server::Server; -use aiken_project::{config::Config, paths}; +use aiken_project::{config::ProjectConfig, paths}; use error::Error; use lsp_server::Connection; use std::env; @@ -23,7 +23,7 @@ pub fn start() -> Result<(), Error> { let config = if paths::project_config().exists() { tracing::info!("Aiken project detected"); - Some(Config::load(&root).expect("failed to load aiken.toml")) + Some(ProjectConfig::load(&root).expect("failed to load aiken.toml")) } else { tracing::info!("Aiken project config not found"); diff --git a/crates/aiken-lsp/src/server.rs b/crates/aiken-lsp/src/server.rs index c1486d7e..9d387093 100644 --- a/crates/aiken-lsp/src/server.rs +++ b/crates/aiken-lsp/src/server.rs @@ -17,7 +17,7 @@ use aiken_lang::{ tipo::pretty::Printer, }; use aiken_project::{ - config::{self, Config}, + config::{self, ProjectConfig}, error::{Error as ProjectError, GetSource}, module::CheckedModule, }; @@ -50,7 +50,7 @@ pub struct Server { // Project root directory root: PathBuf, - config: Option, + config: Option, /// Files that have been edited in memory edited: HashMap, @@ -235,7 +235,7 @@ impl Server { } DidChangeWatchedFiles::METHOD => { - if let Ok(config) = Config::load(&self.root) { + if let Ok(config) = ProjectConfig::load(&self.root) { self.config = Some(config); self.create_new_compiler(); self.compile(connection)?; @@ -603,7 +603,7 @@ impl Server { pub fn new( initialize_params: InitializeParams, - config: Option, + config: Option, root: PathBuf, ) -> Self { let mut server = Server { diff --git a/crates/aiken-lsp/src/server/lsp_project.rs b/crates/aiken-lsp/src/server/lsp_project.rs index 8a13170f..cc695018 100644 --- a/crates/aiken-lsp/src/server/lsp_project.rs +++ b/crates/aiken-lsp/src/server/lsp_project.rs @@ -1,5 +1,5 @@ use aiken_lang::{ast::Tracing, line_numbers::LineNumbers, test_framework::PropertyTest}; -use aiken_project::{config::Config, error::Error as ProjectError, module::CheckedModule, Project}; +use aiken_project::{config::ProjectConfig, error::Error as ProjectError, module::CheckedModule, Project}; use std::{collections::HashMap, path::PathBuf}; #[derive(Debug)] @@ -18,7 +18,7 @@ pub struct LspProject { } impl LspProject { - pub fn new(config: Config, root: PathBuf, telemetry: super::telemetry::Lsp) -> Self { + pub fn new(config: ProjectConfig, root: PathBuf, telemetry: super::telemetry::Lsp) -> Self { Self { project: Project::new_with_config(config, root, telemetry), modules: HashMap::new(), diff --git a/crates/aiken-project/Cargo.toml b/crates/aiken-project/Cargo.toml index 7c10fcaf..eae2a93d 100644 --- a/crates/aiken-project/Cargo.toml +++ b/crates/aiken-project/Cargo.toml @@ -22,6 +22,7 @@ ciborium = "0.2.2" dirs = "4.0.0" fslock = "0.2.1" futures = "0.3.26" +glob = "0.3.2" hex = "0.4.3" ignore = "0.4.20" indexmap = "1.9.2" diff --git a/crates/aiken-project/src/blueprint/mod.rs b/crates/aiken-project/src/blueprint/mod.rs index 33bf6cf9..8ee873e9 100644 --- a/crates/aiken-project/src/blueprint/mod.rs +++ b/crates/aiken-project/src/blueprint/mod.rs @@ -6,7 +6,7 @@ pub mod schema; pub mod validator; use crate::{ - config::{self, Config, PlutusVersion}, + config::{self, ProjectConfig, PlutusVersion}, module::CheckedModules, }; use aiken_lang::gen_uplc::CodeGenerator; @@ -58,7 +58,7 @@ pub enum LookupResult<'a, T> { impl Blueprint { pub fn new( - config: &Config, + config: &ProjectConfig, modules: &CheckedModules, generator: &mut CodeGenerator, ) -> Result { @@ -179,8 +179,8 @@ impl Blueprint { } } -impl From<&Config> for Preamble { - fn from(config: &Config) -> Self { +impl From<&ProjectConfig> for Preamble { + fn from(config: &ProjectConfig) -> Self { Preamble { title: config.name.to_string(), description: if config.description.is_empty() { diff --git a/crates/aiken-project/src/config.rs b/crates/aiken-project/src/config.rs index 9764fc35..c890fb81 100644 --- a/crates/aiken-project/src/config.rs +++ b/crates/aiken-project/src/config.rs @@ -7,6 +7,7 @@ use aiken_lang::{ parser::token::Base, }; pub use aiken_lang::{plutus_version::PlutusVersion, version::compiler_version}; +use glob::glob; use miette::NamedSource; use semver::Version; use serde::{ @@ -14,30 +15,94 @@ use serde::{ ser::{self, SerializeSeq, SerializeStruct}, Deserialize, Serialize, }; -use std::{collections::BTreeMap, fmt::Display, fs, io, path::Path}; +use std::{ + collections::BTreeMap, + fmt::Display, + fs, io, + path::{Path, PathBuf}, +}; #[derive(Deserialize, Serialize, Clone)] -pub struct Config { +pub struct ProjectConfig { pub name: PackageName, + pub version: String, + #[serde( deserialize_with = "deserialize_version", serialize_with = "serialize_version", default = "default_version" )] pub compiler: Version, + #[serde(default, deserialize_with = "validate_v3_only")] pub plutus: PlutusVersion, + pub license: Option, + #[serde(default)] pub description: String, + pub repository: Option, + #[serde(default)] pub dependencies: Vec, + #[serde(default)] pub config: BTreeMap>, } +#[derive(Deserialize, Serialize, Clone)] +struct RawWorkspaceConfig { + members: Vec, +} + +impl RawWorkspaceConfig { + pub fn expand_members(self, root: &Path) -> Vec { + let mut result = Vec::new(); + + for member in self.members { + let pattern = root.join(member); + + let glob_result: Vec<_> = pattern + .to_str() + .and_then(|s| glob(s).ok()) + .map_or(Vec::new(), |g| g.filter_map(Result::ok).collect()); + + if glob_result.is_empty() { + // No matches (or glob failed), treat as literal path + result.push(pattern); + } else { + // Glob worked, add all matches + result.extend(glob_result); + } + } + + result + } +} + +pub struct WorkspaceConfig { + pub members: Vec, +} + +impl WorkspaceConfig { + pub fn load(dir: &Path) -> Result { + let config_path = dir.join(paths::project_config()); + let raw_config = fs::read_to_string(&config_path).map_err(|_| Error::MissingManifest { + path: dir.to_path_buf(), + })?; + + let raw: RawWorkspaceConfig = toml::from_str(&raw_config).map_err(|e| { + from_toml_de_error(e, config_path, raw_config, TomlLoadingContext::Workspace) + })?; + + let members = raw.expand_members(dir); + + Ok(WorkspaceConfig { members }) + } +} + #[derive(Clone, Debug)] pub enum SimpleExpr { Int(i64), @@ -303,9 +368,9 @@ impl Display for Platform { } } -impl Config { +impl ProjectConfig { pub fn default(name: &PackageName) -> Self { - Config { + ProjectConfig { name: name.clone(), version: "0.0.0".to_string(), compiler: default_version(), @@ -338,23 +403,14 @@ impl Config { fs::write(aiken_toml_path, aiken_toml) } - pub fn load(dir: &Path) -> Result { + pub fn load(dir: &Path) -> Result { let config_path = dir.join(paths::project_config()); let raw_config = fs::read_to_string(&config_path).map_err(|_| Error::MissingManifest { path: dir.to_path_buf(), })?; - let result: Self = toml::from_str(&raw_config).map_err(|e| Error::TomlLoading { - ctx: TomlLoadingContext::Project, - path: config_path.clone(), - src: raw_config.clone(), - named: NamedSource::new(config_path.display().to_string(), raw_config).into(), - // this isn't actually a legit way to get the span - location: e.span().map(|range| Span { - start: range.start, - end: range.end, - }), - help: e.message().to_string(), + let result: Self = toml::from_str(&raw_config).map_err(|e| { + from_toml_de_error(e, config_path, raw_config, TomlLoadingContext::Project) })?; Ok(result) @@ -388,6 +444,26 @@ where } } +fn from_toml_de_error( + e: toml::de::Error, + config_path: PathBuf, + raw_config: String, + ctx: TomlLoadingContext, +) -> Error { + Error::TomlLoading { + ctx, + path: config_path.clone(), + src: raw_config.clone(), + named: NamedSource::new(config_path.display().to_string(), raw_config).into(), + // this isn't actually a legit way to get the span + location: e.span().map(|range| Span { + start: range.start, + end: range.end, + }), + help: e.message().to_string(), + } +} + mod built_info { include!(concat!(env!("OUT_DIR"), "/built.rs")); } diff --git a/crates/aiken-project/src/deps.rs b/crates/aiken-project/src/deps.rs index f9609cc6..04723577 100644 --- a/crates/aiken-project/src/deps.rs +++ b/crates/aiken-project/src/deps.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use tokio::time::Instant; use crate::{ - config::{Config, Dependency}, + config::{ProjectConfig, Dependency}, error::{Error, TomlLoadingContext}, package_name::PackageName, paths, @@ -133,7 +133,7 @@ impl From<&Manifest> for LocalPackages { } } -pub fn download(event_listener: &T, root_path: &Path, config: &Config) -> Result +pub fn download(event_listener: &T, root_path: &Path, config: &ProjectConfig) -> Result where T: EventListener, { diff --git a/crates/aiken-project/src/deps/manifest.rs b/crates/aiken-project/src/deps/manifest.rs index da06e466..b9e9602f 100644 --- a/crates/aiken-project/src/deps/manifest.rs +++ b/crates/aiken-project/src/deps/manifest.rs @@ -9,7 +9,7 @@ use std::{ }; use crate::{ - config::{Config, Dependency, Platform}, + config::{ProjectConfig, Dependency, Platform}, error::{Error, TomlLoadingContext}, package_name::PackageName, paths, @@ -27,7 +27,7 @@ pub struct Manifest { impl Manifest { pub fn load( event_listener: &T, - config: &Config, + config: &ProjectConfig, root_path: &Path, ) -> Result<(Self, bool), Error> where @@ -121,7 +121,7 @@ pub struct Package { pub source: Platform, } -fn resolve_versions(config: &Config, event_listener: &T) -> Result +fn resolve_versions(config: &ProjectConfig, event_listener: &T) -> Result where T: EventListener, { diff --git a/crates/aiken-project/src/docs.rs b/crates/aiken-project/src/docs.rs index 4547ed94..b5cb2000 100644 --- a/crates/aiken-project/src/docs.rs +++ b/crates/aiken-project/src/docs.rs @@ -1,5 +1,5 @@ use crate::{ - config::{Config, Repository}, + config::{ProjectConfig, Repository}, module::CheckedModule, }; use aiken_lang::{ @@ -104,7 +104,7 @@ impl DocLink { /// The documentation is built using template files located at the root of this crate. /// With the documentation, we also build a client-side search index to ease navigation /// across multiple modules. -pub fn generate_all(root: &Path, config: &Config, modules: Vec<&CheckedModule>) -> Vec { +pub fn generate_all(root: &Path, config: &ProjectConfig, modules: Vec<&CheckedModule>) -> Vec { let timestamp = new_timestamp(); let modules_links = generate_modules_links(&modules); @@ -155,7 +155,7 @@ pub fn generate_all(root: &Path, config: &Config, modules: Vec<&CheckedModule>) fn generate_module( root: &Path, - config: &Config, + config: &ProjectConfig, module: &CheckedModule, modules: &[DocLink], source: &DocLink, @@ -376,7 +376,7 @@ fn generate_static_assets(search_indexes: Vec) -> Vec { fn generate_readme( root: &Path, - config: &Config, + config: &ProjectConfig, modules: &[DocLink], source: &DocLink, timestamp: &Duration, diff --git a/crates/aiken-project/src/docs/source_links.rs b/crates/aiken-project/src/docs/source_links.rs index 8f2f8e6f..889c93d9 100644 --- a/crates/aiken-project/src/docs/source_links.rs +++ b/crates/aiken-project/src/docs/source_links.rs @@ -1,5 +1,5 @@ use crate::{ - config::{Config, Platform}, + config::{ProjectConfig, Platform}, CheckedModule, }; use aiken_lang::{ast::Span, line_numbers::LineNumbers}; @@ -12,7 +12,7 @@ pub struct SourceLinker { } impl SourceLinker { - pub fn new(root: &Path, config: &Config, module: &CheckedModule) -> Self { + pub fn new(root: &Path, config: &ProjectConfig, module: &CheckedModule) -> Self { let utf8_path = <&Utf8Path>::try_from( module .input_path diff --git a/crates/aiken-project/src/error.rs b/crates/aiken-project/src/error.rs index 0098ca0d..32e3e180 100644 --- a/crates/aiken-project/src/error.rs +++ b/crates/aiken-project/src/error.rs @@ -26,6 +26,7 @@ pub enum TomlLoadingContext { Project, Manifest, Package, + Workspace, } impl fmt::Display for TomlLoadingContext { @@ -34,6 +35,7 @@ impl fmt::Display for TomlLoadingContext { TomlLoadingContext::Project => write!(f, "project"), TomlLoadingContext::Manifest => write!(f, "manifest"), TomlLoadingContext::Package => write!(f, "package"), + TomlLoadingContext::Workspace => write!(f, "workspace"), } } } diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index 888022e1..45384de9 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -25,7 +25,7 @@ use crate::{ schema::{Annotated, Schema}, Blueprint, }, - config::Config, + config::ProjectConfig, error::{Error, Warning}, module::{CheckedModule, CheckedModules, ParsedModule, ParsedModules}, telemetry::Event, @@ -87,7 +87,7 @@ pub struct Project where T: EventListener, { - config: Config, + config: ProjectConfig, defined_modules: HashMap, checked_modules: CheckedModules, id_gen: IdGenerator, @@ -108,7 +108,7 @@ where T: EventListener, { pub fn new(root: PathBuf, event_listener: T) -> Result, Error> { - let config = Config::load(&root)?; + let config = ProjectConfig::load(&root)?; let demanded_compiler_version = format!("v{}", config.compiler); @@ -126,7 +126,7 @@ where Ok(project) } - pub fn new_with_config(config: Config, root: PathBuf, event_listener: T) -> Project { + pub fn new_with_config(config: ProjectConfig, root: PathBuf, event_listener: T) -> Project { let id_gen = IdGenerator::new(); let mut module_types = HashMap::new(); diff --git a/crates/aiken-project/src/watch.rs b/crates/aiken-project/src/watch.rs index 3756728e..05ca24d9 100644 --- a/crates/aiken-project/src/watch.rs +++ b/crates/aiken-project/src/watch.rs @@ -1,4 +1,4 @@ -use crate::{telemetry::EventTarget, Project}; +use crate::{config::WorkspaceConfig, telemetry::EventTarget, Project}; use miette::{Diagnostic, IntoDiagnostic}; use notify::{Event, RecursiveMode, Watcher}; use owo_colors::{OwoColorize, Stream::Stderr}; @@ -108,17 +108,57 @@ where current_dir }; - let mut project = match Project::new(project_path, EventTarget::default()) { - Ok(p) => Ok(p), - Err(e) => { - e.report(); - Err(ExitFailure::into_report()) + let mut warnings = Vec::new(); + let mut errs: Vec = Vec::new(); + let mut check_count = None; + + if let Ok(workspace) = WorkspaceConfig::load(&project_path) { + let res_projects = workspace + .members + .into_iter() + .map(|member| Project::new(member, EventTarget::default())) + .collect::>, crate::error::Error>>(); + + let projects = match res_projects { + Ok(p) => Ok(p), + Err(e) => { + e.report(); + Err(ExitFailure::into_report()) + } + }?; + + for mut project in projects { + let build_result = action(&mut project); + + warnings.extend(project.warnings()); + + let sum = check_count.unwrap_or(0) + project.checks_count.unwrap_or(0); + check_count = if sum > 0 { Some(sum) } else { None }; + + if let Err(e) = build_result { + errs.extend(e); + } } - }?; + } else { + let mut project = match Project::new(project_path, EventTarget::default()) { + Ok(p) => Ok(p), + Err(e) => { + e.report(); + Err(ExitFailure::into_report()) + } + }?; - let build_result = action(&mut project); + let build_result = action(&mut project); - let warnings = project.warnings(); + warnings.extend(project.warnings()); + + let sum = check_count.unwrap_or(0) + project.checks_count.unwrap_or(0); + check_count = if sum > 0 { Some(sum) } else { None }; + + if let Err(e) = build_result { + errs.extend(e); + } + } let warning_count = warnings.len(); @@ -130,7 +170,7 @@ where } } - if let Err(errs) = build_result { + if !errs.is_empty() { for err in &errs { err.report() } @@ -138,7 +178,7 @@ where eprintln!( "{}", Summary { - check_count: project.checks_count, + check_count, warning_count, error_count: errs.len(), } @@ -147,16 +187,14 @@ where return Err(ExitFailure::into_report()); } - if project.checks_count.unwrap_or_default() + warning_count > 0 { - eprintln!( - "{}", - Summary { - check_count: project.checks_count, - error_count: 0, - warning_count - } - ); - } + eprintln!( + "{}", + Summary { + check_count, + error_count: 0, + warning_count + } + ); } if warning_count > 0 && deny { @@ -172,6 +210,7 @@ where /// // Note: doctest disabled, because aiken_project doesn't have an implementation of EventListener I can use /// use aiken_project::watch::{watch_project, default_filter}; /// use aiken_project::{Project}; +/// /// watch_project(None, default_filter, 500, |project| { /// println!("Project changed!"); /// Ok(()) diff --git a/crates/aiken/src/cmd/new.rs b/crates/aiken/src/cmd/new.rs index 0a52c98f..8048a13f 100644 --- a/crates/aiken/src/cmd/new.rs +++ b/crates/aiken/src/cmd/new.rs @@ -1,5 +1,5 @@ use aiken_project::{ - config::{self, Config}, + config::{self, ProjectConfig}, package_name::{self, PackageName}, }; use indoc::{formatdoc, indoc}; @@ -46,7 +46,7 @@ fn create_project(args: Args, package_name: &PackageName) -> miette::Result<()> readme(&root, &package_name.repo)?; - Config::default(package_name) + ProjectConfig::default(package_name) .save(&root) .into_diagnostic()?; diff --git a/crates/aiken/src/cmd/packages/add.rs b/crates/aiken/src/cmd/packages/add.rs index c15e8f63..bdebd8fe 100644 --- a/crates/aiken/src/cmd/packages/add.rs +++ b/crates/aiken/src/cmd/packages/add.rs @@ -1,5 +1,5 @@ use aiken_project::{ - config::{Config, Dependency, Platform}, + config::{ProjectConfig, Dependency, Platform}, error::Warning, package_name::PackageName, pretty, @@ -35,7 +35,7 @@ pub fn exec(args: Args) -> miette::Result<()> { source: Platform::Github, }; - let config = match Config::load(&root) { + let config = match ProjectConfig::load(&root) { Ok(config) => config, Err(e) => { e.report();