rename crates/ to engrams/; add el-compiler el package with bootstrap artifact
- crates/ → engrams/ (Rust engrams live here)
- el-compiler/ added: el self-hosting compiler as an el package
- src/{compiler,lexer,parser,codegen}.el
- bootstrap/el-compiler.elc (114KB, Rust-compiled seed)
- el.toml Cargo.toml workspace paths updated
- neuron-rs cross-repo path deps fixed (were pointing to products/ instead of foundation/)
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
//! Manifest error types.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ManifestError {
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("toml parse error: {0}")]
|
||||
Toml(#[from] toml::de::Error),
|
||||
|
||||
#[error("semver parse error for '{field}': {source}")]
|
||||
Semver {
|
||||
field: String,
|
||||
#[source]
|
||||
source: semver::Error,
|
||||
},
|
||||
|
||||
#[error("missing required field: {0}")]
|
||||
MissingField(String),
|
||||
|
||||
#[error("invalid value for '{field}': {reason}")]
|
||||
InvalidValue { field: String, reason: String },
|
||||
|
||||
#[error("el.toml not found (searched from {0})")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("invalid cross target '{0}': use x86_64-linux, aarch64-linux, x86_64-macos, aarch64-macos, wasm32")]
|
||||
InvalidCrossTarget(String),
|
||||
|
||||
#[error("invalid build target '{0}': use debug, release, prod")]
|
||||
InvalidBuildTarget(String),
|
||||
|
||||
#[error("invalid seal key source '{0}': use env:VAR, file:path, or literal")]
|
||||
InvalidSealKeySource(String),
|
||||
}
|
||||
|
||||
pub type ManifestResult<T> = Result<T, ManifestError>;
|
||||
@@ -0,0 +1,28 @@
|
||||
//! el-manifest — `el.toml` project manifest parser.
|
||||
//!
|
||||
//! Every Engram project has an `el.toml` at its root. This crate defines the
|
||||
//! manifest data model and parses it from TOML text.
|
||||
//!
|
||||
//! # Quick start
|
||||
//! ```rust
|
||||
//! use el_manifest::Manifest;
|
||||
//!
|
||||
//! let toml = r#"
|
||||
//! [package]
|
||||
//! name = "my-service"
|
||||
//! version = "0.1.0"
|
||||
//! edition = "2026"
|
||||
//! "#;
|
||||
//! let manifest = Manifest::parse(toml).unwrap();
|
||||
//! assert_eq!(manifest.package.name, "my-service");
|
||||
//! ```
|
||||
|
||||
mod error;
|
||||
mod manifest;
|
||||
mod parse;
|
||||
|
||||
pub use error::{ManifestError, ManifestResult};
|
||||
pub use manifest::{
|
||||
BuildConfig, BuildTarget, CrossConfig, CrossTarget, Dependency, Manifest, NativeTarget,
|
||||
PackageInfo, SealKeySource,
|
||||
};
|
||||
@@ -0,0 +1,378 @@
|
||||
//! Core data model for the `el.toml` manifest.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use semver::{Version, VersionReq};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ── Package info ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Metadata about the package itself (`[package]` section).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PackageInfo {
|
||||
pub name: String,
|
||||
pub version: Version,
|
||||
pub description: Option<String>,
|
||||
pub authors: Vec<String>,
|
||||
pub license: Option<String>,
|
||||
/// Language edition, e.g. "2026".
|
||||
pub edition: String,
|
||||
}
|
||||
|
||||
// ── Dependencies ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// A single dependency specifier.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Dependency {
|
||||
/// A bare semver version requirement string (`"1.2"`, `"^0.8.1"`).
|
||||
VersionReq(VersionReq),
|
||||
/// A path-local dependency (`{ path = "../some-local" }`).
|
||||
Path(PathBuf),
|
||||
/// A registry package with an explicit registry URL.
|
||||
Registry {
|
||||
version: VersionReq,
|
||||
registry: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Dependency {
|
||||
/// Returns the version requirement if this is a registry / version dep.
|
||||
pub fn version_req(&self) -> Option<&VersionReq> {
|
||||
match self {
|
||||
Dependency::VersionReq(req) => Some(req),
|
||||
Dependency::Registry { version, .. } => Some(version),
|
||||
Dependency::Path(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the local path if this is a path dependency.
|
||||
pub fn local_path(&self) -> Option<&PathBuf> {
|
||||
match self {
|
||||
Dependency::Path(p) => Some(p),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Build config ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// The three compilation targets supported by the Engram toolchain.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum BuildTarget {
|
||||
Debug,
|
||||
Release,
|
||||
Prod,
|
||||
}
|
||||
|
||||
impl BuildTarget {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
BuildTarget::Debug => "debug",
|
||||
BuildTarget::Release => "release",
|
||||
BuildTarget::Prod => "prod",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BuildTarget {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for BuildTarget {
|
||||
type Err = crate::ManifestError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"debug" => Ok(BuildTarget::Debug),
|
||||
"release" => Ok(BuildTarget::Release),
|
||||
"prod" => Ok(BuildTarget::Prod),
|
||||
other => Err(crate::ManifestError::InvalidBuildTarget(other.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where to read the sealing key from.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum SealKeySource {
|
||||
/// `env:VAR_NAME` — read from an environment variable at build time.
|
||||
EnvVar(String),
|
||||
/// `file:path/to/key` — read raw bytes from a file.
|
||||
File(PathBuf),
|
||||
/// A literal key value — for development/testing only.
|
||||
Literal(String),
|
||||
}
|
||||
|
||||
impl SealKeySource {
|
||||
/// Parse from the `el.toml` string representation.
|
||||
pub fn parse(s: &str) -> Result<Self, crate::ManifestError> {
|
||||
if let Some(var) = s.strip_prefix("env:") {
|
||||
Ok(SealKeySource::EnvVar(var.to_string()))
|
||||
} else if let Some(path) = s.strip_prefix("file:") {
|
||||
Ok(SealKeySource::File(PathBuf::from(path)))
|
||||
} else if s.is_empty() {
|
||||
Err(crate::ManifestError::InvalidSealKeySource(s.to_string()))
|
||||
} else {
|
||||
Ok(SealKeySource::Literal(s.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the key bytes at runtime.
|
||||
pub fn resolve(&self) -> Result<Vec<u8>, crate::ManifestError> {
|
||||
match self {
|
||||
SealKeySource::EnvVar(var) => {
|
||||
std::env::var(var)
|
||||
.map(|v| v.into_bytes())
|
||||
.map_err(|_| crate::ManifestError::InvalidValue {
|
||||
field: format!("env:{var}"),
|
||||
reason: "environment variable not set".to_string(),
|
||||
})
|
||||
}
|
||||
SealKeySource::File(path) => {
|
||||
std::fs::read(path).map_err(crate::ManifestError::Io)
|
||||
}
|
||||
SealKeySource::Literal(s) => Ok(s.as_bytes().to_vec()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `[build]` section.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct BuildConfig {
|
||||
pub target: BuildTarget,
|
||||
pub entry: PathBuf,
|
||||
pub output: PathBuf,
|
||||
pub seal_key: Option<SealKeySource>,
|
||||
}
|
||||
|
||||
impl Default for BuildConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
target: BuildTarget::Debug,
|
||||
entry: PathBuf::from("src/main.el"),
|
||||
output: PathBuf::from("dist/"),
|
||||
seal_key: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cross-compilation ─────────────────────────────────────────────────────────
|
||||
|
||||
/// A native target triple for cross-compilation.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum CrossTarget {
|
||||
#[serde(rename = "x86_64-linux")]
|
||||
X86_64Linux,
|
||||
#[serde(rename = "aarch64-linux")]
|
||||
Aarch64Linux,
|
||||
#[serde(rename = "x86_64-macos")]
|
||||
X86_64Macos,
|
||||
#[serde(rename = "aarch64-macos")]
|
||||
Aarch64Macos,
|
||||
#[serde(rename = "wasm32")]
|
||||
Wasm32,
|
||||
}
|
||||
|
||||
impl CrossTarget {
|
||||
/// Parse from the string used in `el.toml`.
|
||||
pub fn parse(s: &str) -> Result<Self, crate::ManifestError> {
|
||||
match s {
|
||||
"x86_64-linux" => Ok(CrossTarget::X86_64Linux),
|
||||
"aarch64-linux" => Ok(CrossTarget::Aarch64Linux),
|
||||
"x86_64-macos" => Ok(CrossTarget::X86_64Macos),
|
||||
"aarch64-macos" => Ok(CrossTarget::Aarch64Macos),
|
||||
"wasm32" => Ok(CrossTarget::Wasm32),
|
||||
other => Err(crate::ManifestError::InvalidCrossTarget(other.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// The canonical Rust/LLVM target triple for this target.
|
||||
pub fn triple(&self) -> &'static str {
|
||||
match self {
|
||||
CrossTarget::X86_64Linux => "x86_64-unknown-linux-gnu",
|
||||
CrossTarget::Aarch64Linux => "aarch64-unknown-linux-gnu",
|
||||
CrossTarget::X86_64Macos => "x86_64-apple-darwin",
|
||||
CrossTarget::Aarch64Macos => "aarch64-apple-darwin",
|
||||
CrossTarget::Wasm32 => "wasm32-unknown-unknown",
|
||||
}
|
||||
}
|
||||
|
||||
/// File extension for compiled artifacts on this target.
|
||||
pub fn artifact_extension(&self) -> &'static str {
|
||||
match self {
|
||||
CrossTarget::Wasm32 => ".wasm",
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
/// The string as it appears in `el.toml`.
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
CrossTarget::X86_64Linux => "x86_64-linux",
|
||||
CrossTarget::Aarch64Linux => "aarch64-linux",
|
||||
CrossTarget::X86_64Macos => "x86_64-macos",
|
||||
CrossTarget::Aarch64Macos => "aarch64-macos",
|
||||
CrossTarget::Wasm32 => "wasm32",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CrossTarget {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// The `[cross]` section.
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct CrossConfig {
|
||||
pub targets: Vec<CrossTarget>,
|
||||
}
|
||||
|
||||
// ── Native target (compiler-level) ────────────────────────────────────────────
|
||||
|
||||
/// Compiler-level native target — includes `Host` for the current machine.
|
||||
///
|
||||
/// This is stored in the sealed artifact header so the runtime knows which
|
||||
/// native code generation backend to use.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum NativeTarget {
|
||||
#[serde(rename = "x86_64-linux")]
|
||||
X86_64Linux,
|
||||
#[serde(rename = "aarch64-linux")]
|
||||
Aarch64Linux,
|
||||
#[serde(rename = "x86_64-macos")]
|
||||
X86_64Macos,
|
||||
#[serde(rename = "aarch64-macos")]
|
||||
Aarch64Macos,
|
||||
#[serde(rename = "wasm32")]
|
||||
Wasm32,
|
||||
/// The host machine — resolved at runtime.
|
||||
#[serde(rename = "host")]
|
||||
Host,
|
||||
}
|
||||
|
||||
impl NativeTarget {
|
||||
/// The canonical LLVM triple for this target.
|
||||
///
|
||||
/// For `Host`, returns the compile-time host triple using
|
||||
/// `std::env::consts`.
|
||||
pub fn triple(&self) -> &str {
|
||||
match self {
|
||||
NativeTarget::X86_64Linux => "x86_64-unknown-linux-gnu",
|
||||
NativeTarget::Aarch64Linux => "aarch64-unknown-linux-gnu",
|
||||
NativeTarget::X86_64Macos => "x86_64-apple-darwin",
|
||||
NativeTarget::Aarch64Macos => "aarch64-apple-darwin",
|
||||
NativeTarget::Wasm32 => "wasm32-unknown-unknown",
|
||||
NativeTarget::Host => {
|
||||
// This is a static string that depends on the compile-time target.
|
||||
// We detect at compile time via cfg! macros.
|
||||
host_triple()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Output file extension for artifacts on this target.
|
||||
pub fn artifact_extension(&self) -> &'static str {
|
||||
match self {
|
||||
NativeTarget::Wasm32 => ".wasm",
|
||||
_ => {
|
||||
if cfg!(target_os = "windows") {
|
||||
".exe"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a [`CrossTarget`] to the equivalent [`NativeTarget`].
|
||||
pub fn from_cross(c: &CrossTarget) -> Self {
|
||||
match c {
|
||||
CrossTarget::X86_64Linux => NativeTarget::X86_64Linux,
|
||||
CrossTarget::Aarch64Linux => NativeTarget::Aarch64Linux,
|
||||
CrossTarget::X86_64Macos => NativeTarget::X86_64Macos,
|
||||
CrossTarget::Aarch64Macos => NativeTarget::Aarch64Macos,
|
||||
CrossTarget::Wasm32 => NativeTarget::Wasm32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn host_triple() -> &'static str {
|
||||
// Determine at compile time — these cfg values are set by rustc.
|
||||
if cfg!(all(target_arch = "x86_64", target_os = "linux")) {
|
||||
"x86_64-unknown-linux-gnu"
|
||||
} else if cfg!(all(target_arch = "aarch64", target_os = "linux")) {
|
||||
"aarch64-unknown-linux-gnu"
|
||||
} else if cfg!(all(target_arch = "x86_64", target_os = "macos")) {
|
||||
"x86_64-apple-darwin"
|
||||
} else if cfg!(all(target_arch = "aarch64", target_os = "macos")) {
|
||||
"aarch64-apple-darwin"
|
||||
} else if cfg!(target_arch = "wasm32") {
|
||||
"wasm32-unknown-unknown"
|
||||
} else {
|
||||
"unknown-unknown-unknown"
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NativeTarget {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.triple())
|
||||
}
|
||||
}
|
||||
|
||||
// ── Top-level manifest ────────────────────────────────────────────────────────
|
||||
|
||||
/// The parsed contents of an `el.toml` project manifest.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Manifest {
|
||||
pub package: PackageInfo,
|
||||
pub dependencies: HashMap<String, Dependency>,
|
||||
pub dev_dependencies: HashMap<String, Dependency>,
|
||||
pub build: BuildConfig,
|
||||
pub cross: CrossConfig,
|
||||
/// Plugin name → version requirement string.
|
||||
pub plugins: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Manifest {
|
||||
/// Parse a manifest from a TOML string.
|
||||
pub fn parse(s: &str) -> crate::ManifestResult<Self> {
|
||||
crate::parse::parse_manifest(s)
|
||||
}
|
||||
|
||||
/// Parse a manifest from a file on disk.
|
||||
pub fn from_file(path: &std::path::Path) -> crate::ManifestResult<Self> {
|
||||
let text = std::fs::read_to_string(path).map_err(crate::ManifestError::Io)?;
|
||||
Self::parse(&text)
|
||||
}
|
||||
|
||||
/// Walk up the directory tree from `from` until an `el.toml` is found.
|
||||
///
|
||||
/// Returns the path to the manifest file (not its directory).
|
||||
pub fn find_manifest(from: &std::path::Path) -> crate::ManifestResult<PathBuf> {
|
||||
let mut dir = if from.is_file() {
|
||||
from.parent().unwrap_or(from).to_path_buf()
|
||||
} else {
|
||||
from.to_path_buf()
|
||||
};
|
||||
|
||||
loop {
|
||||
let candidate = dir.join("el.toml");
|
||||
if candidate.exists() {
|
||||
return Ok(candidate);
|
||||
}
|
||||
match dir.parent() {
|
||||
Some(parent) => dir = parent.to_path_buf(),
|
||||
None => {
|
||||
return Err(crate::ManifestError::NotFound(from.display().to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
//! TOML parsing for `el.toml` manifests.
|
||||
//!
|
||||
//! We define a set of intermediate `Raw*` structs that map 1:1 to the TOML
|
||||
//! schema, then convert them into the strongly-typed `Manifest` model.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::Deserialize;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
use crate::error::{ManifestError, ManifestResult};
|
||||
use crate::manifest::{
|
||||
BuildConfig, BuildTarget, CrossConfig, CrossTarget, Dependency, Manifest, PackageInfo,
|
||||
SealKeySource,
|
||||
};
|
||||
|
||||
// ── Raw TOML structs ──────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RawManifest {
|
||||
package: RawPackage,
|
||||
#[serde(default)]
|
||||
dependencies: HashMap<String, TomlValue>,
|
||||
#[serde(rename = "dev-dependencies", default)]
|
||||
dev_dependencies: HashMap<String, TomlValue>,
|
||||
#[serde(default)]
|
||||
build: RawBuild,
|
||||
#[serde(default)]
|
||||
cross: RawCross,
|
||||
#[serde(default)]
|
||||
plugins: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RawPackage {
|
||||
name: String,
|
||||
version: String,
|
||||
description: Option<String>,
|
||||
#[serde(default)]
|
||||
authors: Vec<String>,
|
||||
license: Option<String>,
|
||||
#[serde(default = "default_edition")]
|
||||
edition: String,
|
||||
}
|
||||
|
||||
fn default_edition() -> String {
|
||||
"2026".to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(default)]
|
||||
struct RawBuild {
|
||||
target: String,
|
||||
entry: String,
|
||||
output: String,
|
||||
seal_key: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for RawBuild {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
target: "debug".to_string(),
|
||||
entry: "src/main.el".to_string(),
|
||||
output: "dist/".to_string(),
|
||||
seal_key: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
struct RawCross {
|
||||
#[serde(default)]
|
||||
targets: Vec<String>,
|
||||
}
|
||||
|
||||
// ── Entry point ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Parse a raw TOML string into a [`Manifest`].
|
||||
pub(crate) fn parse_manifest(s: &str) -> ManifestResult<Manifest> {
|
||||
let raw: RawManifest = toml::from_str(s)?;
|
||||
convert(raw)
|
||||
}
|
||||
|
||||
fn convert(raw: RawManifest) -> ManifestResult<Manifest> {
|
||||
let package = convert_package(raw.package)?;
|
||||
let dependencies = convert_deps(&raw.dependencies, "dependencies")?;
|
||||
let dev_dependencies = convert_deps(&raw.dev_dependencies, "dev-dependencies")?;
|
||||
let build = convert_build(raw.build)?;
|
||||
let cross = convert_cross(raw.cross)?;
|
||||
|
||||
Ok(Manifest {
|
||||
package,
|
||||
dependencies,
|
||||
dev_dependencies,
|
||||
build,
|
||||
cross,
|
||||
plugins: raw.plugins,
|
||||
})
|
||||
}
|
||||
|
||||
fn convert_package(raw: RawPackage) -> ManifestResult<PackageInfo> {
|
||||
let version = semver::Version::parse(&raw.version).map_err(|e| ManifestError::Semver {
|
||||
field: "package.version".to_string(),
|
||||
source: e,
|
||||
})?;
|
||||
|
||||
Ok(PackageInfo {
|
||||
name: raw.name,
|
||||
version,
|
||||
description: raw.description,
|
||||
authors: raw.authors,
|
||||
license: raw.license,
|
||||
edition: raw.edition,
|
||||
})
|
||||
}
|
||||
|
||||
fn convert_deps(
|
||||
raw: &HashMap<String, TomlValue>,
|
||||
section: &str,
|
||||
) -> ManifestResult<HashMap<String, Dependency>> {
|
||||
let mut out = HashMap::new();
|
||||
for (name, value) in raw {
|
||||
let dep = convert_single_dep(name, value, section)?;
|
||||
out.insert(name.clone(), dep);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn convert_single_dep(
|
||||
name: &str,
|
||||
value: &TomlValue,
|
||||
section: &str,
|
||||
) -> ManifestResult<Dependency> {
|
||||
match value {
|
||||
// String form: `name = "1.2"` or `name = "^0.8.1"`
|
||||
TomlValue::String(s) => {
|
||||
let req =
|
||||
semver::VersionReq::parse(s).map_err(|e| ManifestError::Semver {
|
||||
field: format!("{section}.{name}"),
|
||||
source: e,
|
||||
})?;
|
||||
Ok(Dependency::VersionReq(req))
|
||||
}
|
||||
// Table form: `name = { path = "..." }` or `name = { version = "...", registry = "..." }`
|
||||
TomlValue::Table(t) => {
|
||||
if let Some(TomlValue::String(path)) = t.get("path") {
|
||||
return Ok(Dependency::Path(PathBuf::from(path)));
|
||||
}
|
||||
if let Some(TomlValue::String(ver_str)) = t.get("version") {
|
||||
let version =
|
||||
semver::VersionReq::parse(ver_str).map_err(|e| ManifestError::Semver {
|
||||
field: format!("{section}.{name}.version"),
|
||||
source: e,
|
||||
})?;
|
||||
let registry = t
|
||||
.get("registry")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("https://packages.neurontechnologies.ai")
|
||||
.to_string();
|
||||
return Ok(Dependency::Registry { version, registry });
|
||||
}
|
||||
Err(ManifestError::InvalidValue {
|
||||
field: format!("{section}.{name}"),
|
||||
reason: "dependency table must have 'path' or 'version' key".to_string(),
|
||||
})
|
||||
}
|
||||
other => Err(ManifestError::InvalidValue {
|
||||
field: format!("{section}.{name}"),
|
||||
reason: format!("expected string or table, got {}", other.type_str()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_build(raw: RawBuild) -> ManifestResult<BuildConfig> {
|
||||
let target = raw.target.parse::<BuildTarget>()?;
|
||||
let seal_key = raw
|
||||
.seal_key
|
||||
.map(|s| SealKeySource::parse(&s))
|
||||
.transpose()?;
|
||||
|
||||
Ok(BuildConfig {
|
||||
target,
|
||||
entry: PathBuf::from(raw.entry),
|
||||
output: PathBuf::from(raw.output),
|
||||
seal_key,
|
||||
})
|
||||
}
|
||||
|
||||
fn convert_cross(raw: RawCross) -> ManifestResult<CrossConfig> {
|
||||
let targets = raw
|
||||
.targets
|
||||
.iter()
|
||||
.map(|s| CrossTarget::parse(s))
|
||||
.collect::<ManifestResult<Vec<_>>>()?;
|
||||
Ok(CrossConfig { targets })
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::manifest::{BuildTarget, CrossTarget, Dependency, NativeTarget, SealKeySource};
|
||||
|
||||
fn full_toml() -> &'static str {
|
||||
r#"
|
||||
[package]
|
||||
name = "my-service"
|
||||
version = "0.1.0"
|
||||
description = "What this does"
|
||||
authors = ["Will Anderson <will@neurontechnologies.ai>"]
|
||||
license = "MIT"
|
||||
edition = "2026"
|
||||
|
||||
[dependencies]
|
||||
engram-http = "1.2"
|
||||
engram-auth = "0.8.1"
|
||||
some-local = { path = "../some-local" }
|
||||
|
||||
[dev-dependencies]
|
||||
el-test = "0.1"
|
||||
|
||||
[build]
|
||||
target = "prod"
|
||||
entry = "src/main.el"
|
||||
output = "dist/"
|
||||
seal_key = "env:ENGRAM_SEAL_KEY"
|
||||
|
||||
[cross]
|
||||
targets = ["x86_64-linux", "aarch64-linux", "x86_64-macos", "aarch64-macos", "wasm32"]
|
||||
|
||||
[plugins]
|
||||
el-fmt = "1.0"
|
||||
el-doc = "0.3"
|
||||
"#
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_full_manifest() {
|
||||
let m = parse_manifest(full_toml()).unwrap();
|
||||
assert_eq!(m.package.name, "my-service");
|
||||
assert_eq!(m.package.version.to_string(), "0.1.0");
|
||||
assert_eq!(m.package.edition, "2026");
|
||||
assert_eq!(m.package.license.as_deref(), Some("MIT"));
|
||||
assert_eq!(m.package.authors, vec!["Will Anderson <will@neurontechnologies.ai>"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dependencies() {
|
||||
let m = parse_manifest(full_toml()).unwrap();
|
||||
|
||||
// Version requirement dep
|
||||
assert!(m.dependencies.contains_key("engram-http"));
|
||||
match &m.dependencies["engram-http"] {
|
||||
Dependency::VersionReq(req) => {
|
||||
assert!(req.to_string().contains("1"));
|
||||
}
|
||||
other => panic!("expected VersionReq, got {other:?}"),
|
||||
}
|
||||
|
||||
// Path dep
|
||||
assert!(m.dependencies.contains_key("some-local"));
|
||||
match &m.dependencies["some-local"] {
|
||||
Dependency::Path(p) => assert_eq!(p.to_str().unwrap(), "../some-local"),
|
||||
other => panic!("expected Path, got {other:?}"),
|
||||
}
|
||||
|
||||
// Dev dep
|
||||
assert!(m.dev_dependencies.contains_key("el-test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_build_config() {
|
||||
let m = parse_manifest(full_toml()).unwrap();
|
||||
assert_eq!(m.build.target, BuildTarget::Prod);
|
||||
assert_eq!(m.build.entry.to_str().unwrap(), "src/main.el");
|
||||
assert_eq!(m.build.output.to_str().unwrap(), "dist/");
|
||||
match &m.build.seal_key {
|
||||
Some(SealKeySource::EnvVar(v)) => assert_eq!(v, "ENGRAM_SEAL_KEY"),
|
||||
other => panic!("expected EnvVar seal_key, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_cross_targets() {
|
||||
let m = parse_manifest(full_toml()).unwrap();
|
||||
assert_eq!(m.cross.targets.len(), 5);
|
||||
assert!(m.cross.targets.contains(&CrossTarget::X86_64Linux));
|
||||
assert!(m.cross.targets.contains(&CrossTarget::Aarch64Linux));
|
||||
assert!(m.cross.targets.contains(&CrossTarget::X86_64Macos));
|
||||
assert!(m.cross.targets.contains(&CrossTarget::Aarch64Macos));
|
||||
assert!(m.cross.targets.contains(&CrossTarget::Wasm32));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_plugins() {
|
||||
let m = parse_manifest(full_toml()).unwrap();
|
||||
assert_eq!(m.plugins.get("el-fmt").map(|s| s.as_str()), Some("1.0"));
|
||||
assert_eq!(m.plugins.get("el-doc").map(|s| s.as_str()), Some("0.3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_minimal_manifest() {
|
||||
let toml = r#"
|
||||
[package]
|
||||
name = "hello"
|
||||
version = "0.1.0"
|
||||
"#;
|
||||
let m = parse_manifest(toml).unwrap();
|
||||
assert_eq!(m.package.name, "hello");
|
||||
assert_eq!(m.package.edition, "2026"); // default
|
||||
assert_eq!(m.build.target, BuildTarget::Debug); // default
|
||||
assert_eq!(m.build.entry.to_str().unwrap(), "src/main.el"); // default
|
||||
assert!(m.dependencies.is_empty());
|
||||
assert!(m.cross.targets.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_semver_rejected() {
|
||||
let toml = r#"
|
||||
[package]
|
||||
name = "bad"
|
||||
version = "not-a-version"
|
||||
"#;
|
||||
let err = parse_manifest(toml).unwrap_err();
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("semver"), "expected semver error, got: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_build_target_rejected() {
|
||||
let toml = r#"
|
||||
[package]
|
||||
name = "bad"
|
||||
version = "0.1.0"
|
||||
|
||||
[build]
|
||||
target = "turbo"
|
||||
"#;
|
||||
let err = parse_manifest(toml).unwrap_err();
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("turbo"), "expected target name in error: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_cross_target_rejected() {
|
||||
let toml = r#"
|
||||
[package]
|
||||
name = "bad"
|
||||
version = "0.1.0"
|
||||
|
||||
[cross]
|
||||
targets = ["x86_64-linux", "solaris-sparc"]
|
||||
"#;
|
||||
let err = parse_manifest(toml).unwrap_err();
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("solaris-sparc"), "expected target name in error: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_seal_key_env_parse() {
|
||||
let src = SealKeySource::parse("env:MY_KEY").unwrap();
|
||||
assert_eq!(src, SealKeySource::EnvVar("MY_KEY".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_seal_key_file_parse() {
|
||||
let src = SealKeySource::parse("file:/tmp/key.bin").unwrap();
|
||||
assert_eq!(src, SealKeySource::File(PathBuf::from("/tmp/key.bin")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_seal_key_literal_parse() {
|
||||
let src = SealKeySource::parse("my-literal-key").unwrap();
|
||||
assert_eq!(src, SealKeySource::Literal("my-literal-key".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_native_target_host_triple() {
|
||||
let t = NativeTarget::Host;
|
||||
// Should return a non-empty triple
|
||||
assert!(!t.triple().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_native_target_wasm_extension() {
|
||||
let t = NativeTarget::Wasm32;
|
||||
assert_eq!(t.artifact_extension(), ".wasm");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cross_target_triple_roundtrip() {
|
||||
let targets = [
|
||||
CrossTarget::X86_64Linux,
|
||||
CrossTarget::Aarch64Linux,
|
||||
CrossTarget::X86_64Macos,
|
||||
CrossTarget::Aarch64Macos,
|
||||
CrossTarget::Wasm32,
|
||||
];
|
||||
for t in &targets {
|
||||
// as_str → parse → same target
|
||||
let s = t.as_str();
|
||||
let parsed = CrossTarget::parse(s).unwrap();
|
||||
assert_eq!(&parsed, t, "roundtrip failed for {s}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_native_target_from_cross() {
|
||||
let nt = NativeTarget::from_cross(&CrossTarget::Wasm32);
|
||||
assert_eq!(nt, NativeTarget::Wasm32);
|
||||
let nt2 = NativeTarget::from_cross(&CrossTarget::Aarch64Macos);
|
||||
assert_eq!(nt2, NativeTarget::Aarch64Macos);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_registry_dep_table() {
|
||||
let toml = r#"
|
||||
[package]
|
||||
name = "test"
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies]
|
||||
special-pkg = { version = "2.0", registry = "https://custom.registry.io" }
|
||||
"#;
|
||||
let m = parse_manifest(toml).unwrap();
|
||||
match &m.dependencies["special-pkg"] {
|
||||
Dependency::Registry { version, registry } => {
|
||||
assert!(version.to_string().contains("2"));
|
||||
assert_eq!(registry, "https://custom.registry.io");
|
||||
}
|
||||
other => panic!("expected Registry dep, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dep_version_req_method() {
|
||||
let d = Dependency::VersionReq(semver::VersionReq::parse("1.0").unwrap());
|
||||
assert!(d.version_req().is_some());
|
||||
assert!(d.local_path().is_none());
|
||||
|
||||
let d2 = Dependency::Path(PathBuf::from("/tmp"));
|
||||
assert!(d2.local_path().is_some());
|
||||
assert!(d2.version_req().is_none());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user