Skip to content
Closed
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
21 changes: 16 additions & 5 deletions apps/server/scripts/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import { ChildProcess, ChildProcessSpawner } from "effect/process";

import { DEVELOPMENT_ICON_OVERRIDES } from "../../../scripts/lib/brand-assets.ts";
import { findEsmImportsOfExternalPackages } from "../../../scripts/lib/cli-executable-imports.ts";
import {
buildLinuxArm64Sea,
isLinuxArm64SeaTarget,
resolveCliExecutableTarget,
} from "../../../scripts/lib/cli-sea.ts";
import rootPackageJson from "../../../package.json" with { type: "json" };
import { resolveSpawnCommand } from "@t3tools/shared/shell";
import {
Expand Down Expand Up @@ -118,17 +123,15 @@ const buildExeCmd = Command.make(
const serverDir = path.join(repoRoot, "apps/server");

yield* Effect.log("[cli] Building single-executable...");
const target = yield* resolveCliExecutableTarget(Option.getOrUndefined(config.target));
const spawnCommand = yield* resolveSpawnCommand("vp", ["pack"]);
yield* runCommand(
ChildProcess.make(spawnCommand.command, spawnCommand.args, {
cwd: serverDir,
env: {
...process.env,
T3CODE_PACK_EXE: "1",
...Option.match(config.target, {
onNone: () => ({}),
onSome: (target) => ({ T3CODE_PACK_EXE_TARGET: target }),
}),
T3CODE_PACK_EXE_TARGET: target ?? "",
},
stdout: config.verbose ? "inherit" : "ignore",
stderr: "inherit",
Expand All @@ -142,13 +145,21 @@ const buildExeCmd = Command.make(
if (specifiers.length > 0) {
return yield* new ServerCliExecutableImportError({ bundlePath, specifiers });
}
if (yield* isLinuxArm64SeaTarget(target)) {
yield* buildLinuxArm64Sea({
bundlePath,
outputPath: path.join(serverDir, "dist-exe", target ? "t3-linux-arm64" : "t3"),
nodeVersion: rootPackageJson.engines.node,
crossTarget: Boolean(target),
}).pipe(Effect.scoped);
}
yield* Effect.log(
"[cli] Built dist-exe/t3 (expects client/, resource-monitor/, and the runtime-external node_modules beside it; scripts/build-cli-archive.ts assembles that tree)",
);
}),
).pipe(
Command.withDescription(
`Build the server as a Node single-executable. Native builds require Node ${rootPackageJson.engines.node}; explicit cross-target builds need a Node 25.7+ host for --build-sea. The binary still resolves native packages from a node_modules tree beside it.`,
`Build the server as a Node single-executable. Native builds require Node ${rootPackageJson.engines.node}; Linux ARM64 cross-target builds require the same version; other cross-target builds need a Node 25.7+ host for --build-sea. The binary still resolves native packages from a node_modules tree beside it.`,
),
);

Expand Down
8 changes: 6 additions & 2 deletions apps/server/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import "vite-plus/test/config";
import * as Effect from "effect/Effect";
import { defineConfig, mergeConfig } from "vite-plus";

import baseConfig from "../../vite.config.ts";
import { loadRepoEnv } from "../../scripts/lib/public-config.ts";
import { isLinuxArm64SeaTarget, resolveCliExecutableTarget } from "../../scripts/lib/cli-sea.ts";
import rootPackageJson from "../../package.json" with { type: "json" };
import packageJson from "./package.json" with { type: "json" };
import { WeightedShardSequencer } from "./src/testUtils/weightedShardSequencer.ts";
Expand Down Expand Up @@ -38,7 +40,7 @@ const SEA_TARGETS = {
"win-arm64": { platform: "win", arch: "arm64" },
"win-x64": { platform: "win", arch: "x64" },
} as const;
const packExecutableTarget = process.env.T3CODE_PACK_EXE_TARGET?.trim();
const packExecutableTarget = Effect.runSync(resolveCliExecutableTarget());
const cliNodeVersion = rootPackageJson.engines.node;
if (packExecutable && !packExecutableTarget && process.versions.node !== cliNodeVersion) {
throw new Error(
Expand Down Expand Up @@ -83,7 +85,9 @@ export default mergeConfig(
outDir: packExecutable ? "dist-exe" : "dist",
sourcemap: !packExecutable,
clean: true,
...(packExecutable
// SEA can only resolve built-ins; keep dynamic imports inside its one module.
...(packExecutable ? { outputOptions: { codeSplitting: false } } : {}),
...(packExecutable && !Effect.runSync(isLinuxArm64SeaTarget(packExecutableTarget))
? {
exe: {
fileName: "t3",
Expand Down
2 changes: 1 addition & 1 deletion docs/operations/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ the version.
- Includes Electron auto-update metadata (for example `latest*.yml`, `nightly*.yml`, and `*.blockmap`) in release assets.
- Builds a self-contained CLI archive per platform (`t3-<version>-<platform>-<arch>.tar.gz`, `.zip` on Windows) in the same job as that target's desktop artifact and attaches them to the GitHub Release with a `SHA256SUMS` file, on every channel, for five targets: macOS arm64, Linux x64 and arm64, Windows x64 and arm64. Every archive is smoke-tested on hardware of its own architecture, and every one but the Windows archives is also built and signed there. There is no macOS x64 archive: Node single-executables are unsupported on x64 macOS (the SEA docs list macOS as arm64 only) and the binary segfaults on start; the x64 desktop app is Electron and unaffected.
- The archive holds the server as a Node single-executable (`scripts/build-cli-archive.ts`), so unpacking it needs neither Node, npm, nor a compiler. It is the only form in which T3 Code manages a runtime: the desktop's SSH environments, the boot service, `t3 update`, and the install scripts all download and verify this archive against `SHA256SUMS`. The `t3` npm package exists for people who run `npx t3` or `npm install -g t3` themselves, and it downloads this same archive too; nothing in the product installs from npm. The `curl | sh` installers are `scripts/install.sh` and `scripts/install.ps1`; the marketing site copies them into its `public/` at build time (`apps/marketing/scripts/stage-install-scripts.mjs`) and serves them at `t3.codes/install.sh` and `/install.ps1`.
- The root `package.json` pins `engines.node` to the exact Node version used by development, CI and standalone executables. Keep it exact: cross-target builds download that version, and native executable builds reject a different host Node version. Runtime changes need a new T3 release version because installed runtimes are cached by that version.
- The root `package.json` pins `engines.node` to the exact Node version used by development, CI and standalone executables. Keep it exact: cross-target builds download that version, and native executable builds reject a different host Node version. Linux arm64 cross-target builds also require that host version to generate a matching SEA blob. Runtime changes need a new T3 release version because installed runtimes are cached by that version.
- macOS archives are signed with the Developer ID certificate and notarized when the Apple secrets are present (ad hoc otherwise, which still runs from `curl`/`tar` installs). Windows executables use the same Azure Trusted Signing setup as the installer. Every native addon in the macOS archive is signed too, since the hardened runtime refuses unsigned libraries.
- Each archive is extracted and executed (`scripts/smoke-cli-archive.ts`) on its build runner, or on the `verify_runner` for Windows.
- After the GitHub Release is published, publishes the `t3` npm package with OIDC trusted publishing from the same workflow file. It is only a launcher (`scripts/npm-launcher/t3.mjs`): on first run it downloads its exact version's CLI archive from the GitHub Release, verifies it against `SHA256SUMS`, and unpacks it into `<T3 home>/runtime/versions/<version>`, the layout the install scripts and `t3 update` share. The executables are not on npm. They used to be, as `@t3code/t3-<platform>-<arch>` optional dependencies, but npm's publish-time scanning held those 80-250 MB packages back for up to an hour after the launcher was live, and every install in that window got a launcher with no executable. Those packages stay on npm at their old versions and get no new ones. Because the launcher reads from the release, `publish_cli` runs after `release`, never before.
Expand Down
Loading
Loading