Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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 <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 install <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. The bare name `std` pins the latest `SimplicityHL` [standard library](https://github.com/BlockstreamResearch/simplicityhl-std) release.
- `simplex build` - Generates simplicity artifacts.
- `simplex regtest` - Spins up local Electrs + Elements nodes.
- `simplex test` - Runs Simplex tests.
Expand Down
97 changes: 90 additions & 7 deletions crates/build/src/config/dep_spec.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
use std::fmt::Write;
use std::process::Command;

use toml_edit::{InlineTable, Value};

use crate::error::TomlEditError;

/// The bare token that resolves to the `SimplicityHL` standard library.
const STD_ALIAS: &str = "std";

/// The repository backing [`STD_ALIAS`].
const STD_URL: &str = "https://github.com/BlockstreamResearch/simplicityhl-std.git";

pub(super) struct DepSpec {
pub alias: String,
pub source: Source,
}

pub(super) enum Source {
Git(String),
Git { url: String, tag: Option<String> },
Path(String),
}

impl DepSpec {
/// Parses a raw CLI token into a [`DepSpec`].
///
/// Accepted forms:
/// - `std`: Shorthand for [`STD_URL`], pinned to its latest release tag.
/// - `<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.
Expand All @@ -27,7 +37,13 @@ impl DepSpec {
/// - `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).
/// - `TomlEditError::RemoteTags` / `TomlEditError::NoTags`: If `raw` is `std` and
/// its latest tag cannot be resolved.
pub(super) fn parse_dep(raw: &str) -> Result<DepSpec, TomlEditError> {
if raw == STD_ALIAS {
return Self::std_spec();
}

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())),
Expand All @@ -39,6 +55,74 @@ impl DepSpec {
Ok(DepSpec { alias, source })
}

/// Builds the spec for the standard library, pinned to the newest tag currently
/// published by [`STD_URL`].
fn std_spec() -> Result<DepSpec, TomlEditError> {
let tag = Self::latest_tag(STD_URL)?;

Ok(DepSpec {
alias: STD_ALIAS.to_owned(),
source: Source::Git {
url: STD_URL.to_owned(),
tag: Some(tag),
},
})
}

/// Returns the highest release tag advertised by the remote at `url`.
///
/// # Errors
/// - `TomlEditError::RemoteTags`: If `git ls-remote` cannot be run, exits non-zero,
/// or emits non-UTF-8 output.
/// - `TomlEditError::NoTags`: If the remote advertises no release tag.
fn latest_tag(url: &str) -> Result<String, TomlEditError> {
let failed = |reason: String| TomlEditError::RemoteTags {
url: url.to_owned(),
reason,
};

let output = Command::new("git")
.args(["ls-remote", "--tags", "--refs", "--sort=-v:refname", url])
.output()
.map_err(|err| failed(err.to_string()))?;

if !output.status.success() {
return Err(failed(String::from_utf8_lossy(&output.stderr).trim().to_owned()));
}

let stdout = String::from_utf8(output.stdout).map_err(|err| failed(err.to_string()))?;

stdout
.lines()
.filter_map(|line| line.split_once('\t'))
.filter_map(|(_, reference)| reference.strip_prefix("refs/tags/"))
// skip pre-releases
.find(|tag| !tag.contains('-'))
.map(str::to_owned)
.ok_or_else(|| TomlEditError::NoTags(url.to_owned()))
}

/// Renders the spec as the inline table written under `[dependencies]`.
#[must_use]
pub(super) fn to_inline(&self) -> InlineTable {
let mut inline = InlineTable::new();

match &self.source {
Source::Git { url, tag } => {
inline.insert("git", Value::from(url.as_str()));

if let Some(tag) = tag {
inline.insert("tag", Value::from(tag.as_str()));
}
}
Source::Path(p) => {
inline.insert("path", Value::from(p.as_str()));
}
}

inline
}

/// Formats a batch of dependency specs as a bracketed, one-per-line list.
#[must_use]
pub(super) fn format_batch(specs: &[DepSpec]) -> String {
Expand All @@ -53,11 +137,7 @@ impl DepSpec {
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);
let _ = write!(out, "\n {} = {}", spec.alias, spec.to_inline());
}

out.push_str("\n]");
Expand Down Expand Up @@ -94,7 +174,10 @@ impl DepSpec {
|| s.starts_with("ssh://")
|| git_ext
{
Source::Git(s.to_owned())
Source::Git {
url: s.to_owned(),
tag: None,
}
} else {
Source::Path(s.to_owned())
}
Expand Down
26 changes: 3 additions & 23 deletions crates/build/src/config/dependency.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
use std::collections::HashMap;
use std::path::Path;

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

use serde::Deserialize;

use super::dep_spec::DepSpec;
use super::dep_spec::Source;

use crate::error::{BuildError, DependencyValidationError, TomlEditError};

Expand Down Expand Up @@ -103,31 +102,12 @@ impl DependencyConfig {
.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) {
if 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)));
deps_table.insert(&spec.alias, Item::Value(spec.to_inline().into()));
}

std::fs::write(path, doc.to_string())?;
Expand Down
6 changes: 6 additions & 0 deletions crates/build/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ pub enum TomlEditError {

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

#[error("failed to list the tags of `{url}`: {reason}")]
RemoteTags { url: String, reason: String },

#[error("`{0}` publishes no release tag")]
NoTags(String),
}

#[derive(thiserror::Error, Debug)]
Expand Down
1 change: 1 addition & 0 deletions crates/cli/src/commands/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub enum Command {
/// If `deps` is empty, install everything from `Simplex.toml`.
Install {
/// Dependencies to install, as `<source>` or `<alias>=<source>`.
/// The bare name `std` pins the latest `SimplicityHL` standard library release.
#[arg(value_name = "DEP")]
deps: Vec<String>,
},
Expand Down