#!/usr/bin/env node import { fromYaml } from "@t3tools/shared/schemaYaml";import { HostProcessPlatform } from "@t3tools/shared/hostProcess";import { resolveSpawnCommand } from "@t3tools/shared/shell";import rootPackageJson from "../package.json" with { type: "json" };import desktopPackageJson from "../apps/desktop/package.json" with { type: "json" };import serverPackageJson from "../apps/server/package.json" with { type: "json" }; import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";import { getDefaultBuildArch } from "./lib/build-target-arch.ts";import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts"; import * as NodeRuntime from "@effect/platform-node/NodeRuntime";import * as NodeServices from "@effect/platform-node/NodeServices";import * as Config from "effect/Config";import * as Data from "effect/Data";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Logger from "effect/Logger";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Schema from "effect/Schema";import * as Stream from "effect/Stream";import { Command, Flag } from "effect/unstable/cli";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; const LINUX_ICON_SIZES = [16, 22, 24, 32, 48, 64, 128, 256, 512] as const; const BuildPlatform = Schema.Literals(["mac", "linux", "win"]);const BuildArch = Schema.Literals(["arm64", "x64", "universal"]); const WorkspaceConfig = Schema.Struct({ catalog: Schema.optional(Schema.Record(Schema.String, Schema.String)), overrides: Schema.optional(Schema.Record(Schema.String, Schema.String)), patchedDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),});type WorkspaceConfig = typeof WorkspaceConfig.Type; const StageWorkspaceConfig = Schema.Struct({ supportedArchitectures: Schema.Struct({ os: Schema.Array(Schema.String), cpu: Schema.Array(Schema.String), }),}); const RepoRoot = Effect.service(Path.Path).pipe( Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))),);const encodeJsonString = Schema.encodeEffect(Schema.UnknownFromJsonString);const decodeWorkspaceConfig = Schema.decodeEffect(fromYaml(WorkspaceConfig));const encodeStageWorkspaceConfig = Schema.encodeEffect(fromYaml(StageWorkspaceConfig)); const readWorkspaceConfig = Effect.fn("readWorkspaceConfig")(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const repoRoot = yield* RepoRoot; const workspaceYaml = yield* fs.readFileString(path.join(repoRoot, "pnpm-workspace.yaml")); return yield* decodeWorkspaceConfig(workspaceYaml);}); interface DesktopBuildIconAssets { readonly macIconPng: string; readonly linuxIconPng: string; readonly windowsIconIco: string;} interface PlatformConfig { readonly cliFlag: "--mac" | "--linux" | "--win"; readonly defaultTarget: string; readonly archChoices: ReadonlyArray<typeof BuildArch.Type>;} const PLATFORM_CONFIG: Record<typeof BuildPlatform.Type, PlatformConfig> = { mac: { cliFlag: "--mac", defaultTarget: "dmg", archChoices: ["arm64", "x64", "universal"], }, linux: { cliFlag: "--linux", defaultTarget: "AppImage", archChoices: ["x64", "arm64"], }, win: { cliFlag: "--win", defaultTarget: "nsis", archChoices: ["x64", "arm64"], },}; interface BuildCliInput { readonly platform: Option.Option<typeof BuildPlatform.Type>; readonly target: Option.Option<string>; readonly arch: Option.Option<typeof BuildArch.Type>; readonly buildVersion: Option.Option<string>; readonly outputDir: Option.Option<string>; readonly skipBuild: Option.Option<boolean>; readonly keepStage: Option.Option<boolean>; readonly signed: Option.Option<boolean>; readonly verbose: Option.Option<boolean>; readonly mockUpdates: Option.Option<boolean>; readonly mockUpdateServerPort: Option.Option<number>;} function detectHostBuildPlatform(hostPlatform: string): typeof BuildPlatform.Type | undefined { if (hostPlatform === "darwin") return "mac"; if (hostPlatform === "linux") return "linux"; if (hostPlatform === "win32") return "win"; return undefined;} const getDefaultArch = Effect.fn("getDefaultArch")(function* (platform: typeof BuildPlatform.Type) { const config = PLATFORM_CONFIG[platform]; if (!config) { return "x64"; } return yield* getDefaultBuildArch(platform, config);}); class BuildScriptError extends Data.TaggedError("BuildScriptError")<{ readonly message: string; readonly cause?: unknown;}> {} const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> => stream.pipe( Stream.decodeText(), Stream.runFold( () => "", (acc, chunk) => acc + chunk, ), ); const COMMAND_OUTPUT_TAIL_LENGTH = 20_000; function appendOutputTail(acc: string, chunk: string): string { const next = acc + chunk; return next.length > COMMAND_OUTPUT_TAIL_LENGTH ? next.slice(-COMMAND_OUTPUT_TAIL_LENGTH) : next;} function formatOutputSection(label: string, output: string): string | undefined { const trimmed = output.trim(); if (!trimmed) return undefined; return `${label} tail:\n${trimmed}`;} const collectCommandStream = <E>( stream: Stream.Stream<Uint8Array, E>, output: NodeJS.WriteStream, verbose: boolean,): Effect.Effect<string, E> => stream.pipe( Stream.decodeText(), Stream.runFoldEffect( () => "", (acc, chunk) => Effect.as( verbose ? Effect.sync(() => output.write(chunk)) : Effect.void, appendOutputTail(acc, chunk), ), ), ); const spawnAndCollectOutput = Effect.fn("spawnAndCollectOutput")(function* ( command: ChildProcess.Command,) { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const child = yield* spawner.spawn(command); const [stdout, stderr, exitCode] = yield* Effect.all( [ collectStreamAsString(child.stdout), collectStreamAsString(child.stderr), child.exitCode.pipe(Effect.map(Number)), ], { concurrency: "unbounded" }, ); return { stdout, stderr, exitCode } as const;}); const resolveGitCommitHash = Effect.fn("resolveGitCommitHash")(function* (repoRoot: string) { const result = yield* spawnAndCollectOutput( ChildProcess.make("git", ["rev-parse", "--short=12", "HEAD"], { cwd: repoRoot, }), ).pipe( Effect.orElseSucceed(() => ({ stdout: "", stderr: "", exitCode: 1, })), ); if (result.exitCode !== 0) { return "unknown"; } const hash = result.stdout.trim(); if (!/^[0-9a-f]{7,40}$/i.test(hash)) { return "unknown"; } return hash.toLowerCase();}); const resolvePythonForNodeGyp = Effect.fn("resolvePythonForNodeGyp")(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const hostPlatform = yield* HostProcessPlatform; const env = yield* Config.all({ configuredPython: Config.string("npm_config_python").pipe( Config.orElse(() => Config.string("PYTHON")), Config.option, ), localAppData: Config.string("LOCALAPPDATA").pipe(Config.option), }); const configured = Option.getOrUndefined(env.configuredPython); if (configured && (yield* fs.exists(configured))) { return configured; } if (hostPlatform === "win32") { const localAppData = Option.getOrUndefined(env.localAppData); if (localAppData) { for (const version of ["Python313", "Python312", "Python311", "Python310"]) { const candidate = path.join(localAppData, "Programs", "Python", version, "python.exe"); if (yield* fs.exists(candidate)) { return candidate; } } } } const probe = yield* spawnAndCollectOutput( ChildProcess.make("python", ["-c", "import sys;print(sys.executable)"]), ).pipe( Effect.orElseSucceed(() => ({ stdout: "", stderr: "", exitCode: 1, })), ); if (probe.exitCode !== 0) { return undefined; } const executable = probe.stdout.trim(); if (!executable || !(yield* fs.exists(executable))) { return undefined; } return executable;}); interface ResolvedBuildOptions { readonly platform: typeof BuildPlatform.Type; readonly target: string; readonly arch: typeof BuildArch.Type; readonly version: string | undefined; readonly outputDir: string; readonly skipBuild: boolean; readonly keepStage: boolean; readonly signed: boolean; readonly verbose: boolean; readonly mockUpdates: boolean; readonly mockUpdateServerPort: number | undefined;} interface StagePackageJson { readonly name: string; readonly version: string; readonly buildVersion: string; readonly t3codeCommitHash: string; readonly private: true; readonly packageManager: string; readonly description: string; readonly author: string; readonly main: string; readonly build: Record<string, unknown>; readonly dependencies: Record<string, unknown>; readonly devDependencies: { readonly electron: string; }; readonly overrides: Record<string, unknown>; readonly pnpm?: { readonly patchedDependencies?: Record<string, string>; };} export const STAGE_INSTALL_ARGS = ["install", "--prod"] as const;export const DESKTOP_ASAR_UNPACK = ["node_modules/@ff-labs/fff-bin-*/**/*"] as const; export function resolveFffNativeDependencies( platform: typeof BuildPlatform.Type, arch: typeof BuildArch.Type, version: string,): Record<string, string> { const architectures = arch === "universal" ? (["arm64", "x64"] as const) : [arch]; if (platform === "mac") { return Object.fromEntries( architectures.map((architecture) => [`@ff-labs/fff-bin-darwin-${architecture}`, version]), ); } if (platform === "win") { return Object.fromEntries( architectures.map((architecture) => [`@ff-labs/fff-bin-win32-${architecture}`, version]), ); } return Object.fromEntries( architectures.flatMap((architecture) => ["gnu", "musl"].map((libc) => [`@ff-labs/fff-bin-linux-${architecture}-${libc}`, version]), ), );} export function createStageWorkspaceConfig( platform: typeof BuildPlatform.Type, arch: typeof BuildArch.Type,): typeof StageWorkspaceConfig.Type { return { supportedArchitectures: { os: [platform === "mac" ? "darwin" : platform === "win" ? "win32" : "linux"], cpu: arch === "universal" ? ["arm64", "x64"] : [arch], }, };} export function createStagePnpmConfig( patchedDependencies: Record<string, string>, dependencies: Record<string, unknown>,): StagePackageJson["pnpm"] | undefined { const stagePatchedDependencies = Object.fromEntries( Object.entries(patchedDependencies).filter(([patchKey]) => Object.hasOwn(dependencies, getPatchedDependencyPackageName(patchKey)), ), ); return Object.keys(stagePatchedDependencies).length > 0 ? { patchedDependencies: stagePatchedDependencies } : undefined;} function getPatchedDependencyPackageName(patchKey: string): string { const versionSeparator = patchKey.lastIndexOf("@"); return versionSeparator > 0 ? patchKey.slice(0, versionSeparator) : patchKey;} const AzureTrustedSigningOptionsConfig = Config.all({ publisherName: Config.string("AZURE_TRUSTED_SIGNING_PUBLISHER_NAME"), endpoint: Config.string("AZURE_TRUSTED_SIGNING_ENDPOINT"), certificateProfileName: Config.string("AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME"), codeSigningAccountName: Config.string("AZURE_TRUSTED_SIGNING_ACCOUNT_NAME"), fileDigest: Config.string("AZURE_TRUSTED_SIGNING_FILE_DIGEST").pipe(Config.withDefault("SHA256")), timestampDigest: Config.string("AZURE_TRUSTED_SIGNING_TIMESTAMP_DIGEST").pipe( Config.withDefault("SHA256"), ), timestampRfc3161: Config.string("AZURE_TRUSTED_SIGNING_TIMESTAMP_RFC3161").pipe( Config.withDefault("http://timestamp.acs.microsoft.com"), ),}); const BuildEnvConfig = Config.all({ platform: Config.schema(BuildPlatform, "T3CODE_DESKTOP_PLATFORM").pipe(Config.option), target: Config.string("T3CODE_DESKTOP_TARGET").pipe(Config.option), arch: Config.schema(BuildArch, "T3CODE_DESKTOP_ARCH").pipe(Config.option), version: Config.string("T3CODE_DESKTOP_VERSION").pipe(Config.option), outputDir: Config.string("T3CODE_DESKTOP_OUTPUT_DIR").pipe(Config.option), skipBuild: Config.boolean("T3CODE_DESKTOP_SKIP_BUILD").pipe(Config.withDefault(false)), keepStage: Config.boolean("T3CODE_DESKTOP_KEEP_STAGE").pipe(Config.withDefault(false)), signed: Config.boolean("T3CODE_DESKTOP_SIGNED").pipe(Config.withDefault(false)), verbose: Config.boolean("T3CODE_DESKTOP_VERBOSE").pipe(Config.withDefault(false)), mockUpdates: Config.boolean("T3CODE_DESKTOP_MOCK_UPDATES").pipe(Config.withDefault(false)), mockUpdateServerPort: Config.string("T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT").pipe(Config.option),}); const MockUpdateServerPortSchema = Schema.NumberFromString.check( Schema.isInt(), Schema.isBetween({ minimum: 1, maximum: 65535 }),);const decodeMockUpdateServerPort = Schema.decodeUnknownEffect(MockUpdateServerPortSchema); const resolveBooleanFlag = (flag: Option.Option<boolean>, envValue: boolean) => Option.getOrElse(flag, () => envValue);const mergeOptions = <A>(a: Option.Option<A>, b: Option.Option<A>, defaultValue: A) => Option.getOrElse(a, () => Option.getOrElse(b, () => defaultValue)); export const resolveMockUpdateServerPort = Effect.fn("resolveMockUpdateServerPort")(function* ( mockUpdateServerPort: string | undefined,) { const port = mockUpdateServerPort?.trim(); if (!port) { return undefined; } return yield* decodeMockUpdateServerPort(port);}); export const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* ( input: BuildCliInput,) { const path = yield* Path.Path; const repoRoot = yield* RepoRoot; const env = yield* BuildEnvConfig; const hostPlatform = yield* HostProcessPlatform; const platform = mergeOptions( input.platform, env.platform, detectHostBuildPlatform(hostPlatform), ); if (!platform) { return yield* new BuildScriptError({ message: `Unsupported host platform '${hostPlatform}'.`, }); } const target = mergeOptions(input.target, env.target, PLATFORM_CONFIG[platform].defaultTarget); const defaultArch = yield* getDefaultArch(platform); const arch = mergeOptions(input.arch, env.arch, defaultArch); const version = mergeOptions(input.buildVersion, env.version, undefined); const releaseDir = resolveBooleanFlag(input.mockUpdates, env.mockUpdates) ? "release-mock" : "release"; const outputDir = path.resolve( repoRoot, mergeOptions(input.outputDir, env.outputDir, releaseDir), ); const skipBuild = resolveBooleanFlag(input.skipBuild, env.skipBuild); const keepStage = resolveBooleanFlag(input.keepStage, env.keepStage); const signed = resolveBooleanFlag(input.signed, env.signed); const verbose = resolveBooleanFlag(input.verbose, env.verbose); const mockUpdates = resolveBooleanFlag(input.mockUpdates, env.mockUpdates); const mockUpdateServerPort = Option.getOrUndefined(input.mockUpdateServerPort) ?? (yield* resolveMockUpdateServerPort(Option.getOrUndefined(env.mockUpdateServerPort)).pipe( Effect.mapError( (cause) => new BuildScriptError({ message: "Invalid mock update server port.", cause, }), ), )); return { platform, target, arch, version, outputDir, skipBuild, keepStage, signed, verbose, mockUpdates, mockUpdateServerPort, } satisfies ResolvedBuildOptions;}); const runCommand = Effect.fn("runCommand")(function* ( command: ChildProcess.Command, options: { readonly label?: string; readonly verbose: boolean; },) { const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const child = yield* commandSpawner.spawn(command); const [stdout, stderr, exitCode] = yield* Effect.all( [ collectCommandStream(child.stdout, process.stdout, options.verbose), collectCommandStream(child.stderr, process.stderr, options.verbose), child.exitCode.pipe(Effect.map(Number)), ], { concurrency: "unbounded" }, ); if (exitCode !== 0) { const outputSections = [ options.label ? `Command: ${options.label}` : undefined, formatOutputSection("stdout", stdout), formatOutputSection("stderr", stderr), ].filter((section): section is string => section !== undefined); const outputSuffix = outputSections.length > 0 ? `\n\n${outputSections.join("\n\n")}` : ""; return yield* new BuildScriptError({ message: `Command exited with non-zero exit code (${exitCode})${outputSuffix}`, }); }}); function generateMacIconSet(Lines 1–500 of 1180. Load more
#!/usr/bin/env node import { fromYaml } from "@t3tools/shared/schemaYaml"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; import rootPackageJson from "../package.json" with { type: "json" }; import desktopPackageJson from "../apps/desktop/package.json" with { type: "json" }; import serverPackageJson from "../apps/server/package.json" with { type: "json" }; import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts"; import { getDefaultBuildArch } from "./lib/build-target-arch.ts"; import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts"; import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; import * as NodeServices from "@effect/platform-node/NodeServices"; import * as Config from "effect/Config"; import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Logger from "effect/Logger"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import { Command, Flag } from "effect/unstable/cli"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; const LINUX_ICON_SIZES = [16, 22, 24, 32, 48, 64, 128, 256, 512] as const; const BuildPlatform = Schema.Literals(["mac", "linux", "win"]); const BuildArch = Schema.Literals(["arm64", "x64", "universal"]); const WorkspaceConfig = Schema.Struct({ catalog: Schema.optional(Schema.Record(Schema.String, Schema.String)), overrides: Schema.optional(Schema.Record(Schema.String, Schema.String)), patchedDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)), }); type WorkspaceConfig = typeof WorkspaceConfig.Type; const StageWorkspaceConfig = Schema.Struct({ supportedArchitectures: Schema.Struct({ os: Schema.Array(Schema.String), cpu: Schema.Array(Schema.String), }), }); const RepoRoot = Effect.service(Path.Path).pipe( Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), ); const encodeJsonString = Schema.encodeEffect(Schema.UnknownFromJsonString); const decodeWorkspaceConfig = Schema.decodeEffect(fromYaml(WorkspaceConfig)); const encodeStageWorkspaceConfig = Schema.encodeEffect(fromYaml(StageWorkspaceConfig)); const readWorkspaceConfig = Effect.fn("readWorkspaceConfig")(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const repoRoot = yield* RepoRoot; const workspaceYaml = yield* fs.readFileString(path.join(repoRoot, "pnpm-workspace.yaml")); return yield* decodeWorkspaceConfig(workspaceYaml); }); interface DesktopBuildIconAssets { readonly macIconPng: string; readonly linuxIconPng: string; readonly windowsIconIco: string; } interface PlatformConfig { readonly cliFlag: "--mac" | "--linux" | "--win"; readonly defaultTarget: string; readonly archChoices: ReadonlyArray<typeof BuildArch.Type>; } const PLATFORM_CONFIG: Record<typeof BuildPlatform.Type, PlatformConfig> = { mac: { cliFlag: "--mac", defaultTarget: "dmg", archChoices: ["arm64", "x64", "universal"], }, linux: { cliFlag: "--linux", defaultTarget: "AppImage", archChoices: ["x64", "arm64"], }, win: { cliFlag: "--win", defaultTarget: "nsis", archChoices: ["x64", "arm64"], }, }; interface BuildCliInput { readonly platform: Option.Option<typeof BuildPlatform.Type>; readonly target: Option.Option<string>; readonly arch: Option.Option<typeof BuildArch.Type>; readonly buildVersion: Option.Option<string>; readonly outputDir: Option.Option<string>; readonly skipBuild: Option.Option<boolean>; readonly keepStage: Option.Option<boolean>; readonly signed: Option.Option<boolean>; readonly verbose: Option.Option<boolean>; readonly mockUpdates: Option.Option<boolean>; readonly mockUpdateServerPort: Option.Option<number>; } function detectHostBuildPlatform(hostPlatform: string): typeof BuildPlatform.Type | undefined { if (hostPlatform === "darwin") return "mac"; if (hostPlatform === "linux") return "linux"; if (hostPlatform === "win32") return "win"; return undefined; } const getDefaultArch = Effect.fn("getDefaultArch")(function* (platform: typeof BuildPlatform.Type) { const config = PLATFORM_CONFIG[platform]; if (!config) { return "x64"; } return yield* getDefaultBuildArch(platform, config); }); class BuildScriptError extends Data.TaggedError("BuildScriptError")<{ readonly message: string; readonly cause?: unknown; }> {} const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> => stream.pipe( Stream.decodeText(), Stream.runFold( () => "", (acc, chunk) => acc + chunk, ), ); const COMMAND_OUTPUT_TAIL_LENGTH = 20_000; function appendOutputTail(acc: string, chunk: string): string { const next = acc + chunk; return next.length > COMMAND_OUTPUT_TAIL_LENGTH ? next.slice(-COMMAND_OUTPUT_TAIL_LENGTH) : next; } function formatOutputSection(label: string, output: string): string | undefined { const trimmed = output.trim(); if (!trimmed) return undefined; return `${label} tail:\n${trimmed}`; } const collectCommandStream = <E>( stream: Stream.Stream<Uint8Array, E>, output: NodeJS.WriteStream, verbose: boolean, ): Effect.Effect<string, E> => stream.pipe( Stream.decodeText(), Stream.runFoldEffect( () => "", (acc, chunk) => Effect.as( verbose ? Effect.sync(() => output.write(chunk)) : Effect.void, appendOutputTail(acc, chunk), ), ), ); const spawnAndCollectOutput = Effect.fn("spawnAndCollectOutput")(function* ( command: ChildProcess.Command, ) { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const child = yield* spawner.spawn(command); const [stdout, stderr, exitCode] = yield* Effect.all( [ collectStreamAsString(child.stdout), collectStreamAsString(child.stderr), child.exitCode.pipe(Effect.map(Number)), ], { concurrency: "unbounded" }, ); return { stdout, stderr, exitCode } as const; }); const resolveGitCommitHash = Effect.fn("resolveGitCommitHash")(function* (repoRoot: string) { const result = yield* spawnAndCollectOutput( ChildProcess.make("git", ["rev-parse", "--short=12", "HEAD"], { cwd: repoRoot, }), ).pipe( Effect.orElseSucceed(() => ({ stdout: "", stderr: "", exitCode: 1, })), ); if (result.exitCode !== 0) { return "unknown"; } const hash = result.stdout.trim(); if (!/^[0-9a-f]{7,40}$/i.test(hash)) { return "unknown"; } return hash.toLowerCase(); }); const resolvePythonForNodeGyp = Effect.fn("resolvePythonForNodeGyp")(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const hostPlatform = yield* HostProcessPlatform; const env = yield* Config.all({ configuredPython: Config.string("npm_config_python").pipe( Config.orElse(() => Config.string("PYTHON")), Config.option, ), localAppData: Config.string("LOCALAPPDATA").pipe(Config.option), }); const configured = Option.getOrUndefined(env.configuredPython); if (configured && (yield* fs.exists(configured))) { return configured; } if (hostPlatform === "win32") { const localAppData = Option.getOrUndefined(env.localAppData); if (localAppData) { for (const version of ["Python313", "Python312", "Python311", "Python310"]) { const candidate = path.join(localAppData, "Programs", "Python", version, "python.exe"); if (yield* fs.exists(candidate)) { return candidate; } } } } const probe = yield* spawnAndCollectOutput( ChildProcess.make("python", ["-c", "import sys;print(sys.executable)"]), ).pipe( Effect.orElseSucceed(() => ({ stdout: "", stderr: "", exitCode: 1, })), ); if (probe.exitCode !== 0) { return undefined; } const executable = probe.stdout.trim(); if (!executable || !(yield* fs.exists(executable))) { return undefined; } return executable; }); interface ResolvedBuildOptions { readonly platform: typeof BuildPlatform.Type; readonly target: string; readonly arch: typeof BuildArch.Type; readonly version: string | undefined; readonly outputDir: string; readonly skipBuild: boolean; readonly keepStage: boolean; readonly signed: boolean; readonly verbose: boolean; readonly mockUpdates: boolean; readonly mockUpdateServerPort: number | undefined; } interface StagePackageJson { readonly name: string; readonly version: string; readonly buildVersion: string; readonly t3codeCommitHash: string; readonly private: true; readonly packageManager: string; readonly description: string; readonly author: string; readonly main: string; readonly build: Record<string, unknown>; readonly dependencies: Record<string, unknown>; readonly devDependencies: { readonly electron: string; }; readonly overrides: Record<string, unknown>; readonly pnpm?: { readonly patchedDependencies?: Record<string, string>; }; } export const STAGE_INSTALL_ARGS = ["install", "--prod"] as const; export const DESKTOP_ASAR_UNPACK = ["node_modules/@ff-labs/fff-bin-*/**/*"] as const; export function resolveFffNativeDependencies( platform: typeof BuildPlatform.Type, arch: typeof BuildArch.Type, version: string, ): Record<string, string> { const architectures = arch === "universal" ? (["arm64", "x64"] as const) : [arch]; if (platform === "mac") { return Object.fromEntries( architectures.map((architecture) => [`@ff-labs/fff-bin-darwin-${architecture}`, version]), ); } if (platform === "win") { return Object.fromEntries( architectures.map((architecture) => [`@ff-labs/fff-bin-win32-${architecture}`, version]), ); } return Object.fromEntries( architectures.flatMap((architecture) => ["gnu", "musl"].map((libc) => [`@ff-labs/fff-bin-linux-${architecture}-${libc}`, version]), ), ); } export function createStageWorkspaceConfig( platform: typeof BuildPlatform.Type, arch: typeof BuildArch.Type, ): typeof StageWorkspaceConfig.Type { return { supportedArchitectures: { os: [platform === "mac" ? "darwin" : platform === "win" ? "win32" : "linux"], cpu: arch === "universal" ? ["arm64", "x64"] : [arch], }, }; } export function createStagePnpmConfig( patchedDependencies: Record<string, string>, dependencies: Record<string, unknown>, ): StagePackageJson["pnpm"] | undefined { const stagePatchedDependencies = Object.fromEntries( Object.entries(patchedDependencies).filter(([patchKey]) => Object.hasOwn(dependencies, getPatchedDependencyPackageName(patchKey)), ), ); return Object.keys(stagePatchedDependencies).length > 0 ? { patchedDependencies: stagePatchedDependencies } : undefined; } function getPatchedDependencyPackageName(patchKey: string): string { const versionSeparator = patchKey.lastIndexOf("@"); return versionSeparator > 0 ? patchKey.slice(0, versionSeparator) : patchKey; } const AzureTrustedSigningOptionsConfig = Config.all({ publisherName: Config.string("AZURE_TRUSTED_SIGNING_PUBLISHER_NAME"), endpoint: Config.string("AZURE_TRUSTED_SIGNING_ENDPOINT"), certificateProfileName: Config.string("AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME"), codeSigningAccountName: Config.string("AZURE_TRUSTED_SIGNING_ACCOUNT_NAME"), fileDigest: Config.string("AZURE_TRUSTED_SIGNING_FILE_DIGEST").pipe(Config.withDefault("SHA256")), timestampDigest: Config.string("AZURE_TRUSTED_SIGNING_TIMESTAMP_DIGEST").pipe( Config.withDefault("SHA256"), ), timestampRfc3161: Config.string("AZURE_TRUSTED_SIGNING_TIMESTAMP_RFC3161").pipe( Config.withDefault("http://timestamp.acs.microsoft.com"), ), }); const BuildEnvConfig = Config.all({ platform: Config.schema(BuildPlatform, "T3CODE_DESKTOP_PLATFORM").pipe(Config.option), target: Config.string("T3CODE_DESKTOP_TARGET").pipe(Config.option), arch: Config.schema(BuildArch, "T3CODE_DESKTOP_ARCH").pipe(Config.option), version: Config.string("T3CODE_DESKTOP_VERSION").pipe(Config.option), outputDir: Config.string("T3CODE_DESKTOP_OUTPUT_DIR").pipe(Config.option), skipBuild: Config.boolean("T3CODE_DESKTOP_SKIP_BUILD").pipe(Config.withDefault(false)), keepStage: Config.boolean("T3CODE_DESKTOP_KEEP_STAGE").pipe(Config.withDefault(false)), signed: Config.boolean("T3CODE_DESKTOP_SIGNED").pipe(Config.withDefault(false)), verbose: Config.boolean("T3CODE_DESKTOP_VERBOSE").pipe(Config.withDefault(false)), mockUpdates: Config.boolean("T3CODE_DESKTOP_MOCK_UPDATES").pipe(Config.withDefault(false)), mockUpdateServerPort: Config.string("T3CODE_DESKTOP_MOCK_UPDATE_SERVER_PORT").pipe(Config.option), }); const MockUpdateServerPortSchema = Schema.NumberFromString.check( Schema.isInt(), Schema.isBetween({ minimum: 1, maximum: 65535 }), ); const decodeMockUpdateServerPort = Schema.decodeUnknownEffect(MockUpdateServerPortSchema); const resolveBooleanFlag = (flag: Option.Option<boolean>, envValue: boolean) => Option.getOrElse(flag, () => envValue); const mergeOptions = <A>(a: Option.Option<A>, b: Option.Option<A>, defaultValue: A) => Option.getOrElse(a, () => Option.getOrElse(b, () => defaultValue)); export const resolveMockUpdateServerPort = Effect.fn("resolveMockUpdateServerPort")(function* ( mockUpdateServerPort: string | undefined, ) { const port = mockUpdateServerPort?.trim(); if (!port) { return undefined; } return yield* decodeMockUpdateServerPort(port); }); export const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* ( input: BuildCliInput, ) { const path = yield* Path.Path; const repoRoot = yield* RepoRoot; const env = yield* BuildEnvConfig; const hostPlatform = yield* HostProcessPlatform; const platform = mergeOptions( input.platform, env.platform, detectHostBuildPlatform(hostPlatform), ); if (!platform) { return yield* new BuildScriptError({ message: `Unsupported host platform '${hostPlatform}'.`, }); } const target = mergeOptions(input.target, env.target, PLATFORM_CONFIG[platform].defaultTarget); const defaultArch = yield* getDefaultArch(platform); const arch = mergeOptions(input.arch, env.arch, defaultArch); const version = mergeOptions(input.buildVersion, env.version, undefined); const releaseDir = resolveBooleanFlag(input.mockUpdates, env.mockUpdates) ? "release-mock" : "release"; const outputDir = path.resolve( repoRoot, mergeOptions(input.outputDir, env.outputDir, releaseDir), ); const skipBuild = resolveBooleanFlag(input.skipBuild, env.skipBuild); const keepStage = resolveBooleanFlag(input.keepStage, env.keepStage); const signed = resolveBooleanFlag(input.signed, env.signed); const verbose = resolveBooleanFlag(input.verbose, env.verbose); const mockUpdates = resolveBooleanFlag(input.mockUpdates, env.mockUpdates); const mockUpdateServerPort = Option.getOrUndefined(input.mockUpdateServerPort) ?? (yield* resolveMockUpdateServerPort(Option.getOrUndefined(env.mockUpdateServerPort)).pipe( Effect.mapError( (cause) => new BuildScriptError({ message: "Invalid mock update server port.", cause, }), ), )); return { platform, target, arch, version, outputDir, skipBuild, keepStage, signed, verbose, mockUpdates, mockUpdateServerPort, } satisfies ResolvedBuildOptions; }); const runCommand = Effect.fn("runCommand")(function* ( command: ChildProcess.Command, options: { readonly label?: string; readonly verbose: boolean; }, ) { const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const child = yield* commandSpawner.spawn(command); const [stdout, stderr, exitCode] = yield* Effect.all( [ collectCommandStream(child.stdout, process.stdout, options.verbose), collectCommandStream(child.stderr, process.stderr, options.verbose), child.exitCode.pipe(Effect.map(Number)), ], { concurrency: "unbounded" }, ); if (exitCode !== 0) { const outputSections = [ options.label ? `Command: ${options.label}` : undefined, formatOutputSection("stdout", stdout), formatOutputSection("stderr", stderr), ].filter((section): section is string => section !== undefined); const outputSuffix = outputSections.length > 0 ? `\n\n${outputSections.join("\n\n")}` : ""; return yield* new BuildScriptError({ message: `Command exited with non-zero exit code (${exitCode})${outputSuffix}`, }); } }); function generateMacIconSet(