feat: basic ability to have many projects in one repo

This commit is contained in:
rvcas 2025-03-23 20:24:11 -04:00 committed by Lucas
parent e0732c2ecf
commit 5f9b5ac781
16 changed files with 187 additions and 68 deletions

1
Cargo.lock generated vendored
View File

@ -144,6 +144,7 @@ dependencies = [
"dirs",
"fslock",
"futures",
"glob",
"hex",
"ignore",
"indexmap 1.9.3",

View File

@ -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");

View File

@ -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::Config>,
config: Option<config::ProjectConfig>,
/// Files that have been edited in memory
edited: HashMap<String, String>,
@ -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::Config>,
config: Option<config::ProjectConfig>,
root: PathBuf,
) -> Self {
let mut server = Server {

View File

@ -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(),

View File

@ -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"

View File

@ -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<Self, Error> {
@ -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() {

View File

@ -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<String>,
#[serde(default)]
pub description: String,
pub repository: Option<Repository>,
#[serde(default)]
pub dependencies: Vec<Dependency>,
#[serde(default)]
pub config: BTreeMap<String, BTreeMap<String, SimpleExpr>>,
}
#[derive(Deserialize, Serialize, Clone)]
struct RawWorkspaceConfig {
members: Vec<String>,
}
impl RawWorkspaceConfig {
pub fn expand_members(self, root: &Path) -> Vec<PathBuf> {
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<PathBuf>,
}
impl WorkspaceConfig {
pub fn load(dir: &Path) -> Result<WorkspaceConfig, Error> {
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<Config, Error> {
pub fn load(dir: &Path) -> Result<ProjectConfig, Error> {
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"));
}

View File

@ -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<T>(event_listener: &T, root_path: &Path, config: &Config) -> Result<Manifest, Error>
pub fn download<T>(event_listener: &T, root_path: &Path, config: &ProjectConfig) -> Result<Manifest, Error>
where
T: EventListener,
{

View File

@ -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<T>(
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<T>(config: &Config, event_listener: &T) -> Result<Manifest, Error>
fn resolve_versions<T>(config: &ProjectConfig, event_listener: &T) -> Result<Manifest, Error>
where
T: EventListener,
{

View File

@ -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<DocFile> {
pub fn generate_all(root: &Path, config: &ProjectConfig, modules: Vec<&CheckedModule>) -> Vec<DocFile> {
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<SearchIndex>) -> Vec<DocFile> {
fn generate_readme(
root: &Path,
config: &Config,
config: &ProjectConfig,
modules: &[DocLink],
source: &DocLink,
timestamp: &Duration,

View File

@ -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

View File

@ -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"),
}
}
}

View File

@ -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<T>
where
T: EventListener,
{
config: Config,
config: ProjectConfig,
defined_modules: HashMap<String, PathBuf>,
checked_modules: CheckedModules,
id_gen: IdGenerator,
@ -108,7 +108,7 @@ where
T: EventListener,
{
pub fn new(root: PathBuf, event_listener: T) -> Result<Project<T>, 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<T> {
pub fn new_with_config(config: ProjectConfig, root: PathBuf, event_listener: T) -> Project<T> {
let id_gen = IdGenerator::new();
let mut module_types = HashMap::new();

View File

@ -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,6 +108,38 @@ where
current_dir
};
let mut warnings = Vec::new();
let mut errs: Vec<crate::error::Error> = 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::<Result<Vec<Project<_>>, 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) => {
@ -118,7 +150,15 @@ where
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,17 +187,15 @@ where
return Err(ExitFailure::into_report());
}
if project.checks_count.unwrap_or_default() + warning_count > 0 {
eprintln!(
"{}",
Summary {
check_count: project.checks_count,
check_count,
error_count: 0,
warning_count
}
);
}
}
if warning_count > 0 && deny {
Err(ExitFailure::into_report())
@ -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(())

View File

@ -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()?;

View File

@ -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();