Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions docs/install-wizard.md
Original file line number Diff line number Diff line change
Expand Up @@ -787,9 +787,13 @@ publishes main builds, so main is built on this computer
Gentleman-Programming/gentle-ai (GitHub API, raw SHA), runs
`go install github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai@<sha>`
in a sealed Go environment (`GOPROXY=https://proxy.golang.org`,
`GOSUMDB=sum.golang.org`, `GOTOOLCHAIN=local`, `GOFLAGS=-modcacherw`), requires
the binary's version to end in that commit's first 12 characters, keeps it
under `<config home>/main/gentle-ai/<sha>/` and registers it as the
`GOSUMDB=sum.golang.org`, `GOTOOLCHAIN=local`, `GOFLAGS=-modcacherw`),
asks Go (`go list -m -json`, same environment) which module version that
commit is (a release tag such as `v4.2.0`, or a pseudo-version) and requires
its origin to be that exact commit, requires the binary to embed that module
version (`go version -m`) and to report it, all in the `.build` work
folder, and only then moves it to `<config home>/main/gentle-ai/<sha>/`
(a failed rebuild leaves an already registered build in place) and registers it as the
`gentle-pi.dev-binary/v1` override, which never falls back to the pinned
binary silently.
2. `install-shell-main`: downloads the latest `main` commit's source tarball,
Expand Down
63 changes: 50 additions & 13 deletions scripts/main-channel.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
// an exact commit SHA resolved at install or upgrade time:
// - Gentle AI: `go install <module>@<sha>` in a sealed Go environment, so the
// module source is verified by Go's checksum database (sum.golang.org), as
// the Windows release build already is. The binary must report a version
// naming that commit, then it is activated through the existing dev-binary
// override, which never falls back to the pinned binary silently.
// the Windows release build already is. Go must resolve the commit to a
// module version (a tag or a pseudo-version) whose origin is that commit,
// and the binary must embed that version and report it, then it is
// activated through the existing dev-binary override, which never falls
// back to the pinned binary silently.
// - Gentle Shell: the exact commit's source tarball, versioned
// `<version>-main.<sha12>` and packed without `prepack` (which runs the full
// test suite), then installed globally like any local tarball.
Expand All @@ -18,13 +20,14 @@ import { goAcquisition, pnpmGlobalBin, requirements } from "./installer-prefligh

export const SHELL_REPOSITORY = "Gentleman-Programming/gentle-shell";
export const GENTLE_AI_REPOSITORY = "Gentleman-Programming/gentle-ai";
export const GENTLE_AI_MODULE = "github.com/gentleman-programming/gentle-ai/v4";
export const GENTLE_AI_MAIN_PACKAGE = "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai";
export const CHANNEL_SCHEMA = "gentle-shell.channel/v1";
export const CHANNELS = Object.freeze(["release", "main"]);

const SHA = /^[0-9a-f]{40}$/;
const MINUTE = 60_000;
const deadlines = Object.freeze({ build: 15 * MINUTE, version: 30_000, extract: 2 * MINUTE, pack: 5 * MINUTE });
const deadlines = Object.freeze({ build: 15 * MINUTE, resolve: 2 * MINUTE, version: 30_000, extract: 2 * MINUTE, pack: 5 * MINUTE });
const MAX_SOURCE_BYTES = 100 * 1024 * 1024;

export class MainChannelError extends Error {
Expand Down Expand Up @@ -136,23 +139,57 @@ export async function buildMainGentleAi({ commit, ctx, platform, goPath, run, fs
const executable = platform === "win32" ? "gentle-ai.exe" : "gentle-ai";
const directory = join(configHome(ctx), "main", "gentle-ai", commit);
const buildDirectory = join(configHome(ctx), "main", ".build");
// Verified in the work folder before it replaces anything, so a failed rebuild of
// an already registered commit never touches the override; the final move stays
// on one filesystem, and uninstall already accepts a leftover .build.
const staging = join(buildDirectory, commit);
await fs.rm(buildDirectory, { recursive: true, force: true });
const env = sealedGoEnvironment(goPath, buildDirectory, platform);
for (const path of [env.GOBIN, env.GOPATH, env.GOMODCACHE, env.GOCACHE, join(buildDirectory, "tmp")]) await fs.mkdir(path, { recursive: true });
try {
const install = await run(goPath, ["install", `${GENTLE_AI_MAIN_PACKAGE}@${commit}`], { env, cwd: buildDirectory, deadlineMs: deadlines.build, stderrTail: 4096 });
if (!succeeded(install)) throw new MainChannelError("main-gentle-ai-build-failed", `go install ${GENTLE_AI_MAIN_PACKAGE}@${commit} failed`, stderrCause(install));
await fs.mkdir(directory, { recursive: true });
const binaryPath = join(directory, executable);
await fs.copyFile(join(env.GOBIN, executable), binaryPath);
if (platform !== "win32") await fs.chmod(binaryPath, 0o755);
// Go stamps the module pseudo-version, whose suffix is the commit's first 12 characters.
const probe = await run(binaryPath, ["version"], { env: { PATH: env.PATH }, deadlineMs: deadlines.version });
await fs.mkdir(staging, { recursive: true });
const stagedPath = join(staging, executable);
await fs.copyFile(join(env.GOBIN, executable), stagedPath);
if (platform !== "win32") await fs.chmod(stagedPath, 0o755);
const reject = async (message, cause) => {
throw new MainChannelError("main-gentle-ai-unverified", message, cause);
};
const short = commit.slice(0, 12);
const goOptions = { env, cwd: buildDirectory, deadlineMs: deadlines.version, stderrTail: 4096 };
// Go resolves the commit through the proxy and checksum database: a tag commit is
// stamped with its tag, any other with a pseudo-version, so the version is not guessed.
// Resolving may reach the proxy, so it gets a network deadline, not the local one.
const listed = await run(goPath, ["list", "-m", "-json", `${GENTLE_AI_MODULE}@${commit}`], { ...goOptions, deadlineMs: deadlines.resolve });
if (!succeeded(listed)) return await reject(`Go could not resolve Gentle AI commit ${short}`, stderrCause(listed));
let resolved = null;
try {
resolved = JSON.parse(String(listed.stdout ?? ""));
} catch {
return await reject(`Go returned unreadable module information for Gentle AI commit ${short}`);
}
const resolvedVersion = typeof resolved?.Version === "string" ? resolved.Version : "";
const originHash = resolved?.Origin?.Hash;
if (resolvedVersion === "") return await reject(`Go resolved Gentle AI commit ${short} to no module version`);
if (originHash !== commit) {
return await reject(`Go resolved Gentle AI commit ${short} to ${originHash ? `commit ${originHash}` : "no commit"}, not the requested commit`);
}
const embedded = await run(goPath, ["version", "-m", stagedPath], goOptions);
if (!succeeded(embedded)) return await reject("the built Gentle AI's embedded module version could not be read", stderrCause(embedded));
const modLine = String(embedded.stdout ?? "").split(/\r?\n/).map((line) => line.trim().split(/\s+/)).find((fields) => fields[0] === "mod" && fields[1] === GENTLE_AI_MODULE);
if (modLine?.[2] !== resolvedVersion) {
return await reject(`the built Gentle AI embeds module version ${modLine?.[2] ?? "none"}, expected ${resolvedVersion} for commit ${short}`);
}
const probe = await run(stagedPath, ["version"], { env: { PATH: env.PATH }, deadlineMs: deadlines.version });
const match = succeeded(probe) && /^gentle-ai (\S+)$/m.exec(String(probe.stdout ?? ""));
if (!match || !match[1].endsWith(`-${commit.slice(0, 12)}`)) {
await fs.rm(directory, { recursive: true, force: true });
throw new MainChannelError("main-gentle-ai-unverified", `the built Gentle AI does not report commit ${commit.slice(0, 12)}`);
const expectedVersion = resolvedVersion.replace(/^v/, "");
if (!match || match[1] !== expectedVersion) {
return await reject(`the built Gentle AI reports version ${match ? match[1] : "none"}, expected ${expectedVersion} for commit ${short}`);
}
await fs.mkdir(directory, { recursive: true });
const binaryPath = join(directory, executable);
await fs.rename(stagedPath, binaryPath);
registerGentleAiDevBinary(binaryPath, ctx, platform);
return { binaryPath, version: match[1] };
} finally {
Expand Down
10 changes: 9 additions & 1 deletion tests/main-channel-upgrade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as fsPromises from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { CHANNEL_SCHEMA, MainChannelError, ownerManager, readChannel, removeGlobalShell, runUpgrade, writeChannel } from "../scripts/main-channel.mjs";
import { CHANNEL_SCHEMA, GENTLE_AI_MODULE, MainChannelError, ownerManager, readChannel, removeGlobalShell, runUpgrade, writeChannel } from "../scripts/main-channel.mjs";
import { requirements } from "../scripts/installer-preflight.mjs";

const AI_SHA = "1f9d5e6423e37f7d2316859045f379ba9b5d8c3a";
Expand Down Expand Up @@ -55,8 +55,16 @@ function world({ owner = "pnpm", latest = "4.1.0", commits = { ai: AI_SHA, shell
return { code: 0, stdout: "" };
}
if (argv[0] === "version" && command === `${bin}/go`) {
if (argv[1] === "-m") {
const commit = String(argv[2] ?? "").split("/").at(-2) ?? "";
return { code: 0, stdout: `${command} (binary)\n\tmod\t${GENTLE_AI_MODULE}\tv4.0.1-0.20261008202137-${commit.slice(0, 12)}\th1:J8EluS7QrfM+m3vmQlF26B/shZRjD6rBfsKtrABL5mc=\n` };
}
return { code: 0, stdout: `go version go${requirements.go} ${platform}/amd64\n` };
}
if (argv[0] === "list" && command === `${bin}/go`) {
const commit = String(argv[3] ?? "").slice(`${GENTLE_AI_MODULE}@`.length);
return { code: 0, stdout: JSON.stringify({ Path: GENTLE_AI_MODULE, Version: `v4.0.1-0.20261008202137-${commit.slice(0, 12)}`, Origin: { VCS: "git", URL: "https://github.com/gentleman-programming/gentle-ai", Hash: commit } }) };
}
if (argv[0] === "version") {
const sha = command.split("/").at(-2) ?? "";
return { code: 0, stdout: `gentle-ai 4.0.1-0.20261008202137-${sha.slice(0, 12)}\n` };
Expand Down
Loading
Loading