Merge pull request #259 from aiken-lang/basic-add-upgrade-packages

Basic commands for manipulating the aiken.toml
This commit is contained in:
Lucas 2023-01-14 22:03:32 -05:00 committed by GitHub
commit 8ac2d75ef1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 365 additions and 209 deletions

1
Cargo.lock generated
View File

@ -119,6 +119,7 @@ dependencies = [
"ignore",
"itertools",
"miette",
"owo-colors",
"pallas",
"pallas-traverse",
"petgraph",

View File

@ -18,6 +18,7 @@ hex = "0.4.3"
ignore = "0.4.18"
itertools = "0.10.1"
miette = { version = "5.3.0", features = ["fancy"] }
owo-colors = "3.5.0"
pallas = "0.16.0"
pallas-traverse = "0.16.0"
petgraph = "0.6.2"

View File

@ -1,13 +1,14 @@
use crate::error::Error;
use crate::{package_name::PackageName, Error};
use aiken_lang::ast::Span;
use miette::NamedSource;
use serde::{de::Visitor, Deserialize, Serialize};
use std::{fmt::Display, fs, path::PathBuf};
use serde::{Deserialize, Serialize};
use std::{fmt::Display, fs, io, path::Path};
#[derive(Deserialize)]
#[derive(Deserialize, Serialize)]
pub struct Config {
pub name: PackageName,
pub version: String,
pub license: Option<String>,
#[serde(default)]
pub description: String,
pub repository: Option<Repository>,
@ -15,7 +16,7 @@ pub struct Config {
pub dependencies: Vec<Dependency>,
}
#[derive(Deserialize)]
#[derive(Deserialize, Serialize)]
pub struct Repository {
pub user: String,
pub project: String,
@ -37,67 +38,6 @@ pub struct Dependency {
pub source: Platform,
}
#[derive(PartialEq, Eq, Hash, Clone)]
pub struct PackageName {
pub owner: String,
pub repo: String,
}
impl Display for PackageName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}/{}", self.owner, self.repo)
}
}
impl Serialize for PackageName {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for PackageName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct PackageNameVisitor;
impl<'de> Visitor<'de> for PackageNameVisitor {
type Value = PackageName;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter
.write_str("a string representing an owner and repo, ex: aiken-lang/stdlib")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let mut name = v.split('/');
let owner = name.next().ok_or_else(|| {
serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self)
})?;
let repo = name.next().ok_or_else(|| {
serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self)
})?;
Ok(PackageName {
owner: owner.to_string(),
repo: repo.to_string(),
})
}
}
deserializer.deserialize_str(PackageNameVisitor)
}
}
impl Display for Platform {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::result::Result<(), ::std::fmt::Error> {
match *self {
@ -109,10 +49,39 @@ impl Display for Platform {
}
impl Config {
pub fn load(dir: PathBuf) -> Result<Config, Error> {
pub fn default(name: &PackageName) -> Self {
Config {
name: name.clone(),
version: "0.0.0".to_string(),
license: Some("Apache-2.0".to_string()),
description: format!("Aiken contracts for project '{name}'"),
repository: Some(Repository {
user: name.owner.clone(),
project: name.repo.clone(),
platform: Platform::Github,
}),
dependencies: vec![Dependency {
name: PackageName {
owner: "aiken-lang".to_string(),
repo: "stdlib".to_string(),
},
version: "main".to_string(),
source: Platform::Github,
}],
}
}
pub fn save(&self, dir: &Path) -> Result<(), io::Error> {
let aiken_toml_path = dir.join("aiken.toml");
let aiken_toml = toml::to_string_pretty(self).unwrap();
fs::write(aiken_toml_path, aiken_toml)
}
pub fn load(dir: &Path) -> Result<Config, Error> {
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(),
@ -128,4 +97,19 @@ impl Config {
Ok(result)
}
pub fn insert(mut self, dependency: &Dependency, and_replace: bool) -> Option<Self> {
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)
}
}

View File

@ -6,8 +6,9 @@ use serde::{Deserialize, Serialize};
use tokio::time::Instant;
use crate::{
config::{Config, Dependency, PackageName},
config::{Config, Dependency},
error::Error,
package_name::PackageName,
paths,
telemetry::{Event, EventListener},
};

View File

@ -4,8 +4,8 @@ use futures::future;
use reqwest::Client;
use crate::{
config::PackageName,
error::Error,
package_name::PackageName,
paths::{self, CacheKey},
};

View File

@ -5,8 +5,9 @@ use miette::NamedSource;
use serde::{Deserialize, Serialize};
use crate::{
config::{Config, Dependency, PackageName, Platform},
config::{Config, Dependency, Platform},
error::Error,
package_name::PackageName,
paths,
telemetry::{Event, EventListener},
};

View File

@ -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,
@ -264,10 +264,10 @@ impl Diagnostic for Error {
Error::ValidatorMustReturnBool { .. } => Some(Box::new("aiken::scripts")),
Error::WrongValidatorArity { .. } => Some(Box::new("aiken::validators")),
Error::TestFailure { path, .. } => Some(Box::new(path.to_str().unwrap_or(""))),
Error::Http(_) => Some(Box::new("aiken::deps")),
Error::Http(_) => Some(Box::new("aiken::packages::download")),
Error::ZipExtract(_) => None,
Error::JoinError(_) => None,
Error::UnknownPackageVersion { .. } => Some(Box::new("aiken::deps")),
Error::UnknownPackageVersion { .. } => Some(Box::new("aiken::packages::resolve")),
}
}
@ -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::packages::already_exists"))
}
}
}
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
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.",
)),
}
}
}

View File

@ -5,6 +5,7 @@ pub mod error;
pub mod format;
pub mod module;
pub mod options;
pub mod package_name;
pub mod paths;
pub mod pretty;
pub mod script;
@ -18,10 +19,10 @@ use aiken_lang::{
uplc::CodeGenerator,
IdGenerator,
};
use config::PackageName;
use deps::UseManifest;
use miette::NamedSource;
use options::{CodeGenMode, Options};
use package_name::PackageName;
use pallas::{
codec::minicbor,
ledger::{addresses::Address, primitives::babbage},
@ -85,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,

View File

@ -0,0 +1,143 @@
use owo_colors::OwoColorize;
use serde::{de::Visitor, Deserialize, Serialize};
use std::{
fmt::{self, Display},
str::FromStr,
};
use thiserror::Error;
#[derive(PartialEq, Eq, Hash, Clone)]
pub struct PackageName {
pub owner: String,
pub repo: String,
}
impl PackageName {
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: self.to_string(),
});
}
Ok(())
}
}
impl FromStr for PackageName {
type Err = Error;
fn from_str(name: &str) -> Result<Self, Error> {
let mut name_split = name.split('/');
let owner = name_split
.next()
.ok_or_else(|| Error::InvalidProjectName {
name: name.to_string(),
reason: InvalidProjectNameReason::Format,
})?
.to_string();
let repo = name_split
.next()
.ok_or_else(|| Error::InvalidProjectName {
name: name.to_string(),
reason: InvalidProjectNameReason::Format,
})?
.to_string();
let package_name = PackageName { owner, repo };
package_name.validate()?;
Ok(package_name)
}
}
impl Display for PackageName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}/{}", self.owner, self.repo)
}
}
impl Serialize for PackageName {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for PackageName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct PackageNameVisitor;
impl<'de> Visitor<'de> for PackageNameVisitor {
type Value = PackageName;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter
.write_str("a string representing an owner and repo, ex: aiken-lang/stdlib")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let mut name = v.split('/');
let owner = name.next().ok_or_else(|| {
serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self)
})?;
let repo = name.next().ok_or_else(|| {
serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self)
})?;
Ok(PackageName {
owner: owner.to_string(),
repo: repo.to_string(),
})
}
}
deserializer.deserialize_str(PackageNameVisitor)
}
}
#[derive(Debug, Error, miette::Diagnostic)]
pub enum Error {
#[error("{} is not a valid project name: {}", name.red(), reason.to_string())]
InvalidProjectName {
name: String,
reason: InvalidProjectNameReason,
},
#[error("A project named {} already exists.", name.red())]
ProjectExists { name: String },
}
#[derive(Debug, Clone, Copy)]
pub enum InvalidProjectNameReason {
Reserved,
Format,
}
impl fmt::Display for InvalidProjectNameReason {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
InvalidProjectNameReason::Reserved => write!(f, "It's a reserved word in Aiken."),
InvalidProjectNameReason::Format => write!(
f,
"It is malformed.\n\nProjects must be named as:\n\n\t\
{}/{}\n\nEach part must start with a lowercase letter \
and may only contain lowercase letters, numbers, hyphens or underscores.\
\nFor example,\n\n\t{}",
"{owner}".bright_blue(),
"{project}".bright_blue(),
"aiken-lang/stdlib".bright_blue(),
),
}
}
}

View File

@ -1,5 +1,5 @@
use crate::deps::manifest::Package;
use crate::{config::PackageName, error::Error};
use crate::{error::Error, package_name::PackageName};
use reqwest::Client;
use std::path::PathBuf;

View File

@ -1,43 +0,0 @@
use std::fmt;
use owo_colors::OwoColorize;
use thiserror::Error;
#[derive(Debug, Error, miette::Diagnostic)]
pub enum Error {
#[error("{} is not a valid project name: {}", name.red(), reason.to_string())]
InvalidProjectName {
name: String,
reason: InvalidProjectNameReason,
},
#[error("A project named {} already exists.", name.red())]
ProjectExists { name: String },
}
#[derive(Debug, Clone, Copy)]
pub enum InvalidProjectNameReason {
AikenPrefix,
AikenReservedModule,
Format,
}
impl fmt::Display for InvalidProjectNameReason {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
InvalidProjectNameReason::AikenPrefix => write!(f, "it's a reserved word in Aiken."),
InvalidProjectNameReason::AikenReservedModule => {
write!(f, "it's a reserved module name in Aiken.")
}
InvalidProjectNameReason::Format => write!(
f,
"it is malformed.\n\nProjects must be named as:\n\n\t\
{}/{}\n\nEach part must start with a lowercase letter \
and may only contain lowercase letters, numbers, hyphens or underscores.\
\nFor example,\n\n\t{}",
"{owner}".bright_blue(),
"{project}".bright_blue(),
"aiken-lang/stdlib".bright_blue(),
),
}
}
}

View File

@ -1,10 +1,9 @@
pub mod build;
pub mod check;
pub mod deps;
pub mod docs;
pub mod error;
pub mod fmt;
pub mod lsp;
pub mod new;
pub mod packages;
pub mod tx;
pub mod uplc;

View File

@ -1,11 +1,15 @@
use aiken_project::config::PackageName;
use aiken_project::{
config::Config,
package_name::{self, PackageName},
};
use indoc::{formatdoc, indoc};
use miette::IntoDiagnostic;
use owo_colors::OwoColorize;
use std::path::PathBuf;
use std::{fs, path::Path};
use super::error::{Error, InvalidProjectNameReason};
use std::{
fs,
path::{Path, PathBuf},
str::FromStr,
};
#[derive(clap::Args)]
/// Create a new Aiken project
@ -18,14 +22,9 @@ pub struct Args {
}
pub fn exec(args: Args) -> miette::Result<()> {
validate_name(&args.name)?;
let package_name = package_name_from_str(&args.name)?;
let package_name = PackageName::from_str(&args.name).into_diagnostic()?;
create_project(args, &package_name)?;
print_success_message(&package_name);
Ok(())
}
@ -33,7 +32,7 @@ fn create_project(args: Args, package_name: &PackageName) -> miette::Result<()>
let root = PathBuf::from(&package_name.repo);
if root.exists() {
Err(Error::ProjectExists {
Err(package_name::Error::ProjectExists {
name: package_name.repo.clone(),
})?;
}
@ -44,10 +43,12 @@ fn create_project(args: Args, package_name: &PackageName) -> miette::Result<()>
create_validators_folder(&root)?;
}
aiken_toml(&root, package_name)?;
readme(&root, &package_name.repo)?;
Config::default(package_name)
.save(&root)
.into_diagnostic()?;
gitignore(&root)?;
Ok(())
@ -85,28 +86,6 @@ fn create_validators_folder(root: &Path) -> miette::Result<()> {
Ok(())
}
fn aiken_toml(root: &Path, package_name: &PackageName) -> miette::Result<()> {
let aiken_toml_path = root.join("aiken.toml");
fs::write(
aiken_toml_path,
formatdoc! {
r#"
name = "{name}"
version = "0.0.0"
licences = ["Apache-2.0"]
description = "Aiken contracts for project '{name}'"
dependencies = [
{{ name = "aiken-lang/stdlib", version = "main", source = "github" }},
]
"#,
name = package_name.to_string(),
},
)
.into_diagnostic()
}
fn readme(root: &Path, project_name: &str) -> miette::Result<()> {
fs::write(
root.join("README.md"),
@ -198,49 +177,3 @@ fn gitignore(root: &Path) -> miette::Result<()> {
Ok(())
}
fn validate_name(name: &str) -> Result<(), Error> {
if name.starts_with("aiken_") {
Err(Error::InvalidProjectName {
name: name.to_string(),
reason: InvalidProjectNameReason::AikenPrefix,
})
} else if name == "aiken" {
Err(Error::InvalidProjectName {
name: name.to_string(),
reason: InvalidProjectNameReason::AikenReservedModule,
})
} else if !regex::Regex::new("^[a-z0-9_-]+/[a-z0-9_-]+$")
.expect("regex could not be compiled")
.is_match(name)
{
Err(Error::InvalidProjectName {
name: name.to_string(),
reason: InvalidProjectNameReason::Format,
})
} else {
Ok(())
}
}
fn package_name_from_str(name: &str) -> Result<PackageName, Error> {
let mut name_split = name.split('/');
let owner = name_split
.next()
.ok_or_else(|| Error::InvalidProjectName {
name: name.to_string(),
reason: InvalidProjectNameReason::Format,
})?
.to_string();
let repo = name_split
.next()
.ok_or_else(|| Error::InvalidProjectName {
name: name.to_string(),
reason: InvalidProjectNameReason::Format,
})?
.to_string();
Ok(PackageName { owner, repo })
}

View File

@ -0,0 +1,78 @@
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.
pub package: String,
/// The package version, as a git commit hash, a tag or a branch name.
#[clap(long)]
pub version: String,
#[clap(hide = true)]
pub overwrite: bool,
}
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("Package".to_string(), 13, " ")
.bold()
.purple(),
dependency.name.bright_blue(),
);
match config.insert(&dependency, args.overwrite) {
Some(config) => {
config.save(&root).into_diagnostic()?;
println!(
"{} version → {}",
pretty::pad_left(
if args.overwrite { "Changed" } else { "Added" }.to_string(),
13,
" "
)
.bold()
.purple(),
dependency.version.yellow()
);
Ok(())
}
None => {
let warning = Warning::DependencyAlreadyExists {
name: dependency.name,
};
warning.report();
process::exit(1)
}
}
}

View File

@ -1,4 +1,6 @@
pub mod add;
pub mod clear_cache;
pub mod upgrade;
use clap::Subcommand;
@ -6,12 +8,20 @@ use clap::Subcommand;
#[derive(Subcommand)]
#[clap(setting(clap::AppSettings::DeriveDisplayOrder))]
pub enum Cmd {
/// Add a new package dependency
Add(add::Args),
/// Change the version of an installed dependency
Upgrade(upgrade::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(),
Cmd::Upgrade(args) => upgrade::exec(args),
}
}

View File

@ -0,0 +1,24 @@
use super::add;
#[derive(clap::Args)]
/// Change the version of an installed 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<()> {
add::exec(add::Args {
package: args.package,
version: args.version,
overwrite: true,
})
}

View File

@ -1,4 +1,8 @@
use aiken::cmd::{build, check, deps, docs, fmt, lsp, new, tx, uplc};
use aiken::cmd::{
build, check, docs, fmt, lsp, new,
packages::{self, add},
tx, uplc,
};
use clap::Parser;
/// Aiken: a smart-contract language and toolchain for Cardano
@ -12,9 +16,10 @@ pub enum Cmd {
Build(build::Args),
Check(check::Args),
Docs(docs::Args),
Add(add::Args),
#[clap(subcommand)]
Deps(deps::Cmd),
Packages(packages::Cmd),
#[clap(subcommand)]
Tx(tx::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::Packages(args) => packages::exec(args),
Cmd::Lsp(args) => lsp::exec(args),
Cmd::Tx(sub_cmd) => tx::exec(sub_cmd),
Cmd::Uplc(sub_cmd) => uplc::exec(sub_cmd),