Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 bar name `std` pins the latest `SimpliclityHL` [standard library](https://github.com/BlockstreamResearch/simplicityhl-std) release.
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
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