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:
2026-08-08 17:17:20 -04:00
parent 3fb03b23d7
commit 348f0e3bac
3 changed files with 725 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
Warning
Warning ## Warning for package tofu ##
Warning
Warning The following compiler flags have been specified in the package description
Warning file. They are handled by DUB and direct use in packages is discouraged.
Warning Alternatively, you can set the DFLAGS environment variable to pass custom flags
Warning to the compiler, or use one of the suggestions below:
Warning
Warning warningsAsErrors: Use "buildRequirements" to control the warning level
Warning
Generating test runner configuration 'tofu-test-application' for 'application' (executable).
Warning Excluding main source file src/main.d from test.
Starting Performing "unittest" build using /usr/bin/dmd for x86_64.
Up-to-date toml 1.0.0: target for configuration [library] is up to date.
Building tofu ~main: building configuration [tofu-test-application]
src/tofu/config.d(125,9): Error: found `private` instead of statement
private @trusted void warn() {
^
src/tofu/config.d(132,1): Error: unmatched closing brace
}
^
src/tofu/types.d(147,13): Error: variable name expected after type `string`, not `version`
string version = "";
^
src/tofu/types.d(147,13): `version` is a keyword, perhaps append `_` to make it an identifier
src/tofu/types.d(147,21): Error: declaration expected, not `=`
string version = "";
^
src/tofu/types.d(159,16): Error: variable name expected after type `string`, not `version`
string version = "";
^
src/tofu/types.d(159,16): `version` is a keyword, perhaps append `_` to make it an identifier
src/tofu/types.d(159,29): Error: declaration expected, not `=`
string version = "";
^
src/tofu/types.d(185,12): Error: variable name expected after type `string`, not `version`
string version = "";
^
src/tofu/types.d(185,12): `version` is a keyword, perhaps append `_` to make it an identifier
src/tofu/types.d(185,20): Error: declaration expected, not `=`
string version = "";
^
src/tofu/types.d(268,16): Error: identifier or `new` expected following `.`, not `version`
result.version = spec[verStart .. verEnd];
^
src/tofu/types.d(326,12): Error: variable name expected after type `string`, not `version`
string version = "";
^
src/tofu/types.d(326,12): `version` is a keyword, perhaps append `_` to make it an identifier
src/tofu/types.d(326,22): Error: declaration expected, not `=`
string version = "";
^
src/tofu/types.d(366,12): Error: variable name expected after type `string`, not `version`
string version = "";
^
src/tofu/types.d(366,12): `version` is a keyword, perhaps append `_` to make it an identifier
src/tofu/types.d(366,22): Error: declaration expected, not `=`
string version = "";
^
src/tofu/types.d(405,18): Error: identifier or `new` expected following `.`, not `version`
assert(d.version == "");
^
src/tofu/types.d(411,18): Error: identifier or `new` expected following `.`, not `version`
assert(d.version == "");
^
src/tofu/types.d(422,18): Error: identifier or `new` expected following `.`, not `version`
assert(d.version == "2.0");
^
src/tofu/types.d(428,18): Error: identifier or `new` expected following `.`, not `version`
assert(d.version == "2.0");
^
src/tofu/types.d(434,18): Error: identifier or `new` expected following `.`, not `version`
assert(d.version == "2.0");
^
src/tofu/types.d(440,18): Error: identifier or `new` expected following `.`, not `version`
assert(d.version == "2.0");
^
src/tofu/types.d(446,18): Error: identifier or `new` expected following `.`, not `version`
assert(d.version == "2.0");
^
error limit (20) reached, use `-verrors=0` to show all
Error /usr/bin/dmd failed with exit code 1.
Warning
Warning ## Warning for package tofu ##
Warning
Warning The following compiler flags have been specified in the package description
Warning file. They are handled by DUB and direct use in packages is discouraged.
Warning Alternatively, you can set the DFLAGS environment variable to pass custom flags
Warning to the compiler, or use one of the suggestions below:
Warning
Warning warningsAsErrors: Use "buildRequirements" to control the warning level
Warning
Generating test runner configuration 'tofu-test-application' for 'application' (executable).
Warning Excluding main source file src/main.d from test.
Starting Performing "unittest" build using /usr/bin/dmd for x86_64.
Up-to-date toml 1.0.0: target for configuration [library] is up to date.
Building tofu ~main: building configuration [tofu-test-application]
src/tofu/config.d(126,20): Error: function `fprintf` is not callable using argument types `(File, string, immutable(char)*, immutable(char)*, int)`
fprintf(stderr, "Warning: invalid %s '%s', using default %d\n",
^
src/tofu/config.d(126,20): cannot pass argument `makeGlobal()` of type `File` to parameter `shared(_IO_FILE)* stream`
/usr/include/dlang/dmd/core/stdc/stdio.d(1347,13): `core.stdc.stdio.fprintf(shared(_IO_FILE)* stream, scope const(char*) format, scope const ...)` declared here
int fprintf(FILE* stream, scope const char* format, scope const ...);
^
src/tofu/config.d(182,32): Error: `@safe` function `tofu.config.load` cannot call `@system` function `toml.toml.parseTOML`
doc = parseTOML(content);
^
../../.dub/packages/toml/1.0.0/toml/src/toml/toml.d(431,14): `toml.toml.parseTOML` is declared here
TOMLDocument parseTOML(string data, TOMLOptions options=TOMLOptions.none) {
^
src/tofu/config.d(187,28): Error: function `fprintf` is not callable using argument types `(File, string, immutable(char)*, immutable(char)*)`
fprintf(stderr, "Warning: malformed TOML config at %s: %s\n",
^
src/tofu/config.d(187,28): cannot pass argument `makeGlobal()` of type `File` to parameter `shared(_IO_FILE)* stream`
/usr/include/dlang/dmd/core/stdc/stdio.d(1347,13): `core.stdc.stdio.fprintf(shared(_IO_FILE)* stream, scope const(char*) format, scope const ...)` declared here
int fprintf(FILE* stream, scope const char* format, scope const ...);
^
src/tofu/config.d(194,28): Error: function `fprintf` is not callable using argument types `(File, string, immutable(char)*)`
fprintf(stderr, "Warning: unreadable TOML config at %s, "
^
src/tofu/config.d(194,28): cannot pass argument `makeGlobal()` of type `File` to parameter `shared(_IO_FILE)* stream`
/usr/include/dlang/dmd/core/stdc/stdio.d(1347,13): `core.stdc.stdio.fprintf(shared(_IO_FILE)* stream, scope const(char*) format, scope const ...)` declared here
int fprintf(FILE* stream, scope const char* format, scope const ...);
^
Error /usr/bin/dmd failed with exit code 1.
Warning
Warning ## Warning for package tofu ##
Warning
Warning The following compiler flags have been specified in the package description
Warning file. They are handled by DUB and direct use in packages is discouraged.
Warning Alternatively, you can set the DFLAGS environment variable to pass custom flags
Warning to the compiler, or use one of the suggestions below:
Warning
Warning warningsAsErrors: Use "buildRequirements" to control the warning level
Warning
Generating test runner configuration 'tofu-test-application' for 'application' (executable).
Warning Excluding main source file src/main.d from test.
Starting Performing "unittest" build using /usr/bin/dmd for x86_64.
Up-to-date toml 1.0.0: target for configuration [library] is up to date.
Building tofu ~main: building configuration [tofu-test-application]
Linking tofu-test-application
Finished To force a rebuild of up-to-date targets, run again with --force
Running tofu-test-application
Warning: malformed TOML config at /tmp/tofu-test-config-bad-173904.toml: Invalid table key declaration (2:0)
Warning: invalid TOFU_DEFAULT_JOBS 'not-a-number', using default 1
3 modules passed unittests
Warning
Warning ## Warning for package tofu ##
Warning
Warning The following compiler flags have been specified in the package description
Warning file. They are handled by DUB and direct use in packages is discouraged.
Warning Alternatively, you can set the DFLAGS environment variable to pass custom flags
Warning to the compiler, or use one of the suggestions below:
Warning
Warning warningsAsErrors: Use "buildRequirements" to control the warning level
Warning
Starting Performing "debug" build using /usr/bin/dmd for x86_64.
Up-to-date toml 1.0.0: target for configuration [library] is up to date.
Up-to-date tofu ~main: target for configuration [application] is up to date.
Finished To force a rebuild of up-to-date targets, run again with --force
+24
View File
@@ -0,0 +1,24 @@
# Decisions — tofu-core
Architectural choices and rationales discovered during work on this plan.
_Auto-scaffolded by /start-work. Append new entries below - never overwrite._
---
### Task 4: `version` → `ver` rename
`version` is a reserved keyword in D (conditional compilation). All struct fields bearing this name are renamed to `ver` — PackageIndex.ver, Recipe.ver, DepConstraint.ver, CacheManifest.ver, BinaryCheckResult.ver. This keeps the API readable while avoiding the keyword conflict. Downstream modules referencing these structs must use `.ver` for the version field.
### Task 4: DepConstraint uses typed `DepOp` enum, not string op
The Lua reference stores `op` as a string (`nil`, `">="`, `"=="`, etc.). In D we use a typed `DepOp` enum (`ge, le, eq, ne, gt, lt, none`) for type safety and exhaustive switching. The `parse` factory handles string-to-enum conversion at parse time.
### Task 4: BuildPlan is a plain container — no sorting
The plan specifies that `order()` returns entries in deps-first order *guaranteed by the caller*. The struct itself is just a container with `add()` and `order()` — no topological sort or dependency resolution. Sorting logic belongs in a later module (`tofu.resolver` or similar).
### Task 4: Single exception type `TypesException`
One exception class for the entire types module — no separate subclasses per error category. The parse failures (bad dep spec, invalid pool) all throw `TypesException` with a descriptive message. Callers catch `TypesException` for all type-parsing errors.
### Task 4: Manual char scanning for dep spec parsing
Rather than pulling in `std.regex`, the parser uses hand-written `isNameChar()` and `isWhite()` helpers with a simple position cursor. This keeps the module dependency-light (only `std.ascii`). The logic is a direct 1:1 port of the Lua reference patterns: `[A-Za-z0-9_.+-]` for names/versions, whitespace `[ \t]` for separators.
---
+541
View File
@@ -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");
}