Skip to content
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debian deps omit GBM library

Medium Severity

New Debian packages set deb.depends from DEB_DEPENDENCIES, which replaces electron-builder’s generated dependency set rather than merging with it. RPM_DEPENDENCIES includes mesa-libgbm, but the Debian list has no matching GBM package (for example libgbm1), so installs on minimal Debian/Ubuntu systems can pass dependency checks yet fail at runtime when Electron/Chromium loads libgbm.so.1.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f5629f8. Configure here.


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