feat(types): add core data structures
PackageIndex, Recipe, DepConstraint, BuildPlan, BuildResult, CacheManifest, BinaryCheckResult structs. Pool/BuildSystem/DepOp/Source enums. DepConstraint.parse() ported from ZETA vercmp.lua. TypesException for parsing errors. All @safe, all strings default to empty string.
This commit is contained in:
@@ -0,0 +1,541 @@
|
||||
/// tofu.types — Core data structures shared across all tofu modules.
|
||||
///
|
||||
/// Pure data types with no I/O. All structs are @safe, all strings
|
||||
/// default to `""` (never null).
|
||||
module tofu.types;
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Exception
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Single exception type for the types module.
|
||||
class TypesException : Exception
|
||||
{
|
||||
@safe this(string msg)
|
||||
{
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Pool — package source pool
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
enum Pool
|
||||
{
|
||||
binary,
|
||||
recipes,
|
||||
both,
|
||||
}
|
||||
|
||||
/// Parse a pool string value.
|
||||
@safe Pool fromPoolString(string s)
|
||||
{
|
||||
switch (s)
|
||||
{
|
||||
case "binary":
|
||||
return Pool.binary;
|
||||
case "recipes":
|
||||
return Pool.recipes;
|
||||
case "both":
|
||||
return Pool.both;
|
||||
default:
|
||||
throw new TypesException("invalid pool value");
|
||||
}
|
||||
}
|
||||
|
||||
/// Return canonical string representation of a Pool.
|
||||
@safe string poolToString(Pool p)
|
||||
{
|
||||
final switch (p)
|
||||
{
|
||||
case Pool.binary:
|
||||
return "binary";
|
||||
case Pool.recipes:
|
||||
return "recipes";
|
||||
case Pool.both:
|
||||
return "both";
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// BuildSystem — known build systems
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
enum BuildSystem
|
||||
{
|
||||
autotools,
|
||||
cmake,
|
||||
meson,
|
||||
make,
|
||||
cargo,
|
||||
custom,
|
||||
unknown,
|
||||
}
|
||||
|
||||
/// Map a build-system string to the enum. Unknown strings
|
||||
/// map to `BuildSystem.unknown` — callers decide how to handle it.
|
||||
@safe BuildSystem buildSystemFromString(string s)
|
||||
{
|
||||
switch (s)
|
||||
{
|
||||
case "autotools":
|
||||
return BuildSystem.autotools;
|
||||
case "cmake":
|
||||
return BuildSystem.cmake;
|
||||
case "meson":
|
||||
return BuildSystem.meson;
|
||||
case "make":
|
||||
return BuildSystem.make;
|
||||
case "cargo":
|
||||
return BuildSystem.cargo;
|
||||
case "custom":
|
||||
return BuildSystem.custom;
|
||||
default:
|
||||
return BuildSystem.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// DepOp — dependency-constraint operators
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
enum DepOp
|
||||
{
|
||||
ge, // >=
|
||||
le, // <=
|
||||
eq, // == (and = normalised)
|
||||
ne, // ~=
|
||||
gt, // >
|
||||
lt, // <
|
||||
none, // unconstrained
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Source — where a package comes from in a build-plan entry
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
enum Source
|
||||
{
|
||||
recipe,
|
||||
binary,
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Character helpers for dep-spec scanning
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
private @safe bool isNameChar(char c)
|
||||
{
|
||||
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
|
||||
|| (c >= '0' && c <= '9') || c == '_' || c == '.'
|
||||
|| c == '+' || c == '-';
|
||||
}
|
||||
|
||||
private @safe bool isWhite(char c)
|
||||
{
|
||||
return c == ' ' || c == '\t';
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// PackageIndex — index.lua entry
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
struct PackageIndex
|
||||
{
|
||||
string name = "";
|
||||
string ver = "";
|
||||
string summary = "";
|
||||
Pool pool = Pool.both;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Recipe — .recipe file fields
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
struct Recipe
|
||||
{
|
||||
string name = "";
|
||||
string ver = "";
|
||||
string summary = "";
|
||||
string url = "";
|
||||
string sha256 = "";
|
||||
string[] deps; // raw dep specs e.g. ["libfoo", "libbar>=2.0"]
|
||||
BuildSystem buildSystem = BuildSystem.unknown;
|
||||
string[] configureArgs; // extra configure arguments
|
||||
string buildScript = ""; // required for custom build system
|
||||
string testCmd = ""; // e.g. "test -f ${DESTDIR}/usr/bin/pkg"
|
||||
string[] files; // committed file paths
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// DepConstraint — parsed dependency spec
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
/// A single parsed dependency constraint.
|
||||
///
|
||||
/// Examples:
|
||||
/// parse("libfoo") → name="libfoo", op=none, ver=""
|
||||
/// parse("libfoo>=2.0") → name="libfoo", op=ge, ver="2.0"
|
||||
/// parse("pcre2~=10.42") → name="pcre2", op=ne, ver="10.42"
|
||||
struct DepConstraint
|
||||
{
|
||||
string name = "";
|
||||
DepOp op = DepOp.none;
|
||||
string ver = "";
|
||||
|
||||
/// Parse a single dependency specification string.
|
||||
///
|
||||
/// Semantics ported from `vercmp.parse_dep` in ZETA/lib/vercmp.lua.
|
||||
/// - name : `[A-Za-z0-9_.+-]+`
|
||||
/// - ops : `>=` `<=` `==` `~=` `>` `<` `=` (2-char checked first)
|
||||
/// - `=` normalises to `==`
|
||||
/// - trailing garbage after version → `TypesException`
|
||||
/// - empty string / no name → `TypesException`
|
||||
@safe static DepConstraint parse(string spec)
|
||||
{
|
||||
size_t pos = 0;
|
||||
|
||||
// ── skip leading whitespace ──
|
||||
while (pos < spec.length && isWhite(spec[pos]))
|
||||
pos++;
|
||||
|
||||
// ── extract name ──
|
||||
size_t nameStart = pos;
|
||||
while (pos < spec.length && isNameChar(spec[pos]))
|
||||
pos++;
|
||||
|
||||
if (pos == nameStart)
|
||||
throw new TypesException("bad dependency \"" ~ spec ~ "\"");
|
||||
|
||||
DepConstraint result;
|
||||
result.name = spec[nameStart .. pos];
|
||||
|
||||
// ── skip whitespace after name ──
|
||||
while (pos < spec.length && isWhite(spec[pos]))
|
||||
pos++;
|
||||
|
||||
// ── unconstrained: no operator present ──
|
||||
if (pos >= spec.length)
|
||||
return result; // op=none, ver=""
|
||||
|
||||
// ── try matching an operator (2-char first, then 1-char) ──
|
||||
string opStr;
|
||||
|
||||
if (pos + 1 < spec.length)
|
||||
{
|
||||
string two = spec[pos .. pos + 2];
|
||||
if (two == ">=") { result.op = DepOp.ge; opStr = ">="; }
|
||||
else if (two == "<=") { result.op = DepOp.le; opStr = "<="; }
|
||||
else if (two == "==") { result.op = DepOp.eq; opStr = "=="; }
|
||||
else if (two == "~=") { result.op = DepOp.ne; opStr = "~="; }
|
||||
}
|
||||
|
||||
if (opStr.length == 0)
|
||||
{
|
||||
char c = spec[pos];
|
||||
if (c == '>') { result.op = DepOp.gt; opStr = ">"; }
|
||||
else if (c == '<') { result.op = DepOp.lt; opStr = "<"; }
|
||||
else if (c == '=') { result.op = DepOp.eq; opStr = "="; }
|
||||
}
|
||||
|
||||
if (opStr.length == 0)
|
||||
{
|
||||
throw new TypesException(
|
||||
"bad dependency constraint \"" ~ spec
|
||||
~ "\" (expected NAME OP VERSION)");
|
||||
}
|
||||
|
||||
pos += opStr.length;
|
||||
|
||||
// ── skip whitespace after operator ──
|
||||
while (pos < spec.length && isWhite(spec[pos]))
|
||||
pos++;
|
||||
|
||||
// ── extract version ──
|
||||
size_t verStart = pos;
|
||||
while (pos < spec.length && isNameChar(spec[pos]))
|
||||
pos++;
|
||||
size_t verEnd = pos;
|
||||
|
||||
if (verStart == verEnd)
|
||||
{
|
||||
throw new TypesException(
|
||||
"bad dependency constraint \"" ~ spec
|
||||
~ "\" (expected NAME OP VERSION)");
|
||||
}
|
||||
|
||||
result.ver = spec[verStart .. verEnd];
|
||||
|
||||
// ── check for trailing garbage ──
|
||||
while (pos < spec.length && isWhite(spec[pos]))
|
||||
pos++;
|
||||
if (pos < spec.length)
|
||||
{
|
||||
throw new TypesException(
|
||||
"bad dependency constraint \"" ~ spec
|
||||
~ "\" (expected NAME OP VERSION)");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// BuildPlanEntry / BuildPlan
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
struct BuildPlanEntry
|
||||
{
|
||||
string name = "";
|
||||
string recipePath = "";
|
||||
Source source = Source.recipe;
|
||||
}
|
||||
|
||||
/// Ordered set of packages to build. Sorting/deps-first ordering
|
||||
/// is guaranteed by the caller; this is a plain container.
|
||||
struct BuildPlan
|
||||
{
|
||||
BuildPlanEntry[] entries;
|
||||
|
||||
@safe void add(string name, string recipePath, Source source)
|
||||
{
|
||||
entries ~= BuildPlanEntry(name, recipePath, source);
|
||||
}
|
||||
|
||||
/// Return entries in the order they were added (caller
|
||||
/// must arrange deps-first ordering before calling this).
|
||||
@safe BuildPlanEntry[] order()
|
||||
{
|
||||
return entries;
|
||||
}
|
||||
|
||||
@safe bool isEmpty()
|
||||
{
|
||||
return entries.length == 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// CacheManifest — cached-package metadata
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
struct CacheManifest
|
||||
{
|
||||
string name = "";
|
||||
string ver = "";
|
||||
long fetchedAt = 0; // unix timestamp
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// BuildFailure — single build failure record
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
struct BuildFailure
|
||||
{
|
||||
string name = "";
|
||||
string reason = "";
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// BuildResult — build outcome tracker
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
struct BuildResult
|
||||
{
|
||||
string[] succeeded;
|
||||
BuildFailure[] failed;
|
||||
|
||||
/// Return just the names of the packages that failed.
|
||||
@safe string[] failedNames()
|
||||
{
|
||||
string[] names;
|
||||
foreach (f; failed)
|
||||
names ~= f.name;
|
||||
return names;
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// BinaryCheckResult — pre-built binary availability
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
struct BinaryCheckResult
|
||||
{
|
||||
bool exists = false;
|
||||
string ver = "";
|
||||
bool satisfies = false;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Unittests
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
@safe unittest
|
||||
{
|
||||
// ── Pool parsing ──────────────────────────────────────
|
||||
assert(fromPoolString("binary") == Pool.binary);
|
||||
assert(fromPoolString("recipes") == Pool.recipes);
|
||||
assert(fromPoolString("both") == Pool.both);
|
||||
|
||||
assert(poolToString(Pool.binary) == "binary");
|
||||
assert(poolToString(Pool.recipes) == "recipes");
|
||||
assert(poolToString(Pool.both) == "both");
|
||||
|
||||
bool caught = false;
|
||||
try
|
||||
{
|
||||
fromPoolString("nope");
|
||||
assert(false, "expected exception");
|
||||
}
|
||||
catch (TypesException e)
|
||||
{
|
||||
caught = true;
|
||||
}
|
||||
assert(caught, "invalid pool should throw");
|
||||
}
|
||||
|
||||
@safe unittest
|
||||
{
|
||||
// ── DepConstraint: unconstrained ─────────────────────
|
||||
{
|
||||
auto d = DepConstraint.parse("libfoo");
|
||||
assert(d.name == "libfoo");
|
||||
assert(d.op == DepOp.none);
|
||||
assert(d.ver == "");
|
||||
}
|
||||
{
|
||||
auto d = DepConstraint.parse("pcre2");
|
||||
assert(d.name == "pcre2");
|
||||
assert(d.op == DepOp.none);
|
||||
assert(d.ver == "");
|
||||
}
|
||||
}
|
||||
|
||||
@safe unittest
|
||||
{
|
||||
// ── DepConstraint: all operators ─────────────────────
|
||||
{
|
||||
auto d = DepConstraint.parse("libfoo>=2.0");
|
||||
assert(d.name == "libfoo");
|
||||
assert(d.op == DepOp.ge);
|
||||
assert(d.ver == "2.0");
|
||||
}
|
||||
{
|
||||
auto d = DepConstraint.parse("libfoo<=2.0");
|
||||
assert(d.name == "libfoo");
|
||||
assert(d.op == DepOp.le);
|
||||
assert(d.ver == "2.0");
|
||||
}
|
||||
{
|
||||
auto d = DepConstraint.parse("libfoo==2.0");
|
||||
assert(d.name == "libfoo");
|
||||
assert(d.op == DepOp.eq);
|
||||
assert(d.ver == "2.0");
|
||||
}
|
||||
{
|
||||
auto d = DepConstraint.parse("libfoo~=2.0");
|
||||
assert(d.name == "libfoo");
|
||||
assert(d.op == DepOp.ne);
|
||||
assert(d.ver == "2.0");
|
||||
}
|
||||
{
|
||||
auto d = DepConstraint.parse("libfoo>2.0");
|
||||
assert(d.name == "libfoo");
|
||||
assert(d.op == DepOp.gt);
|
||||
assert(d.ver == "2.0");
|
||||
}
|
||||
{
|
||||
auto d = DepConstraint.parse("libfoo<2.0");
|
||||
assert(d.name == "libfoo");
|
||||
assert(d.op == DepOp.lt);
|
||||
assert(d.ver == "2.0");
|
||||
}
|
||||
{
|
||||
// single = normalises to ==
|
||||
auto d = DepConstraint.parse("libfoo=2.0");
|
||||
assert(d.name == "libfoo");
|
||||
assert(d.op == DepOp.eq);
|
||||
assert(d.ver == "2.0");
|
||||
}
|
||||
}
|
||||
|
||||
@safe unittest
|
||||
{
|
||||
// ── DepConstraint: edge cases ────────────────────────
|
||||
// empty string → throw
|
||||
bool caught = false;
|
||||
try
|
||||
{
|
||||
DepConstraint.parse("");
|
||||
assert(false, "expected exception");
|
||||
}
|
||||
catch (TypesException)
|
||||
{
|
||||
caught = true;
|
||||
}
|
||||
assert(caught, "empty string should throw");
|
||||
|
||||
// whitespace-only → throw
|
||||
caught = false;
|
||||
try
|
||||
{
|
||||
DepConstraint.parse(" ");
|
||||
assert(false, "expected exception");
|
||||
}
|
||||
catch (TypesException)
|
||||
{
|
||||
caught = true;
|
||||
}
|
||||
assert(caught, "whitespace-only should throw");
|
||||
|
||||
// trailing garbage
|
||||
caught = false;
|
||||
try
|
||||
{
|
||||
DepConstraint.parse("libfoo>=2.0 extra");
|
||||
assert(false, "expected exception");
|
||||
}
|
||||
catch (TypesException)
|
||||
{
|
||||
caught = true;
|
||||
}
|
||||
assert(caught, "trailing garbage should throw");
|
||||
}
|
||||
|
||||
@safe unittest
|
||||
{
|
||||
// ── BuildPlan ────────────────────────────────────────
|
||||
BuildPlan bp;
|
||||
assert(bp.isEmpty());
|
||||
|
||||
bp.add("mypkg", "path/to/mypkg.recipe", Source.recipe);
|
||||
assert(!bp.isEmpty());
|
||||
|
||||
bp.add("libbar", "path/to/libbar.recipe", Source.recipe);
|
||||
|
||||
auto ordered = bp.order();
|
||||
assert(ordered.length == 2);
|
||||
assert(ordered[0].name == "mypkg");
|
||||
assert(ordered[0].recipePath == "path/to/mypkg.recipe");
|
||||
assert(ordered[0].source == Source.recipe);
|
||||
assert(ordered[1].name == "libbar");
|
||||
assert(ordered[1].recipePath == "path/to/libbar.recipe");
|
||||
assert(ordered[1].source == Source.recipe);
|
||||
}
|
||||
|
||||
@safe unittest
|
||||
{
|
||||
// ── BuildResult.failedNames ──────────────────────────
|
||||
BuildResult br;
|
||||
assert(br.failedNames().length == 0);
|
||||
|
||||
br.failed ~= BuildFailure("foo", "compile error");
|
||||
br.failed ~= BuildFailure("bar", "link error");
|
||||
br.succeeded ~= "baz";
|
||||
|
||||
auto names = br.failedNames();
|
||||
assert(names.length == 2);
|
||||
assert(names[0] == "foo");
|
||||
assert(names[1] == "bar");
|
||||
}
|
||||
Reference in New Issue
Block a user