diff --git a/crates/aiken-project/src/config.rs b/crates/aiken-project/src/config.rs index 83d90f45..956dca25 100644 --- a/crates/aiken-project/src/config.rs +++ b/crates/aiken-project/src/config.rs @@ -2,17 +2,13 @@ use crate::{package_name::PackageName, Error}; use aiken_lang::ast::Span; use miette::NamedSource; use serde::{Deserialize, Serialize}; -use std::{ - fmt::Display, - fs, io, - path::{Path, PathBuf}, -}; +use std::{fmt::Display, fs, io, path::Path}; #[derive(Deserialize, Serialize)] pub struct Config { pub name: PackageName, pub version: String, - pub license: String, + pub license: Option, #[serde(default)] pub description: String, pub repository: Option, @@ -57,7 +53,7 @@ impl Config { Config { name: name.clone(), version: "0.0.0".to_string(), - license: "Apache-2.0".to_string(), + license: Some("Apache-2.0".to_string()), description: format!("Aiken contracts for project '{name}'"), repository: Some(Repository { user: name.owner.clone(), @@ -81,10 +77,11 @@ impl Config { fs::write(aiken_toml_path, aiken_toml) } - pub fn load(dir: PathBuf) -> Result { + pub fn load(dir: &Path) -> Result { let config_path = dir.join("aiken.toml"); - let raw_config = fs::read_to_string(&config_path) - .map_err(|_| Error::MissingManifest { path: dir.clone() })?; + 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 { path: config_path.clone(), @@ -100,4 +97,19 @@ impl Config { Ok(result) } + + pub fn insert(mut self, dependency: &Dependency, and_replace: bool) -> Option { + for mut existing in self.dependencies.iter_mut() { + if existing.name == dependency.name { + return if and_replace { + existing.version = dependency.version.clone(); + Some(self) + } else { + None + }; + } + } + self.dependencies.push(dependency.clone()); + Some(self) + } } diff --git a/crates/aiken-project/src/error.rs b/crates/aiken-project/src/error.rs index b0b5c7ab..35e2c8f6 100644 --- a/crates/aiken-project/src/error.rs +++ b/crates/aiken-project/src/error.rs @@ -1,4 +1,4 @@ -use crate::{deps::manifest::Package, pretty, script::EvalHint}; +use crate::{deps::manifest::Package, package_name::PackageName, pretty, script::EvalHint}; use aiken_lang::{ ast::{BinOp, Span}, parser::error::ParseError, @@ -418,6 +418,8 @@ pub enum Warning { #[source] warning: tipo::error::Warning, }, + #[error("{name} is already a dependency.")] + DependencyAlreadyExists { name: PackageName }, } impl Diagnostic for Warning { @@ -429,6 +431,7 @@ impl Diagnostic for Warning { match self { Warning::Type { named, .. } => Some(named), Warning::NoValidators => None, + Warning::DependencyAlreadyExists { .. } => None, } } @@ -436,6 +439,7 @@ impl Diagnostic for Warning { match self { Warning::Type { warning, .. } => warning.labels(), Warning::NoValidators => None, + Warning::DependencyAlreadyExists { .. } => None, } } @@ -443,6 +447,19 @@ impl Diagnostic for Warning { match self { Warning::Type { .. } => Some(Box::new("aiken::check")), Warning::NoValidators => Some(Box::new("aiken::check")), + Warning::DependencyAlreadyExists { .. } => { + Some(Box::new("aiken::deps::already_exists")) + } + } + } + + fn help<'a>(&'a self) -> Option> { + match self { + Warning::Type { .. } => None, + Warning::NoValidators => None, + Warning::DependencyAlreadyExists { .. } => Some(Box::new( + "If you need to change the version, try 'aiken packages upgrade' instead.", + )), } } } diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index 89c572d0..0e161666 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -86,7 +86,7 @@ where module_types.insert("aiken".to_string(), builtins::prelude(&id_gen)); module_types.insert("aiken/builtin".to_string(), builtins::plutus(&id_gen)); - let config = Config::load(root.clone())?; + let config = Config::load(&root)?; Ok(Project { config, diff --git a/crates/aiken-project/src/package_name.rs b/crates/aiken-project/src/package_name.rs index c468cc0d..0454de9b 100644 --- a/crates/aiken-project/src/package_name.rs +++ b/crates/aiken-project/src/package_name.rs @@ -13,22 +13,24 @@ pub struct PackageName { } impl PackageName { - fn validate(&self) -> Result<(), Error> { - let name = format!("{}/{}", self.owner, self.repo); - + pub fn restrict(&self) -> Result<(), Error> { if self.owner.starts_with("aiken") { return Err(Error::InvalidProjectName { reason: InvalidProjectNameReason::Reserved, - name, + name: self.to_string(), }); } + Ok(()) + } + + fn validate(&self) -> Result<(), Error> { let r = regex::Regex::new("^[a-z0-9_-]+$").expect("regex could not be compiled"); if !(r.is_match(&self.owner) && r.is_match(&self.repo)) { return Err(Error::InvalidProjectName { reason: InvalidProjectNameReason::Format, - name, + name: self.to_string(), }); } diff --git a/crates/aiken/src/cmd/deps/add.rs b/crates/aiken/src/cmd/deps/add.rs new file mode 100644 index 00000000..964ab495 --- /dev/null +++ b/crates/aiken/src/cmd/deps/add.rs @@ -0,0 +1,71 @@ +use aiken_project::{ + config::{Config, Dependency, Platform}, + error::Warning, + package_name::PackageName, + pretty, +}; +use miette::IntoDiagnostic; +use owo_colors::OwoColorize; +use std::{path::PathBuf, process, str::FromStr}; + +#[derive(clap::Args)] +/// Add a new project package as dependency +pub struct Args { + /// Package name, in the form of {owner}/{repository}. + /// + /// For example → 'add aiken-lang/stdlib' + /// + /// Note that by default, this assumes the package is located + /// on Github. + package: String, + /// The package version, as a git commit hash, a tag or a branch name. + #[clap(long)] + version: String, +} + +pub fn exec(args: Args) -> miette::Result<()> { + let root = PathBuf::from("."); + + let dependency = Dependency { + name: PackageName::from_str(&args.package)?, + version: args.version, + source: Platform::Github, + }; + + let config = match Config::load(&root) { + Ok(config) => config, + Err(e) => { + e.report(); + process::exit(1); + } + }; + + println!( + "{} {}", + pretty::pad_left("Adding".to_string(), 13, " ") + .bold() + .purple(), + dependency.name.bright_blue(), + ); + + match config.insert(&dependency, false) { + Some(config) => { + config.save(&root).into_diagnostic()?; + println!( + "{} version = {}", + pretty::pad_left("Added".to_string(), 13, " ") + .bold() + .purple(), + dependency.version.yellow() + ); + Ok(()) + } + None => { + let warning = Warning::DependencyAlreadyExists { + name: dependency.name, + }; + warning.report(); + process::exit(1) + } + } +} diff --git a/crates/aiken/src/cmd/deps/mod.rs b/crates/aiken/src/cmd/deps/mod.rs index cc35df18..3001d7f6 100644 --- a/crates/aiken/src/cmd/deps/mod.rs +++ b/crates/aiken/src/cmd/deps/mod.rs @@ -1,3 +1,4 @@ +pub mod add; pub mod clear_cache; use clap::Subcommand; @@ -6,12 +7,16 @@ use clap::Subcommand; #[derive(Subcommand)] #[clap(setting(clap::AppSettings::DeriveDisplayOrder))] pub enum Cmd { + /// Add a new dependency + Add(add::Args), + /// Clear the system-wide dependencies cache ClearCache, } pub fn exec(cmd: Cmd) -> miette::Result<()> { match cmd { + Cmd::Add(args) => add::exec(args), Cmd::ClearCache => clear_cache::exec(), } } diff --git a/crates/aiken/src/cmd/new.rs b/crates/aiken/src/cmd/new.rs index b9c24e7d..fb08bdc5 100644 --- a/crates/aiken/src/cmd/new.rs +++ b/crates/aiken/src/cmd/new.rs @@ -29,6 +29,8 @@ pub fn exec(args: Args) -> miette::Result<()> { } fn create_project(args: Args, package_name: &PackageName) -> miette::Result<()> { + package_name.restrict().into_diagnostic()?; + let root = PathBuf::from(&package_name.repo); if root.exists() { diff --git a/crates/aiken/src/main.rs b/crates/aiken/src/main.rs index ebb977eb..1876abba 100644 --- a/crates/aiken/src/main.rs +++ b/crates/aiken/src/main.rs @@ -1,4 +1,8 @@ -use aiken::cmd::{build, check, deps, docs, fmt, lsp, new, tx, uplc}; +use aiken::cmd::{ + build, check, + deps::{self, add}, + docs, fmt, lsp, new, tx, uplc, +}; use clap::Parser; /// Aiken: a smart-contract language and toolchain for Cardano @@ -12,6 +16,7 @@ pub enum Cmd { Build(build::Args), Check(check::Args), Docs(docs::Args), + Add(add::Args), #[clap(subcommand)] Deps(deps::Cmd), @@ -38,9 +43,10 @@ fn main() -> miette::Result<()> { Cmd::New(args) => new::exec(args), Cmd::Fmt(args) => fmt::exec(args), Cmd::Build(args) => build::exec(args), - Cmd::Docs(args) => docs::exec(args), - Cmd::Deps(args) => deps::exec(args), Cmd::Check(args) => check::exec(args), + Cmd::Docs(args) => docs::exec(args), + Cmd::Add(args) => add::exec(args), + Cmd::Deps(args) => deps::exec(args), Cmd::Lsp(args) => lsp::exec(args), Cmd::Tx(sub_cmd) => tx::exec(sub_cmd), Cmd::Uplc(sub_cmd) => uplc::exec(sub_cmd),