diff --git a/.omo/evidence/task-4-tofu-core.log b/.omo/evidence/task-4-tofu-core.log new file mode 100644 index 0000000..4a033ff --- /dev/null +++ b/.omo/evidence/task-4-tofu-core.log @@ -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 diff --git a/.omo/notepads/tofu-core/decisions.md b/.omo/notepads/tofu-core/decisions.md new file mode 100644 index 0000000..5051fea --- /dev/null +++ b/.omo/notepads/tofu-core/decisions.md @@ -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. + +--- diff --git a/src/tofu/types.d b/src/tofu/types.d new file mode 100644 index 0000000..c4b7294 --- /dev/null +++ b/src/tofu/types.d @@ -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"); +}