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