Browse Source
suite_tcl90 gains the no-tclsh source path: - build.zig.zon: content-hashed manifest pinning each source as a fossil per-checkin tarball (tcl 1a9c3b9d96 / thread caa1d3aa30 / tclvfs f082c47f9b / tk effef6c657 / tklib 2d53beafc0 / tcllib c784f6fda4 - the exact checkouts the promoted punk905 runtime was built from) plus critcl 3.3.1 as a github archive. All lazy: the fossil dev flow never triggers a fetch. Refresh = bump a pin (zig fetch --save). Verified honored under --build-file build905.zig. - build905.zig mode split: from the staged recipe copy the full graph registers as before; from the TRACKED suite dir (sources.config present, no ../tcl905) only the staging steps register. 'zig build stage' materializes pinned sources + a recipe copy into ../_build/<suite> (the same layout suite.tcl's fossil staging produces - both flows share the staged invocation, and the stage dir derives from the suite folder name so copy-and-tweak isolation holds). 'zig build bootstrap' chains the staged build: nested zig invocation with suite.tcl-parity cwd (parts of the recipe resolve sources cwd-relative), forward-slash prefix (the prefix flows into -DTCL_PACKAGE_PATH as a C string literal - backslashes broke the stubs compile), per-zig-version shared cache dir, hermetic-child env scrub, ReleaseFast default. - tools/stagetree.zig: host-compiled materializer. Pinned mode keys a .stage-materialized marker on the dependency's content-addressed cache path (pin bump = invalidation, stale trees deleted so orphans cannot survive); always-mode for recipe items; fossil/git checkouts at the destination are never touched (tested). Verified: 'zig build stage' against the fossil-populated stage refreshes only the recipe copy (checkouts untouched, fossil changes empty); a full copy-and-tweak suite copy (suite_tcl90zt) ran 'zig build bootstrap' from nothing but the tracked tree + pinned zig - all 7 sources fetched/staged from the manifest, all four shells built; tcl::build-info reports the same 9.0.5+1a9c3b9d... as the fossil-flow build and the zip shell boots its attached library (tzdata + autoload). Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.commaster
3 changed files with 384 additions and 0 deletions
@ -0,0 +1,68 @@ |
|||||||
|
//build.zig.zon - suite_tcl90 pinned source manifest (G-102). |
||||||
|
// |
||||||
|
//This is the CONTENT-HASHED tweak surface for the no-tclsh bootstrap flow: each |
||||||
|
//dependency pins an exact upstream state (fossil per-checkin tarball urls / |
||||||
|
//github archive urls) plus the zig package hash of the extracted tree. 'Refresh' |
||||||
|
//means bumping a pin: point .url at the new checkin/commit tarball and update |
||||||
|
//.hash ('zig fetch <url>' prints it; 'zig fetch --save=<name> <url>' edits this |
||||||
|
//file in place). Branch-tip tarball urls are deliberately NOT used - they are not |
||||||
|
//hash-stable, and the pin IS the provenance record. |
||||||
|
// |
||||||
|
//The fossil-checkout flow (suite.tcl + sources.config) remains the dev |
||||||
|
//alternative for live-branch work; sources.config documents the relationship. |
||||||
|
//Dependency names here match sources.config source names; the checkout dirs they |
||||||
|
//materialize into are the recipe-coupled dir names documented there. |
||||||
|
// |
||||||
|
//All dependencies are lazy: nothing is fetched unless the bootstrap staging flow |
||||||
|
//actually needs it (the fossil-staged flow never triggers a fetch). |
||||||
|
.{ |
||||||
|
.name = .suite_tcl90, |
||||||
|
.version = "0.0.1", |
||||||
|
.fingerprint = 0xacfa2b8743c3fafc, |
||||||
|
.minimum_zig_version = "0.16.0", |
||||||
|
.dependencies = .{ |
||||||
|
//tcl core-9-0-branch checkout 1a9c3b9d96 2026-07-16 (9.0.5 era) |
||||||
|
.tcl = .{ |
||||||
|
.url = "https://core.tcl-lang.org/tcl/tarball/1a9c3b9d965d66f69f037cab6718d64bd802ef2c/tcl.tar.gz", |
||||||
|
.hash = "N-V-__8AACdREwL2XmVaouKzc7VAtnbgvv6DiWJ9PZEpDEIA", |
||||||
|
.lazy = true, |
||||||
|
}, |
||||||
|
//thread trunk checkout caa1d3aa30 2026-07-13 (3.0.1 era) |
||||||
|
.tclthread = .{ |
||||||
|
.url = "https://core.tcl-lang.org/thread/tarball/caa1d3aa302798de2200de7297f9d4ca40102a5d/thread.tar.gz", |
||||||
|
.hash = "N-V-__8AALOqFAA-WUgJ777DabyIuinMYLe_jXido0KjnJbT", |
||||||
|
.lazy = true, |
||||||
|
}, |
||||||
|
//tclvfs trunk checkout f082c47f9b 2023-09-05 (1.4.2 era) |
||||||
|
.tclvfs = .{ |
||||||
|
.url = "https://core.tcl-lang.org/tclvfs/tarball/f082c47f9b2e04566e8ae70d56c6a02980d43674/tclvfs.tar.gz", |
||||||
|
.hash = "N-V-__8AAK_2DQDNpjv04LswPFyezzY97rkc5cuwcaqO-Nyq", |
||||||
|
.lazy = true, |
||||||
|
}, |
||||||
|
//tk core-9-0-branch checkout effef6c657 2025-04-07 (9.0.2) |
||||||
|
.tk = .{ |
||||||
|
.url = "https://core.tcl-lang.org/tk/tarball/effef6c657952d3304048267fa40a795a318997a/tk.tar.gz", |
||||||
|
.hash = "N-V-__8AANEfGQECMKAN0YhYXuom0BxQMprrwcZL_bc28RB5", |
||||||
|
.lazy = true, |
||||||
|
}, |
||||||
|
//tklib trunk checkout 2d53beafc0 2026-07-03 (0.9 era) |
||||||
|
.tklib = .{ |
||||||
|
.url = "https://core.tcl-lang.org/tklib/tarball/2d53beafc0e8621dacc75b3c809898cc9f1f92f9/tklib.tar.gz", |
||||||
|
.hash = "N-V-__8AABomLAFd2U5OW96AfpQLoLm3Q9swpQZEwslrEJx3", |
||||||
|
.lazy = true, |
||||||
|
}, |
||||||
|
//tcllib trunk checkout c784f6fda4 2026-06-30 (2.0 era) |
||||||
|
.tcllib = .{ |
||||||
|
.url = "https://core.tcl-lang.org/tcllib/tarball/c784f6fda4516f676453a2785b3c56f1ca5999f4/tcllib.tar.gz", |
||||||
|
.hash = "N-V-__8AAFvlBARw4fIY107nOJmfxdwAjr1Vr89cYx98czpF", |
||||||
|
.lazy = true, |
||||||
|
}, |
||||||
|
//critcl 3.3.1 (tag commit 254bdffe2a) |
||||||
|
.critcl = .{ |
||||||
|
.url = "https://github.com/andreas-kupries/critcl/archive/254bdffe2afc78148f79f92df8d6823a24f971bf.tar.gz", |
||||||
|
.hash = "N-V-__8AAF8-bABDlxChmtnyalKgofVfN3kzcZdmZ7pjEmE0", |
||||||
|
.lazy = true, |
||||||
|
}, |
||||||
|
}, |
||||||
|
.paths = .{""}, |
||||||
|
} |
||||||
@ -0,0 +1,173 @@ |
|||||||
|
//stagetree - materialize a source tree (or single file) into the build stage (G-102). |
||||||
|
// |
||||||
|
//Used by the suite recipe's bootstrap mode to copy zon-fetched dependency trees and |
||||||
|
//the tracked recipe files into the _build/<suite> stage layout that the staged build |
||||||
|
//invocation (and the fossil dev flow) share. Invariant guarded here: an existing |
||||||
|
//fossil/git CHECKOUT at the destination is never touched - the fossil dev flow owns |
||||||
|
//those directories. |
||||||
|
// |
||||||
|
// stagetree -key <pin|-> -src <path> -dest <path> |
||||||
|
// |
||||||
|
// -key <pin> pinned-tree mode: if <dest>/.stage-materialized already contains |
||||||
|
// exactly <pin>, the destination is up to date and nothing is done. |
||||||
|
// Otherwise the destination tree is DELETED, the source tree copied, |
||||||
|
// and the marker written. Pass the dependency's content-addressed |
||||||
|
// cache path as the pin - a pin bump changes it, invalidating the copy. |
||||||
|
// -key - always mode (recipe files): delete destination, copy, no marker. |
||||||
|
// |
||||||
|
//Informational prints go to stdout only: zig 0.16's build_runner reports any step |
||||||
|
//with nonempty stderr under a misleading 'failed command:' block even on success. |
||||||
|
const std = @import("std"); |
||||||
|
|
||||||
|
const usage = |
||||||
|
\\Usage: ./stagetree -key <pin|-> -src <path> -dest <path> |
||||||
|
; |
||||||
|
|
||||||
|
const marker_name = ".stage-materialized"; |
||||||
|
|
||||||
|
pub fn main(init: std.process.Init.Minimal) !void { |
||||||
|
var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); |
||||||
|
defer arena_state.deinit(); |
||||||
|
const arena = arena_state.allocator(); |
||||||
|
|
||||||
|
var threaded: std.Io.Threaded = .init(arena, .{ |
||||||
|
.environ = init.environ, |
||||||
|
.argv0 = .init(init.args), |
||||||
|
}); |
||||||
|
defer threaded.deinit(); |
||||||
|
const io = threaded.io(); |
||||||
|
|
||||||
|
const args = try init.args.toSlice(arena); |
||||||
|
var opt_key: ?[]const u8 = null; |
||||||
|
var opt_src: ?[]const u8 = null; |
||||||
|
var opt_dest: ?[]const u8 = null; |
||||||
|
{ |
||||||
|
var i: usize = 1; |
||||||
|
while (i < args.len) : (i += 1) { |
||||||
|
const arg = args[i]; |
||||||
|
if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "-help", arg)) { |
||||||
|
std.debug.print("{s}\n", .{usage}); |
||||||
|
return std.process.cleanExit(io); |
||||||
|
} else if (std.mem.eql(u8, "-key", arg)) { |
||||||
|
i += 1; |
||||||
|
if (i >= args.len) fatal("expected arg after '{s}'", .{arg}); |
||||||
|
opt_key = args[i]; |
||||||
|
} else if (std.mem.eql(u8, "-src", arg)) { |
||||||
|
i += 1; |
||||||
|
if (i >= args.len) fatal("expected arg after '{s}'", .{arg}); |
||||||
|
opt_src = args[i]; |
||||||
|
} else if (std.mem.eql(u8, "-dest", arg)) { |
||||||
|
i += 1; |
||||||
|
if (i >= args.len) fatal("expected arg after '{s}'", .{arg}); |
||||||
|
opt_dest = args[i]; |
||||||
|
} else { |
||||||
|
fatal("unknown argument '{s}'\n{s}", .{ arg, usage }); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
const key = opt_key orelse fatal("missing -key", .{}); |
||||||
|
const src = opt_src orelse fatal("missing -src", .{}); |
||||||
|
const dest = opt_dest orelse fatal("missing -dest", .{}); |
||||||
|
const cwd = std.Io.Dir.cwd(); |
||||||
|
|
||||||
|
//never touch a fossil/git checkout at the destination |
||||||
|
inline for (.{ ".fslckout", "_FOSSIL_", ".git" }) |vcsmark| { |
||||||
|
const p = try std.fs.path.join(arena, &.{ dest, vcsmark }); |
||||||
|
if (pathExists(cwd, io, p)) { |
||||||
|
try say(io, arena, "stagetree: {s} is a VCS checkout ({s} present) - left untouched\n", .{ dest, vcsmark }); |
||||||
|
return std.process.cleanExit(io); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const src_stat = cwd.statFile(io, src, .{}) catch |err| { |
||||||
|
fatal("cannot stat -src '{s}': {s}", .{ src, @errorName(err) }); |
||||||
|
}; |
||||||
|
|
||||||
|
const marker_path = try std.fs.path.join(arena, &.{ dest, marker_name }); |
||||||
|
const pinned = !std.mem.eql(u8, key, "-"); |
||||||
|
if (pinned and src_stat.kind == .directory) { |
||||||
|
if (cwd.readFileAlloc(io, marker_path, arena, .limited(64 * 1024))) |existing| { |
||||||
|
if (std.mem.eql(u8, std.mem.trim(u8, existing, " \r\n"), key)) { |
||||||
|
try say(io, arena, "stagetree: {s} up to date (pin match)\n", .{dest}); |
||||||
|
return std.process.cleanExit(io); |
||||||
|
} |
||||||
|
} else |_| {} |
||||||
|
} |
||||||
|
|
||||||
|
if (src_stat.kind == .file) { |
||||||
|
//single recipe file: parent dir must exist or be creatable; plain replace |
||||||
|
if (std.fs.path.dirname(dest)) |destparent| { |
||||||
|
cwd.createDirPath(io, destparent) catch |err| { |
||||||
|
fatal("cannot create '{s}': {s}", .{ destparent, @errorName(err) }); |
||||||
|
}; |
||||||
|
} |
||||||
|
cwd.deleteFile(io, dest) catch |err| switch (err) { |
||||||
|
error.FileNotFound => {}, |
||||||
|
else => fatal("cannot remove stale '{s}': {s}", .{ dest, @errorName(err) }), |
||||||
|
}; |
||||||
|
cwd.copyFile(src, cwd, dest, io, .{}) catch |err| { |
||||||
|
fatal("copy '{s}' -> '{s}' failed: {s}", .{ src, dest, @errorName(err) }); |
||||||
|
}; |
||||||
|
try say(io, arena, "stagetree: staged file {s}\n", .{dest}); |
||||||
|
return std.process.cleanExit(io); |
||||||
|
} |
||||||
|
|
||||||
|
//directory tree: delete stale destination entirely (orphan files from a previous |
||||||
|
//pin must not survive), then copy |
||||||
|
cwd.deleteTree(io, dest) catch |err| { |
||||||
|
fatal("cannot remove stale tree '{s}': {s}", .{ dest, @errorName(err) }); |
||||||
|
}; |
||||||
|
cwd.createDirPath(io, dest) catch |err| { |
||||||
|
fatal("cannot create '{s}': {s}", .{ dest, @errorName(err) }); |
||||||
|
}; |
||||||
|
|
||||||
|
var src_dir = cwd.openDir(io, src, .{ .iterate = true }) catch |err| { |
||||||
|
fatal("cannot open -src dir '{s}': {s}", .{ src, @errorName(err) }); |
||||||
|
}; |
||||||
|
defer src_dir.close(io); |
||||||
|
var dest_dir = cwd.openDir(io, dest, .{}) catch |err| { |
||||||
|
fatal("cannot open -dest dir '{s}': {s}", .{ dest, @errorName(err) }); |
||||||
|
}; |
||||||
|
defer dest_dir.close(io); |
||||||
|
|
||||||
|
var nfiles: usize = 0; |
||||||
|
var walker = try src_dir.walk(arena); |
||||||
|
defer walker.deinit(); |
||||||
|
while (try walker.next(io)) |entry| { |
||||||
|
switch (entry.kind) { |
||||||
|
.directory => { |
||||||
|
dest_dir.createDirPath(io, entry.path) catch |err| { |
||||||
|
fatal("mkdir '{s}' in '{s}': {s}", .{ entry.path, dest, @errorName(err) }); |
||||||
|
}; |
||||||
|
}, |
||||||
|
.file => { |
||||||
|
src_dir.copyFile(entry.path, dest_dir, entry.path, io, .{}) catch |err| { |
||||||
|
fatal("copy '{s}' into '{s}' failed: {s}", .{ entry.path, dest, @errorName(err) }); |
||||||
|
}; |
||||||
|
nfiles += 1; |
||||||
|
}, |
||||||
|
else => {}, |
||||||
|
} |
||||||
|
} |
||||||
|
if (pinned) { |
||||||
|
try cwd.writeFile(io, .{ .sub_path = marker_path, .data = key }); |
||||||
|
} |
||||||
|
try say(io, arena, "stagetree: staged {d} files -> {s}\n", .{ nfiles, dest }); |
||||||
|
return std.process.cleanExit(io); |
||||||
|
} |
||||||
|
|
||||||
|
fn say(io: std.Io, arena: std.mem.Allocator, comptime fmt: []const u8, args: anytype) !void { |
||||||
|
const msg = try std.fmt.allocPrint(arena, fmt, args); |
||||||
|
var f = std.Io.File.stdout(); |
||||||
|
try f.writeStreamingAll(io, msg); |
||||||
|
} |
||||||
|
|
||||||
|
fn pathExists(dir: std.Io.Dir, io: std.Io, p: []const u8) bool { |
||||||
|
dir.access(io, p, .{}) catch return false; |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
fn fatal(comptime format: []const u8, args: anytype) noreturn { |
||||||
|
std.debug.print(format, args); |
||||||
|
std.process.exit(1); |
||||||
|
} |
||||||
Loading…
Reference in new issue