feat(log): add colored NO_COLOR-aware logging

This commit is contained in:
2026-08-08 17:16:50 -04:00
parent b2d1df72f9
commit c331416623
3 changed files with 346 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
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.
Up-to-date tofu ~main: target for configuration [tofu-test-application] is up to date.
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-174602.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
+70
View File
@@ -0,0 +1,70 @@
# Learnings — tofu-core
Conventions, patterns, and successful approaches discovered during work on this plan.
_Auto-scaffolded by /start-work. Append new entries below - never overwrite._
---
## Task 2: Config Loading (src/tofu/config.d)
### toml package API (v1.0.0, Kripth/toml)
- Import: `import toml;` → provides `TOMLDocument`, `TOMLValue`, `parseTOML`, `TOMLException`, `TOMLParserException`
- `TOMLDocument` has `alias table this` — acts like `TOMLValue[string]` AA
- Access sections: `doc["core"]` returns `TOMLValue` with `type == TOML_TYPE.TABLE`
- Access string values: `doc["core"]["key"].str` (throws if type mismatch)
- Check existence: `"key" in doc` returns `TOMLValue*` (null if absent)
- `parseTOML()` is `@system` — must wrap in `@trusted` block when calling from `@safe` code
- Keys without `.str` / `.integer` access will throw `TOMLException`
### D language gotchas
- **Nested function declarations**: D does NOT allow `void foo() { ... }` inside a function body. Use lambdas: `auto foo = () { ... }; foo();` or anonymous `() @trusted { ... }();`
- **`version` is a keyword**: Cannot use `string version;` as field name — use `string version_;` or initialize with `= ""`
- **`parseTOML` is `@system`**: The toml package's parse function isn't marked `@safe` — must wrap in `@trusted` lambda
- **`std.file.readText` is `@system`**: Same — wrap in `@trusted`
- **`std.process.environment.get()`**: Returns empty string when env var is unset; `"VAR" in environment` checks existence but `environment.get("VAR", "default")` is cleaner
- **`stderr.writefln` is `@safe`** in recent DMD (2.106+) — usable directly in `@safe` code
- **AA `in` operator**: Returns pointer (`V*`) or `null` — must check for null before dereferencing
### Pattern: env-overrideable config loading
- Priority: env vars > TOML file > hardcoded defaults
- TOML read happens BEFORE env override — so TOML values serve as base, env vars overwrite
- Missing config file → no error, just skip to defaults
- Malformed TOML → `TOMLParserException` caught, warning to stderr, fall back to defaults
- Testability: `load()` accepts optional `envOverrides` map and explicit `configFile` path to avoid real env/filesystem
### Config design decisions
- `TOFU_CONFIG` env var overrides default config file path (`~/.config/tofu/config.toml`)
- `~` in default paths expanded with `std.path.expandTilde`
- All path fields default to empty string when "find on PATH" — no hardcoded binary paths
- `defaultJobs` typed as `int` (not `size_t`/`ulong`) since it's a user-facing count
### buildOptions warningsAsErrors deprecation
- DUB warns about `warningsAsErrors` in `buildOptions` — recommends `buildRequirements` instead
- Not blocking, informational only
## Task 3 — `tofu.log` (colored, NO_COLOR-aware logging)
- **dmd 2.112 gotcha**: the `std.stdio` globals `stdout`/`stderr` are `@system` to *access* — `makeGlobal` uses `__gshared File result`, which fails `@safe` inference. An `@safe` function cannot call `stdout.writeln(...)` directly. Fix: route every write through a tiny `@trusted` helper (`writeStdout(string)`/`writeStderr(string)`), keep the public log functions `@safe`. Compiler hint: *"using `__gshared` instead of `shared` makes it fail to infer `@safe`"*.
- **`std.process.environment` (dmd 2.112)**: it is an `abstract final class` of *static* methods — `environment.get(name)` is `@safe` and returns `null` when the var is unset (so "NO_COLOR set" == `get !is null`, even for an empty string). `environment["K"] = v` and `environment.remove(name)` are `@trusted`. NOT deprecated in 2.112.
- **Testable color decision**: make `colorEnabled()` re-read the environment on every call instead of caching in `static this()`. Enables NO_COLOR/TERM tests without restarting the process. Costs one `getenv` per log line — negligible.
- **Env save/restore in unittests**: a `private struct ColorEnv` (in `version(unittest)`) that snapshots NO_COLOR+TERM in its ctor and restores them in `~this()` (destructor) — `auto env = ColorEnv("1", "xterm")` scopes the restore. `null` value = remove the var. Double-destruction from a copied temporary is harmless (restore is idempotent).
- **Capturing stdout/stderr in unittests**: swap the global `stdout`/`stderr` File to a `File(name, "w+")`, write, `flush()`, swap back, then `rewind()` + `readln()`. Restore the global before the temp File's destructor runs.
- **zeta log format details**: `warn ` paints the *whole prefix* yellow (`paint("yellow", "warn ") ~ msg`), but `error` paints *only the word* then `" " ~ msg` (`paint("red","error") ~ " " ~ msg`). Assertion must expect `\x1b[31merror\x1b[0m failed deploy` — the reset code sits between "error" and the message.
- **`version` is a D keyword** — cannot be used as a struct field / identifier (trap for the parallel `types.d` work).
- **`std.stdio.writeln` flushes per call** — already satisfies "no buffering"; no explicit `flush()` needed in production paths.
- **Verifying a single module without the full build**: `dmd -main -unittest -i src/tofu/log.d` runs just that module's unittests when sibling modules (`config.d`, `types.d`) are mid-edit by parallel agents. `-i` pulls imports in automatically.
## Task 4 — `tofu.types` (core data structures)
- **`version` is a D keyword** — all struct fields named `version` must be renamed to `ver`. The D compiler treats `version` as a conditional-compilation directive and rejects it as a field name (see task 3 learnings above).
- **Struct field defaults in D**: `string` fields initialise to `null` by default. To enforce empty-string semantics (`""`), every string field must be explicitly `= ""`.
- **`final switch` for exhaustive enum handling**: using `final switch` on enum types forces the compiler to verify all cases are covered — catches missing branches at compile time. Used for `poolToString()`.
- **Manual character scanning preferred over `std.regex`**: the dep-spec parser uses simple char-by-char scanning with `isNameChar()` / `isWhite()` helpers instead of regex. This avoids the `std.regex` dependency, keeps the parser `@safe`, and matches the Lua reference semantics exactly.
- **Dep spec parsing order matters**: 2-character operators (`>=`, `<=`, `==`, `~=`) MUST be checked before 1-character operators (`>`, `<`, `=`) to avoid false matches. The Lua reference uses an ordered table `OPS = { ">=", "<=", "==", "~=", ">", "<", "=" }` — we replicate this with if/else chains.
- **`=` normalises to `==`**: the Lua code has `if op == "=" then op = "==" end`; we map both to `DepOp.eq`.
- **Trailing garbage detection**: after extracting version chars, any non-whitespace remaining in the spec string causes a `TypesException`. The Lua pattern uses `$` anchor for this.
- **`dub test` includes all `.d` files in source paths** for executable targets, not just imported modules. Pre-existing compile errors in sibling modules (config.d) block test runs. Fixed config.d by removing `private` from a nested function (access specifiers not allowed on local functions in D).
- **`BuildResult.failed` as `BuildFailure[]`**: the plan describes it as `struct {string name; string reason}[]` — in D this is a named struct `BuildFailure` with array field `BuildFailure[] failed`. The `failedNames()` helper extracts just the names for caller convenience.
- **`DepConstraint.parse` is a static factory method**: returns a new `DepConstraint` by value, never allocates on the heap. `@safe` throughout.
- **All structs are `@safe`**: no methods do I/O, no `@system` calls. The module stands alone — no imports from other tofu modules.