fix(errors): add actionable error messages for all 16 failure paths

This commit is contained in:
2026-08-08 18:21:17 -04:00
parent 47cfdee424
commit 854e7155f9
7 changed files with 435 additions and 9 deletions
+126 -3
View File
@@ -1,7 +1,130 @@
import std.stdio;
/// tofu — package manager for the ZereneOS Unofficial User Repository.
///
/// Entry point: parses args, acquires the global lock, dispatches to command
/// implementations, and catches ALL exceptions with actionable messages and
/// correct exit codes.
///
/// Exit codes (per plan):
/// 0 success | 1 generic | 2 pkg not found | 3 dep resolution | 4 build
/// failure | 5 install failure | 6 network | 7 config | 130 SIGINT
///
/// Implementation note:
/// Tasks 20–24 (command modules) may not exist yet in parallel execution.
/// This module dispatches via a `switch` on `Command`. Commands whose
/// modules don't exist yet print "command '<x>' not implemented yet" and
/// exit 1 — these stubs are replaced when tasks 20–24 land. Only `help`
/// is fully implemented here.
module main;
int main()
import tofu.errors; // exitCodeFor, acquireLock, releaseLock, LockException
import tofu.config; // Config, load
import tofu.cli; // Command, ParsedArgs, parseArgs, helpText, CliException
import tofu.log; // logError, logInfo
import std.stdio; // writeln, stderr
// ────────────────────────────────────────────────────────────
// SIGINT handler
// ────────────────────────────────────────────────────────────
version (Posix) {
import core.sys.posix.signal : signal, SIGINT, SIG_ERR;
import core.sys.posix.unistd : write, _exit;
__gshared bool g_interrupted;
/// Signal handler for SIGINT. Sets the global flag, writes a bare message
/// to stderr via the `write(2)` syscall (fully async-signal-safe), then calls
/// `_exit(130)` immediately (no atexit / stdio flush).
/// The @nogc attribute is required by DMD's `core.sys.posix.signal` wrapper.
extern (C) void _onSigint(int) nothrow @nogc
{
g_interrupted = true;
// write(2, ...) — POSIX syscall, no allocation, no GC, async-signal-safe.
const char[] msg = "error interrupted\n";
write(2 /* STDERR_FILENO */, msg.ptr, msg.length);
_exit(130);
}
}
// ────────────────────────────────────────────────────────────
// Main
// ────────────────────────────────────────────────────────────
int main(string[] args)
{
writeln("tofu: ZUUR package manager");
// ── 1. Register SIGINT handler (must happen early) ─────────────
version (Posix) {
if (signal(SIGINT, &_onSigint) == SIG_ERR) {
stderr.writeln("error failed to register SIGINT handler");
}
}
// ── 2. Load configuration ─────────────────────────────────────
// Config::load() never throws — falls back to defaults on errors.
auto cfg = load(null);
// ── 3. Acquire global lock (prevents concurrent tofu runs) ────
// Wrapped in try/catch: LockException has a fixed, actionable
// message and should not be remapped through exitCodeFor.
try {
acquireLock(cfg.cacheDir);
} catch (LockException e) {
logError("%s", e.msg);
return 1;
}
scope (exit) releaseLock(cfg.cacheDir);
scope (failure) releaseLock(cfg.cacheDir);
// ── 4. Check for premature SIGINT flag ────────────────────────
version (Posix) {
if (g_interrupted)
return 130;
}
// ── 5. Parse command-line args ─────────────────────────────────
// parseArgs expects argv[1..$] (no program name).
ParsedArgs pa;
try {
pa = parseArgs(args[1 .. $]);
} catch (CliException e) {
logError("%s", e.msg);
return 1;
}
// ── 6. Dispatch ────────────────────────────────────────────────
try {
final switch (pa.cmd) {
case Command.help:
writeln(tofu.cli.helpText);
return 0;
case Command.install:
logError("command 'install' not implemented yet");
return 1;
case Command.search:
logError("command 'search' not implemented yet");
return 1;
case Command.upgrade:
logError("command 'upgrade' not implemented yet");
return 1;
case Command.remove_:
logError("command 'remove' not implemented yet");
return 1;
case Command.info:
logError("command 'info' not implemented yet");
return 1;
}
} catch (Exception e) {
// Any uncaught exception during dispatch — map to exit code
int ec = exitCodeFor(e);
logError("%s", e.msg);
return ec;
}
return 0;
}
+12 -3
View File
@@ -29,6 +29,10 @@ version (unittest) {
class BuildException : Exception {
mixin basicExceptionCtors;
/// True when the error is "zeta-makepkg not found" (→ exit code 7,
/// not 4). Set by the two throw sites below.
bool toolMissing = false;
this(string message, string file = __FILE__, size_t line = __LINE__)
@safe pure nothrow {
super(message, file, line);
@@ -132,10 +136,13 @@ string runMakepkg(string recipePath, string outputDir, int jobs, bool force,
string zetaMakepkg;
if (cfg.zetaToolchainPath.length > 0) {
zetaMakepkg = cfg.zetaToolchainPath;
if (!() @trusted { return exists(zetaMakepkg); }())
throw new BuildException(
if (!() @trusted { return exists(zetaMakepkg); }()) {
auto be = new BuildException(
"zeta-makepkg not found. Install zeta-toolchain or set "
~ "TOFU_ZETA_TOOLCHAIN_PATH in ~/.config/tofu/config.toml");
be.toolMissing = true;
throw be;
}
} else {
zetaMakepkg = "zeta-makepkg";
}
@@ -169,9 +176,11 @@ string runMakepkg(string recipePath, string outputDir, int jobs, bool force,
// Binary not found on PATH (or exec failed)
if (cfg.zetaToolchainPath.length == 0
&& e.msg.indexOf("not found") >= 0) {
throw new BuildException(
auto be = new BuildException(
"zeta-makepkg not found. Install zeta-toolchain or set "
~ "TOFU_ZETA_TOOLCHAIN_PATH in ~/.config/tofu/config.toml");
be.toolMissing = true;
throw be;
}
throw new BuildException(
"failed to execute zeta-makepkg: " ~ e.msg);
+7 -1
View File
@@ -32,6 +32,10 @@ import std.string;
/// errors are propagated with their original message.
class FetchException : Exception
{
/// True when the error is "package not found in ZUUR recipes"
/// (→ exit code 2, not 6). Set by the 404-on-.recipe throw site.
bool notFound = false;
@safe this(string msg)
{
super(msg);
@@ -191,8 +195,10 @@ private void cleanupFiles(string[] paths) @trusted
{
if (e.msg.indexOf("HTTP 404") >= 0)
{
throw new FetchException(
auto fe = new FetchException(
format("package '%s' not found in ZUUR recipes", name));
fe.notFound = true;
throw fe;
}
throw new FetchException(e.msg);
}
+7 -1
View File
@@ -30,6 +30,10 @@ import std.file : exists;
/// Thrown when a package install fails.
class InstallException : Exception
{
/// True when the error is "zeta not found" (→ exit code 7,
/// not 5). Set by the throw site below.
bool toolMissing = false;
this(string message, string file = __FILE__, size_t line = __LINE__)
@safe pure nothrow
{
@@ -87,8 +91,10 @@ void runLocalProvide(string pkgName, Config cfg)
}
catch (ProcessException e)
{
throw new InstallException(
auto ie = new InstallException(
"zeta not found. Install Zeta to use package management.");
ie.toolMissing = true;
throw ie;
}
}();