diff --git a/.gitignore b/.gitignore index 8e1669c8115..f480a82946a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ build/ .logs/ release/ release-mock/ +packaging-output/ .t3 .idea/ apps/web/.playwright diff --git a/package.json b/package.json index 3c0a8cc6b77..ea8c5d76b9d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index da45376954f..e381333aa58 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -31,6 +31,7 @@ import { resolveDesktopProductName, resolveDesktopUpdateChannel, resolveDesktopWebAssetBrand, + renderLinuxHeadlessLauncher, resolveGitHubPublishConfig, resolveMockUpdateServerPort, resolveMockUpdateServerUrl, @@ -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; + assert.deepStrictEqual(linux.target, ["deb"]); + assert.equal(linux.executableName, "t3code"); + assert.equal(linux.category, "Development"); + assert.equal(linux.maintainer, "T3 Tools "); + 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; + 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; + 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", diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 8a1099a9ed0..f984d112fe7 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -112,6 +112,33 @@ const PLATFORM_CONFIG: Record = { }, }; +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; readonly target: Option.Option; @@ -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; readonly dependencies: Record; @@ -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, @@ -1385,6 +1433,7 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( readonly provisioningProfilePath: string; } | undefined, + linuxHeadlessLauncherPath?: string, ) { const buildConfig: Record = { appId: DESKTOP_APP_ID, @@ -1447,12 +1496,30 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( executableName: "t3code", icon: "icons", category: "Development", + maintainer: "T3 Tools ", + 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") { @@ -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, @@ -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 ", + homepage: "https://github.com/pingdotgg/t3code", + license: "MIT", + desktopName: "t3code.desktop", main: "apps/desktop/dist-electron/main.cjs", build: yield* createBuildConfig( options.platform, @@ -1776,6 +1855,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( provisioningProfilePath: macPasskeySigning.provisioningProfilePath, } : undefined, + linuxHeadlessLauncherPath, ), dependencies: stageDependencies, devDependencies: {