2f0d457a97ba

home: detect platform-specific attr (user-arch-os) with fallback chain for home-manager flake ref

time
2026-06-15 12:24 UTC · 3 months ago
machine
koh2
agent
claude-opus-4-8
session
murazaki feedback fixes
parent
7acbc7250302
3 files src/commands.zig+49 −11src/exec.zig+51 −11src/output.zig+6 −0
src/commands.zig+49 −11
@@ -1500,25 +1500,63 @@15001500     }15011501 15021502     output.homeApplyStart(ctx.machine.name);1503-    const flake_ref = exec.homeFlakeRef(ctx.gpa, ctx.io, ctx.environ);1504-    defer if (flake_ref) |r| ctx.gpa.free(r);1505-    if (flake_ref) |ref| {1506-        output.printSubstep("home-manager switch --flake {s}", .{ref});1507-    } else {1508-        output.printSubstep("home-manager switch", .{});1509-    }15101503 1504+    // Flake-based home config: switch by attribute with a graceful fallback1505+    // chain. A channel-based config (no flake.nix) uses a plain switch.1506+    const dir = exec.homeFlakeDir(ctx.gpa, ctx.io, ctx.environ);1507+    defer if (dir) |d| ctx.gpa.free(d);1508+    if (dir) |d| return homeApplyFlake(ctx, d);1509+1510+    output.printSubstep("home-manager switch", .{});15111511     const start = std.Io.Clock.awake.now(ctx.io);1512-    const code = if (flake_ref) |ref|1513-        try exec.homeManagerSwitchFlake(ctx.io, &ctx.machine, ctx.gpa, ref)1514-    else1515-        try exec.homeManagerSwitch(ctx.io, &ctx.machine, ctx.gpa);1512+    const code = try exec.homeManagerSwitch(ctx.io, &ctx.machine, ctx.gpa);15161513     if (code != 0) return error.BuildFailed;15171514 15181515     const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());15191516     const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));15201517     output.homeApplyDone(homeCurrentGen(ctx), elapsed);15211518     runPostHook(ctx, "post-home");1519+}1520+1521+// Switch a flake-based home config, trying the most specific attribute first:1522+//   <dir>#<user>-<arch>-<os>  →  <dir>#<user>  →  <dir>  (home-manager resolves)1523+// Each fallback fires only on a missing-attribute error, never on a real build1524+// failure. Mirrors how ${USER}-${system} homeConfigurations keys are written.1525+fn homeApplyFlake(ctx: Ctx, dir: []const u8) !void {1526+    const user = ctx.environ.get("USER") orelse "default";1527+1528+    var refs: std.ArrayList([]const u8) = .empty;1529+    defer {1530+        for (refs.items) |r| ctx.gpa.free(r);1531+        refs.deinit(ctx.gpa);1532+    }1533+    if (exec.homeManagerAttr(ctx.gpa, ctx.io, &ctx.machine, ctx.environ) catch null) |attr| {1534+        defer ctx.gpa.free(attr);1535+        try refs.append(ctx.gpa, try std.fmt.allocPrint(ctx.gpa, "{s}#{s}", .{ dir, attr }));1536+    }1537+    try refs.append(ctx.gpa, try std.fmt.allocPrint(ctx.gpa, "{s}#{s}", .{ dir, user }));1538+    try refs.append(ctx.gpa, try ctx.gpa.dupe(u8, dir));1539+1540+    const start = std.Io.Clock.awake.now(ctx.io);1541+    for (refs.items, 0..) |ref, i| {1542+        output.printSubstep("home-manager switch --flake {s}", .{ref});1543+        switch (try exec.homeManagerSwitchFlake(ctx.io, &ctx.machine, ctx.gpa, ref)) {1544+            .ok => {1545+                const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());1546+                const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));1547+                output.homeApplyDone(homeCurrentGen(ctx), elapsed);1548+                runPostHook(ctx, "post-home");1549+                return;1550+            },1551+            // Attr not defined by the flake — drop to the next candidate.1552+            .attr_missing => {1553+                if (i + 1 < refs.items.len) output.homeAttrFallback(ref);1554+                continue;1555+            },1556+            .failed => return error.BuildFailed,1557+        }1558+    }1559+    return error.BuildFailed;15221560 }15231561 15241562 // --- Service commands ---

view file

src/exec.zig+51 −11
@@ -1527,19 +1527,48 @@15271527     return .none;15281528 }15291529 1530-// If the home-manager config dir holds a flake.nix, returns the flake ref1531-// "<dir>#<user>" for `home-manager switch --flake`. Username comes from $USER.1532-// Caller frees. null when there's no flake.nix (channel-based home config).1533-pub fn homeFlakeRef(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) ?[]const u8 {1530+// The home-manager config dir when it holds a flake.nix (a flake-based home1531+// config that `home-manager switch --flake` drives). null for a channel-based1532+// config, which uses a plain `home-manager switch`. Caller frees.1533+pub fn homeFlakeDir(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) ?[]const u8 {15341534     const dir = homeConfigDir(gpa, environ) orelse return null;1535-    defer gpa.free(dir);1536-    const flake = std.fmt.allocPrint(gpa, "{s}/flake.nix", .{dir}) catch return null;1535+    const flake = std.fmt.allocPrint(gpa, "{s}/flake.nix", .{dir}) catch {1536+        gpa.free(dir);1537+        return null;1538+    };15371539     defer gpa.free(flake);1538-    if (!pathExists(io, flake)) return null;1539-    const user = environ.get("USER") orelse "default";1540-    return std.fmt.allocPrint(gpa, "{s}#{s}", .{ dir, user }) catch null;1540+    if (!pathExists(io, flake)) {1541+        gpa.free(dir);1542+        return null;1543+    }1544+    return dir;15411545 }15421546 1547+// Platform-specific home-manager attribute "<user>-<arch>-<os>" (e.g.1548+// murazaki-x86_64-linux) — the homeConfigurations key many flakes derive from1549+// ${USER}-${system}. arch/os come from uname on the local host (home commands1550+// are local-only). Caller frees.1551+pub fn homeManagerAttr(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine, environ: *const std.process.Environ.Map) ![]const u8 {1552+    const user = environ.get("USER") orelse "user";1553+1554+    const arch_r = try capture(gpa, io, machine, &.{ "uname", "-m" });1555+    defer gpa.free(arch_r.stdout);1556+    defer gpa.free(arch_r.stderr);1557+    if (arch_r.exit_code != 0) return error.ProcessFailed;1558+    const arch = std.mem.trim(u8, arch_r.stdout, " \t\r\n");1559+1560+    const os_r = try capture(gpa, io, machine, &.{ "uname", "-s" });1561+    defer gpa.free(os_r.stdout);1562+    defer gpa.free(os_r.stderr);1563+    if (os_r.exit_code != 0) return error.ProcessFailed;1564+    const os = try gpa.dupe(u8, std.mem.trim(u8, os_r.stdout, " \t\r\n"));1565+    defer gpa.free(os);1566+    for (os) |*ch| ch.* = std.ascii.toLower(ch.*);1567+1568+    if (arch.len == 0 or os.len == 0) return error.ProcessFailed;1569+    return std.fmt.allocPrint(gpa, "{s}-{s}-{s}", .{ user, arch, os });1570+}1571+15431572 // Map a nixos-version string to the matching home-manager branch: an unstable15441573 // channel ("…pre…"/"unstable") → master; a stable release like15451574 // "25.05.20250115.abcdef (Warbler)" → release-25.05. Anything unparseable@@ -1621,13 +1650,24 @@16211650     return r.exit_code;16221651 }16231652 1624-pub fn homeManagerSwitchFlake(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, flake_ref: []const u8) !u8 {1653+// Outcome of a flake-based switch, so the caller can fall back to a less1654+// specific attribute on a missing-attribute evaluation error — but never on a1655+// genuine build failure.1656+pub const SwitchOutcome = enum { ok, attr_missing, failed };1657+1658+pub fn homeManagerSwitchFlake(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, flake_ref: []const u8) !SwitchOutcome {16251659     output.flush();16261660     const r = try capture(gpa, io, machine, &.{ "home-manager", "switch", "--flake", flake_ref });16271661     defer gpa.free(r.stdout);16281662     defer gpa.free(r.stderr);1663+    if (r.exit_code == 0) return .ok;1664+    // A flake that doesn't define this homeConfiguration fails during evaluation1665+    // with "does not provide attribute" — surface that so the caller can retry a1666+    // different attr rather than treating it as a build failure.1667+    if (std.mem.indexOf(u8, r.stderr, "does not provide attribute") != null)1668+        return .attr_missing;16291669     errors.setBuildStderr(r.stderr);1630-    return r.exit_code;1670+    return .failed;16311671 }16321672 16331673 // home-manager has no system-profile rollback; it re-activates the previous

view file

src/output.zig+6 −0
@@ -1389,6 +1389,12 @@13891389     p("{s}home packages  {s}\n\n", .{ hdr(), machine });13901390 }13911391 1392+// A flake attribute wasn't defined — note it (dim) before trying the next, less1393+// specific candidate in the home-apply fallback chain.1394+pub fn homeAttrFallback(ref: []const u8) void {1395+    p("   {s}{s} not defined — trying next{s}\n", .{ c(DIM), ref, c(RESET) });1396+}1397+13921398 pub fn homeInitStart(switch_on_init: bool, dir: ?[]const u8) void {13931399     const target = dir orelse "~/.config/home-manager";13941400     if (switch_on_init) {

view file