Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ build/
.logs/
release/
release-mock/
packaging-output/
.t3
.idea/
apps/web/.playwright
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"dist:desktop:dmg:arm64": "node scripts/build-desktop-artifact.ts --platform mac --target dmg --arch arm64",
"dist:desktop:dmg:x64": "node scripts/build-desktop-artifact.ts --platform mac --target dmg --arch x64",
"dist:desktop:linux": "node scripts/build-desktop-artifact.ts --platform linux --target AppImage --arch x64",
"dist:desktop:deb": "node scripts/build-desktop-artifact.ts --platform linux --target deb --arch x64 --output-dir packaging-output",
"dist:desktop:rpm": "node scripts/build-desktop-artifact.ts --platform linux --target rpm --arch x64 --output-dir packaging-output",
"dist:desktop:win": "node scripts/build-desktop-artifact.ts --platform win --target nsis",
"dist:desktop:win:arm64": "node scripts/build-desktop-artifact.ts --platform win --target nsis --arch arm64",
"dist:desktop:win:x64": "node scripts/build-desktop-artifact.ts --platform win --target nsis --arch x64",
Expand Down
85 changes: 85 additions & 0 deletions scripts/build-desktop-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
resolveDesktopProductName,
resolveDesktopUpdateChannel,
resolveDesktopWebAssetBrand,
renderLinuxHeadlessLauncher,
resolveGitHubPublishConfig,
resolveMockUpdateServerPort,
resolveMockUpdateServerUrl,
Expand Down Expand Up @@ -498,6 +499,90 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
}).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
);

it.effect("includes distribution metadata in Linux package builds", () =>
Effect.gen(function* () {
const config = yield* createBuildConfig(
"linux",
"deb",
"1.2.3",
false,
false,
undefined,
undefined,
"/tmp/t3",
);

const linux = config.linux as Record<string, unknown>;
assert.deepStrictEqual(linux.target, ["deb"]);
assert.equal(linux.executableName, "t3code");
assert.equal(linux.category, "Development");
assert.equal(linux.maintainer, "T3 Tools <support@t3.gg>");
assert.equal(linux.vendor, "T3 Tools");
assert.equal(linux.synopsis, "A desktop GUI for AI coding agents");
assert.equal(linux.syncDesktopName, true);

const deb = config.deb as Record<string, unknown>;
assert.deepStrictEqual(deb.depends, [
"libgtk-3-0",
"libnotify4",
"libnss3",
"libxss1",
"libxtst6",
"xdg-utils",
"libatspi2.0-0",
"libuuid1",
"libsecret-1-0",
"libasound2t64 | libasound2",
]);
assert.deepStrictEqual(deb.fpm, ["/tmp/t3=/usr/bin/t3"]);
}).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
);

it.effect("includes runtime dependencies and the headless launcher in RPM packages", () =>
Effect.gen(function* () {
const config = yield* createBuildConfig(
"linux",
"rpm",
"1.2.3",
false,
false,
undefined,
undefined,
"/tmp/t3",
);

const rpm = config.rpm as Record<string, unknown>;
assert.deepStrictEqual(rpm.depends, [
"gtk3",
"libnotify",
"nss",
"libXScrnSaver",
"(libXtst or libXtst6)",
"xdg-utils",
"at-spi2-core",
"(libuuid or libuuid1)",
"alsa-lib",
"libsecret",
"mesa-libgbm",
]);
assert.deepStrictEqual(rpm.fpm, ["/tmp/t3=/usr/bin/t3"]);
}).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
);

it("renders a headless launcher for production and nightly packages", () => {
assert.equal(
renderLinuxHeadlessLauncher("1.2.3"),
[
"#!/bin/sh",
"set -eu",
"export ELECTRON_RUN_AS_NODE=1",
`exec '/opt/T3 Code (Alpha)/t3code' '/opt/T3 Code (Alpha)/resources/app.asar.unpacked/apps/server/dist/bin.mjs' "$@"`,
"",
].join("\n"),
);
assert.match(renderLinuxHeadlessLauncher("1.2.3-nightly.20260720.1"), /T3 Code \(Nightly\)/);
});

it("promotes target fff binaries to direct staged dependencies", () => {
assert.deepStrictEqual(resolveFffNativeDependencies("mac", "arm64", "0.9.4"), {
"@ff-labs/fff-bin-darwin-arm64": "0.9.4",
Expand Down
84 changes: 82 additions & 2 deletions scripts/build-desktop-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,33 @@ const PLATFORM_CONFIG: Record<typeof BuildPlatform.Type, PlatformConfig> = {
},
};

const DEB_DEPENDENCIES = [
"libgtk-3-0",
"libnotify4",
"libnss3",
"libxss1",
"libxtst6",
"xdg-utils",
"libatspi2.0-0",
"libuuid1",
"libsecret-1-0",
"libasound2t64 | libasound2",
] as const;

const RPM_DEPENDENCIES = [
"gtk3",
"libnotify",
"nss",
"libXScrnSaver",
"(libXtst or libXtst6)",
"xdg-utils",
"at-spi2-core",
"(libuuid or libuuid1)",
"alsa-lib",
"libsecret",
"mesa-libgbm",
] as const;

interface BuildCliInput {
readonly platform: Option.Option<typeof BuildPlatform.Type>;
readonly target: Option.Option<string>;
Expand Down Expand Up @@ -570,6 +597,9 @@ interface StagePackageJson {
readonly packageManager: string;
readonly description: string;
readonly author: string;
readonly homepage: string;
readonly license: string;
readonly desktopName: string;
readonly main: string;
readonly build: Record<string, unknown>;
readonly dependencies: Record<string, unknown>;
Expand Down Expand Up @@ -1372,6 +1402,24 @@ export function resolveDesktopProductName(version: string): string {
: (desktopPackageJson.productName ?? "T3 Code");
}

function quotePosixShellArgument(value: string): string {
return `'${value.replaceAll("'", `'\\''`)}'`;
}

export function renderLinuxHeadlessLauncher(version: string): string {
const installDir = `/opt/${resolveDesktopProductName(version)}`;
const desktopExecutable = `${installDir}/t3code`;
const serverEntry = `${installDir}/resources/app.asar.unpacked/apps/server/dist/bin.mjs`;

return [
"#!/bin/sh",
"set -eu",
"export ELECTRON_RUN_AS_NODE=1",
`exec ${quotePosixShellArgument(desktopExecutable)} ${quotePosixShellArgument(serverEntry)} "$@"`,
"",
].join("\n");
}

export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
platform: typeof BuildPlatform.Type,
target: string,
Expand All @@ -1385,6 +1433,7 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
readonly provisioningProfilePath: string;
}
| undefined,
linuxHeadlessLauncherPath?: string,
) {
const buildConfig: Record<string, unknown> = {
appId: DESKTOP_APP_ID,
Expand Down Expand Up @@ -1447,12 +1496,30 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
executableName: "t3code",
icon: "icons",
category: "Development",
maintainer: "T3 Tools <support@t3.gg>",
vendor: "T3 Tools",
synopsis: "A desktop GUI for AI coding agents",
syncDesktopName: true,
desktop: {
entry: {
StartupWMClass: "t3code",
},
},
};

if (target === "deb") {
buildConfig.deb = {
depends: [...DEB_DEPENDENCIES],
...(linuxHeadlessLauncherPath ? { fpm: [`${linuxHeadlessLauncherPath}=/usr/bin/t3`] } : {}),
};
}

if (target === "rpm") {
buildConfig.rpm = {
depends: [...RPM_DEPENDENCIES],
...(linuxHeadlessLauncherPath ? { fpm: [`${linuxHeadlessLauncherPath}=/usr/bin/t3`] } : {}),
};
}
}

if (platform === "win") {
Expand Down Expand Up @@ -1729,6 +1796,15 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
yield* fs.writeFileString(macEntitlementsPath, renderMacPasskeyEntitlements(macPasskeySigning));
}

const linuxHeadlessLauncherPath =
options.platform === "linux" && (options.target === "deb" || options.target === "rpm")
? path.join(stageRoot, "t3")
: undefined;
if (linuxHeadlessLauncherPath) {
yield* fs.writeFileString(linuxHeadlessLauncherPath, renderLinuxHeadlessLauncher(appVersion));
yield* fs.chmod(linuxHeadlessLauncherPath, 0o755);
}

const stageDependencies = {
...resolvedServerDependencies,
...resolvedDesktopRuntimeDependencies,
Expand Down Expand Up @@ -1760,8 +1836,11 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
t3codeCommitHash: commitHash,
private: true,
packageManager: rootPackageJson.packageManager,
description: "T3 Code desktop build",
author: "T3 Tools",
description: "A desktop GUI for AI coding agents such as Codex, Claude, Cursor, and OpenCode.",
author: "T3 Tools <support@t3.gg>",
homepage: "https://github.com/pingdotgg/t3code",
license: "MIT",
desktopName: "t3code.desktop",
main: "apps/desktop/dist-electron/main.cjs",
build: yield* createBuildConfig(
options.platform,
Expand All @@ -1776,6 +1855,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
provisioningProfilePath: macPasskeySigning.provisioningProfilePath,
}
: undefined,
linuxHeadlessLauncherPath,
),
dependencies: stageDependencies,
devDependencies: {
Expand Down
Loading