Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
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]...` - Installs SimplicityHL dependencies. With no arguments, installs everything listed in `[dependencies]`. With one or more `DEP` arguments, appends new entries to `[dependencies]` and installs them.
Comment thread
LesterEvSe marked this conversation as resolved.
Outdated
- `simplex build` - Generates simplicity artifacts.
- `simplex regtest` - Spins up local Electrs + Elements nodes.
- `simplex test` - Runs Simplex tests.
Expand Down
9 changes: 7 additions & 2 deletions crates/build/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ use serde::Deserialize;
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 +48,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 +73,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 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.23.9"
ctrlc = { version = "3.5.2", features = ["termination"] }
serde_json = { version = "1.0.149" }
3 changes: 2 additions & 1 deletion crates/cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ impl Cli {

Ok(Regtest::run(&loaded_config.regtest)?)
}
Command::Install => {
Command::Install { deps } => {
let config_path = Config::get_default_path()?;
Config::add_dependency_to(&config_path, deps)?;
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,
Install {
/// Dependencies to install, as `<source>` or `<alias>=<source>`.
/// With no arguments, installs everything from `Simplex.toml`.
Comment thread
LesterEvSe marked this conversation as resolved.
Outdated
#[arg(value_name = "DEP")]
deps: Vec<String>,
},
/// Generates the simplicity contracts artifacts
Build,
/// Cleans Simplex artifacts in the current directory
Expand Down
69 changes: 68 additions & 1 deletion crates/cli/src/config/core.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use serde::Deserialize;
use std::path::{Path, PathBuf};
use toml_edit::{DocumentMut, InlineTable, Item, Value};

use smplx_build::{BuildConfig, DependencyConfig};
use smplx_build::{BuildConfig, DependencyConfig, config::DEPENDENCIES_SECTION};
use smplx_regtest::RegtestConfig;
use smplx_test::TestConfig;

use crate::config::dep_spec::{DepSpec, Source};

use super::error::ConfigError;

pub const INIT_CONFIG: &str = include_str!("../../assets/Simplex.default.toml");
Expand Down Expand Up @@ -68,6 +71,70 @@ impl Config {
Ok(config)
}

/// Appends new entries to the `[dependencies]` table of the config file at `path`,
/// preserving existing formatting and comments.
///
/// # Errors
/// - `ConfigError::MalformedDep`: If an entry in `deps` cannot be parsed as
/// `<source>` or `<alias>=<source>`.
/// - `ConfigError::UnableToEdit`: If the file at `path` cannot be parsed as TOML
/// for editing.
/// - `ConfigError::MalformedDependenciesTable`: If `[dependencies]` exists but
/// is not a TOML table.
/// - `ConfigError::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<(), ConfigError> {
Comment thread
Arvolear marked this conversation as resolved.
Outdated
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| ConfigError::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(ConfigError::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(ConfigError::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(())
}

fn validate(config: &Config) -> Result<(), ConfigError> {
if let Some(esplora_config) = config.test.esplora.clone() {
Self::validate_network(&esplora_config.network)?;
Expand Down
102 changes: 102 additions & 0 deletions crates/cli/src/config/dep_spec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use std::fmt::Write;

use super::error::ConfigError;

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
/// - `ConfigError::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, ConfigError> {
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(ConfigError::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
/// - `ConfigError::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, ConfigError> {
let last = source
.rsplit(['/', '\\'])
.find(|s| !s.is_empty())
.ok_or_else(|| ConfigError::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())
}
}
}
15 changes: 15 additions & 0 deletions crates/cli/src/config/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,19 @@ pub enum ConfigError {

#[error("Path doesn't exist: '{0}'")]
PathNotExists(PathBuf),

#[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),
}
1 change: 1 addition & 0 deletions crates/cli/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod core;
pub mod dep_spec;
pub mod error;

pub use core::{CONFIG_FILENAME, Config, INIT_CONFIG};