Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [Unreleased]

- Extended `simplex install` to accept dependency arguments (`simplex install <dep>` or `simplex install <alias>=<dep>`).

## [0.0.8]

- Fixed many issues in the sdk crate regarding the Liquid mainnet network:
Expand Down
15 changes: 8 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ Simplex CLI provides the following commands:

- `simplex init` - Initializes a new Simplex project.
- `simplex config` - Prints the current config.
- `simplex install` - Installs specified SimplicityHL dependencies.
- `simplex install <dep> <dep>` - Installs SimplicityHL dependencies. Without a `<dep>` provided, installs everything listed in the `[dependencies]` config section. With one or more `<dep>` arguments, appends new entries to the config and then installs everything.
- `simplex build` - Generates simplicity artifacts.
- `simplex regtest` - Spins up local Electrs + Elements nodes.
- `simplex test` - Runs Simplex tests.
Expand Down
1 change: 1 addition & 0 deletions crates/build/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ pathdiff = { version = "0.2.3" }
prettyplease = { version = "0.2.37" }
glob = { version = "0.3.3"}
globwalk = { version = "0.9.1"}
toml_edit = "0.25.12"
80 changes: 78 additions & 2 deletions crates/build/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
use std::collections::HashMap;
use std::path::Path;

use toml_edit::{DocumentMut, InlineTable, Item, Value};

use serde::Deserialize;

use crate::dep_spec::DepSpec;
use crate::dep_spec::Source;
use crate::error::TomlEditError;

use super::error::BuildError;
use super::error::DependencyValidationError;

// Default values for optional [build] fields.
pub const DEFAULT_OUT_DIR_NAME: &str = "src/artifacts";
pub const DEFAULT_INCLUDE_PATH: &str = "**/*.simf";
pub const DEFAULT_SRC_DIR_NAME: &str = "simf";
pub const DEFAULT_DEPENDENCY_DIR: &str = "deps";

// TOML section names.
pub const BUILD_SECTION: &str = "build";
pub const DEPENDENCIES_SECTION: &str = "dependencies";

#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct BuildConfig {
Expand Down Expand Up @@ -43,7 +55,7 @@ impl BuildConfig {
pub fn from_source(content: &str) -> Result<Self, BuildError> {
let table: toml::Table = toml::from_str(content)?;

match table.get("build") {
match table.get(BUILD_SECTION) {
Some(section) => Ok(section.clone().try_into()?),
None => Ok(Self::default()),
}
Expand All @@ -68,7 +80,7 @@ impl DependencyConfig {
/// (empty) config. Each dependency is validated to declare exactly one source.
pub fn from_source(content: &str) -> Result<Self, BuildError> {
let table: toml::Table = toml::from_str(content)?;
let res: Self = match table.get("dependencies") {
let res: Self = match table.get(DEPENDENCIES_SECTION) {
Some(section) => section.clone().try_into()?,
None => Self::default(),
};
Expand All @@ -78,6 +90,70 @@ impl DependencyConfig {
Ok(res)
}

/// Appends new entries to the `[dependencies]` table of the config file at `path`,
/// preserving existing formatting and comments.
///
/// # Errors
/// - `TomlEditError::MalformedDep`: If an entry in `deps` cannot be parsed as
/// `<source>` or `<alias>=<source>`.
/// - `TomlEditError::UnableToEdit`: If the file at `path` cannot be parsed as TOML
/// for editing.
/// - `TomlEditError::MalformedDependenciesTable`: If `[dependencies]` exists but
/// is not a TOML table.
/// - `TomlEditError::DuplicateAlias`: If an alias appears twice in `deps`, or
/// already exists in `[dependencies]`.
/// - Any other I/O errors that may occur when reading or writing the file.
pub fn add_dependency_to(path: &Path, deps: &[String]) -> Result<(), TomlEditError> {
if deps.is_empty() {
return Ok(());
}

let specs: Vec<DepSpec> = deps
.iter()
.map(|raw| DepSpec::parse_dep(raw))
.collect::<Result<_, _>>()?;

let raw = std::fs::read_to_string(path)?;
let mut doc: DocumentMut = raw.parse().map_err(|source| TomlEditError::UnableToEdit {
path: path.to_path_buf(),
source,
})?;

let deps_table = doc
.entry(DEPENDENCIES_SECTION)
.or_insert(Item::Table(toml_edit::Table::new()))
.as_table_mut()
.ok_or(TomlEditError::MalformedDependenciesTable)?;

// Batches are small (typically <=10), so a linear scan over a Vec is cheaper
// than the constant overhead of a HashSet.
let mut seen_in_batch: Vec<&str> = Vec::with_capacity(specs.len());
for spec in &specs {
if seen_in_batch.contains(&spec.alias.as_str()) || deps_table.contains_key(&spec.alias) {
return Err(TomlEditError::DuplicateAlias(spec.alias.clone()));
}
seen_in_batch.push(spec.alias.as_str());
}

for spec in &specs {
let mut inline = InlineTable::new();
match &spec.source {
Source::Git(url) => {
inline.insert("git", Value::from(url.as_str()));
}
Source::Path(p) => {
inline.insert("path", Value::from(p.as_str()));
}
}
deps_table.insert(&spec.alias, Item::Value(Value::InlineTable(inline)));
}

std::fs::write(path, doc.to_string())?;
println!("Added: {}", DepSpec::format_batch(&specs));

Ok(())
}

pub fn validate(&self) -> Result<(), DependencyValidationError> {
for (dep_name, dep) in &self.inner {
match (&dep.path, &dep.git) {
Expand Down
102 changes: 102 additions & 0 deletions crates/build/src/dep_spec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use std::fmt::Write;

use super::error::TomlEditError;

pub struct DepSpec {
pub alias: String,
pub source: Source,
}

pub enum Source {
Git(String),
Path(String),
}

impl DepSpec {
/// Parses a raw CLI token into a [`DepSpec`].
///
/// Accepted forms:
/// - `<source>`: The alias is derived from the last path segment of the source,
/// with a trailing `.git` stripped.
/// - `<alias>=<source>`: Both parts must be non-empty.
///
/// The source is then classified as [`Source::Git`] or [`Source::Path`] based on
/// its scheme or `.git` suffix.
///
/// # Errors
/// - `TomlEditError::MalformedDep`: If `raw` contains `=` but either side is empty,
/// or if the alias cannot be derived from the source (e.g. the source contains
/// no non-empty path segment).
pub fn parse_dep(raw: &str) -> Result<DepSpec, TomlEditError> {
let (alias, source_str) = match raw.split_once('=') {
Some((a, s)) if !a.is_empty() && !s.is_empty() => (a.to_owned(), s),
Some(_) => return Err(TomlEditError::MalformedDep(raw.to_owned())),
None => (Self::derive_alias(raw)?, raw),
};

let source = Self::classify_source(source_str);

Ok(DepSpec { alias, source })
}

/// Formats a batch of dependency specs as a bracketed, one-per-line list.
#[must_use]
pub fn format_batch(specs: &[DepSpec]) -> String {
if specs.is_empty() {
return "[]".to_owned();
}

let mut out = String::from("[");

for (index, spec) in specs.iter().enumerate() {
if index > 0 {
out.push(',');
}

let source = match &spec.source {
Source::Git(url) => url.as_str(),
Source::Path(p) => p.as_str(),
};
let _ = write!(out, "\n {} = {}", spec.alias, source);
}

out.push_str("\n]");

out
}

/// Derives a default alias from a source string by taking its last non-empty
/// path segment and stripping a trailing `.git`.
///
/// # Errors
/// - `BuildError::MalformedDep`: If `source` contains no non-empty path segment
/// (e.g. an empty string or one consisting only of separators).
fn derive_alias(source: &str) -> Result<String, TomlEditError> {
let last = source
.rsplit(['/', '\\'])
.find(|s| !s.is_empty())
.ok_or_else(|| TomlEditError::MalformedDep(source.to_owned()))?;

Ok(last.trim_end_matches(".git").to_owned())
}

/// Classifies a source string as [`Source::Git`] if it carries a recognised
/// scheme (`http`, `https`, `git`, `ssh`) or ends in `.git`, otherwise as
/// [`Source::Path`]. The `.git` check is case-insensitive.
fn classify_source(s: &str) -> Source {
let git_ext = std::path::Path::new(s)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("git"));

if s.starts_with("http://")
|| s.starts_with("https://")
|| s.starts_with("git://")
|| s.starts_with("ssh://")
|| git_ext
{
Source::Git(s.to_owned())
} else {
Source::Path(s.to_owned())
}
}
}
25 changes: 25 additions & 0 deletions crates/build/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,28 @@ pub enum DependencyValidationError {
Conflicting(String),
}

/// Errors produced while editing `Simplex.toml` to add or modify a dependency.
#[derive(thiserror::Error, Debug)]
pub enum TomlEditError {
#[error("IO error: {0}")]
Io(#[from] io::Error),

#[error("failed to parse `{path}` for editing: {source}")]
UnableToEdit {
path: PathBuf,
source: toml_edit::TomlError,
},

#[error("`[dependencies]` in `Simplex.toml` is not a table")]
MalformedDependenciesTable,

#[error("malformed dependency spec `{0}` (expected `<source>` or `<alias>=<source>`)")]
MalformedDep(String),

#[error("dependency `{0}` already exists")]
DuplicateAlias(String),
}

#[derive(thiserror::Error, Debug)]
pub enum BuildError {
#[error("IO error: {0}")]
Expand Down Expand Up @@ -61,4 +83,7 @@ pub enum BuildError {

#[error("Invalid git repository URL: '{0}'")]
InvalidGitUrl(String),

#[error(transparent)]
TomlEdit(#[from] TomlEditError),
}
1 change: 1 addition & 0 deletions crates/build/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod collector;
pub mod config;
pub mod dep_spec;
pub mod error;
pub mod generator;
pub mod macros;
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ minreq = { workspace = true }
anyhow = "1"
dotenvy = "0.15"
clap = { version = "4", features = ["derive", "env"] }
toml_edit = { version = "0.23.9" }
toml_edit = "0.25.12"
Comment thread
LesterEvSe marked this conversation as resolved.
Outdated
ctrlc = { version = "3.5.2", features = ["termination"] }
serde_json = { version = "1.0.149" }
6 changes: 5 additions & 1 deletion crates/cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use std::path::PathBuf;

use clap::Parser;

use smplx_build::DependencyConfig;

use crate::commands::Command;
use crate::commands::build::Build;
use crate::commands::clean::Clean;
Expand All @@ -10,6 +12,7 @@ use crate::commands::install::Install;
use crate::commands::regtest::Regtest;
use crate::commands::test::Test;
use crate::config::Config;
use crate::config::error::ConfigError;
use crate::error::CliError;

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -67,8 +70,9 @@ impl Cli {

Ok(Regtest::run(&loaded_config.regtest)?)
}
Command::Install => {
Command::Install { deps } => {
let config_path = Config::get_default_path()?;
DependencyConfig::add_dependency_to(&config_path, deps).map_err(ConfigError::from)?;
let loaded_config = Config::load(config_path)?;

Ok(Install::run(&loaded_config.dependencies)?)
Expand Down
7 changes: 6 additions & 1 deletion crates/cli/src/commands/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ pub enum Command {
flags: TestFlags,
},
/// Install a `SimplicityHL` dependency (requires the dep to be a simplex project)
Comment thread
LesterEvSe marked this conversation as resolved.
Install,
/// If `deps` is empty, install everything from `Simplex.toml`.
Install {
/// Dependencies to install, as `<source>` or `<alias>=<source>`.
#[arg(value_name = "DEP")]
deps: Vec<String>,
},
/// Generates the simplicity contracts artifacts
Build,
/// Cleans Simplex artifacts in the current directory
Expand Down
Loading
Loading