1180 lines · 39.0 KB · TypeScript
RawDownload
#!/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.