authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-03 20:47:20-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-03 20:47:20-07:00
log1288909b6611436e8cb2c4084f9e197408caa8b4
tree03e068ea6988cac95010356189fcb769f3ee9bf2
parent30d36b8c48d51c4e6727b1f0971a7ac3a3a01cf2
signature Signed by PGP key B5690EEEBB952194

fix(release): surface desktop packaging subprocess output (#2937)

Co-authored-by: Julius Marminge <julius@mac.lan>

1 files changed, 82 insertions(+), 31 deletions(-)

scripts/build-desktop-artifact.ts+82-31
......@@ -121,6 +121,36 @@ const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.
121121 ),
122122 );
123123
124const COMMAND_OUTPUT_TAIL_LENGTH = 20_000;
125
126function appendOutputTail(acc: string, chunk: string): string {
127 const next = acc + chunk;
128 return next.length > COMMAND_OUTPUT_TAIL_LENGTH ? next.slice(-COMMAND_OUTPUT_TAIL_LENGTH) : next;
129}
130
131function formatOutputSection(label: string, output: string): string | undefined {
132 const trimmed = output.trim();
133 if (!trimmed) return undefined;
134 return `${label} tail:\n${trimmed}`;
135}
136
137const collectCommandStream = <E>(
138 stream: Stream.Stream<Uint8Array, E>,
139 output: NodeJS.WriteStream,
140 verbose: boolean,
141): Effect.Effect<string, E> =>
142 stream.pipe(
143 Stream.decodeText(),
144 Stream.runFoldEffect(
145 () => "",
146 (acc, chunk) =>
147 Effect.as(
148 verbose ? Effect.sync(() => output.write(chunk)) : Effect.void,
149 appendOutputTail(acc, chunk),
150 ),
151 ),
152 );
153
124154const spawnAndCollectOutput = Effect.fn("spawnAndCollectOutput")(function* (
125155 command: ChildProcess.Command,
126156) {
......@@ -353,20 +383,33 @@ export const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* (
353383 } satisfies ResolvedBuildOptions;
354384});
355385
356const commandOutputOptions = (verbose: boolean) =>
357 ({
358 stdout: verbose ? "inherit" : "ignore",
359 stderr: "inherit",
360 }) as const;
361
362const runCommand = Effect.fn("runCommand")(function* (command: ChildProcess.Command) {
386const runCommand = Effect.fn("runCommand")(function* (
387 command: ChildProcess.Command,
388 options: {
389 readonly label?: string;
390 readonly verbose: boolean;
391 },
392) {
363393 const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
364394 const child = yield* commandSpawner.spawn(command);
365 const exitCode = yield* child.exitCode;
395 const [stdout, stderr, exitCode] = yield* Effect.all(
396 [
397 collectCommandStream(child.stdout, process.stdout, options.verbose),
398 collectCommandStream(child.stderr, process.stderr, options.verbose),
399 child.exitCode.pipe(Effect.map(Number)),
400 ],
401 { concurrency: "unbounded" },
402 );
366403
367404 if (exitCode !== 0) {
405 const outputSections = [
406 options.label ? `Command: ${options.label}` : undefined,
407 formatOutputSection("stdout", stdout),
408 formatOutputSection("stderr", stderr),
409 ].filter((section): section is string => section !== undefined);
410 const outputSuffix = outputSections.length > 0 ? `\n\n${outputSections.join("\n\n")}` : "";
368411 return yield* new BuildScriptError({
369 message: `Command exited with non-zero exit code (${exitCode})`,
412 message: `Command exited with non-zero exit code (${exitCode})${outputSuffix}`,
370413 });
371414 }
372415});
......@@ -386,24 +429,25 @@ function generateMacIconSet(
386429 const iconSizes = [16, 32, 128, 256, 512] as const;
387430 for (const size of iconSizes) {
388431 yield* runCommand(
389 ChildProcess.make({
390 ...commandOutputOptions(verbose),
391 })`sips -z ${size} ${size} ${sourcePng} --out ${path.join(iconsetDir, `icon_${size}x${size}.png`)}`,
432 ChildProcess.make(
433 {},
434 )`sips -z ${size} ${size} ${sourcePng} --out ${path.join(iconsetDir, `icon_${size}x${size}.png`)}`,
435 { label: `sips icon ${size}x${size}`, verbose },
392436 );
393437
394438 const retinaSize = size * 2;
395439 yield* runCommand(
396 ChildProcess.make({
397 ...commandOutputOptions(verbose),
398 })`sips -z ${retinaSize} ${retinaSize} ${sourcePng} --out ${path.join(iconsetDir, `icon_${size}x${size}@2x.png`)}`,
440 ChildProcess.make(
441 {},
442 )`sips -z ${retinaSize} ${retinaSize} ${sourcePng} --out ${path.join(iconsetDir, `icon_${size}x${size}@2x.png`)}`,
443 { label: `sips icon ${size}x${size}@2x`, verbose },
399444 );
400445 }
401446
402 yield* runCommand(
403 ChildProcess.make({
404 ...commandOutputOptions(verbose),
405 })`iconutil -c icns ${iconsetDir} -o ${targetIcns}`,
406 );
447 yield* runCommand(ChildProcess.make({})`iconutil -c icns ${iconsetDir} -o ${targetIcns}`, {
448 label: "iconutil icns",
449 verbose,
450 });
407451 });
408452}
409453
......@@ -424,11 +468,10 @@ function stageMacIcons(stageResourcesDir: string, sourcePng: string, verbose: bo
424468 const iconPngPath = path.join(stageResourcesDir, "icon.png");
425469 const iconIcnsPath = path.join(stageResourcesDir, "icon.icns");
426470
427 yield* runCommand(
428 ChildProcess.make({
429 ...commandOutputOptions(verbose),
430 })`sips -z 512 512 ${sourcePng} --out ${iconPngPath}`,
431 );
471 yield* runCommand(ChildProcess.make({})`sips -z 512 512 ${sourcePng} --out ${iconPngPath}`, {
472 label: "sips mac icon",
473 verbose,
474 });
432475
433476 yield* generateMacIconSet(sourcePng, iconIcnsPath, tmpRoot, path, verbose);
434477 });
......@@ -468,9 +511,8 @@ function stageLinuxIconSize(
468511) {
469512 const resize = (command: string) =>
470513 runCommand(
471 ChildProcess.make(command, [sourcePng, "-resize", `${iconSize}x${iconSize}`, targetPng], {
472 ...commandOutputOptions(verbose),
473 }),
514 ChildProcess.make(command, [sourcePng, "-resize", `${iconSize}x${iconSize}`, targetPng]),
515 { label: `${command} linux icon ${iconSize}x${iconSize}`, verbose },
474516 );
475517
476518 return resize("magick").pipe(
......@@ -778,10 +820,10 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
778820 yield* runCommand(
779821 ChildProcess.make({
780822 cwd: repoRoot,
781 ...commandOutputOptions(options.verbose),
782823 // Windows needs shell mode to resolve .cmd shims (e.g. vp.cmd).
783824 shell: process.platform === "win32",
784825 })`vp run build:desktop`,
826 { label: "vp run build:desktop", verbose: options.verbose },
785827 );
786828 }
787829
......@@ -858,10 +900,10 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
858900 yield* runCommand(
859901 ChildProcess.make({
860902 cwd: stageAppDir,
861 ...commandOutputOptions(options.verbose),
862903 // Windows needs shell mode to resolve .cmd shims (e.g. vp.cmd).
863904 shell: process.platform === "win32",
864905 })`vp install --prod --no-optional`,
906 { label: "vp install --prod --no-optional", verbose: options.verbose },
865907 );
866908
867909 const buildEnv: NodeJS.ProcessEnv = {
......@@ -890,6 +932,12 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
890932 buildEnv.npm_config_msvs_version = buildEnv.npm_config_msvs_version ?? "2022";
891933 buildEnv.GYP_MSVS_VERSION = buildEnv.GYP_MSVS_VERSION ?? "2022";
892934 }
935 if (options.verbose) {
936 buildEnv.DEBUG =
937 buildEnv.DEBUG === undefined || buildEnv.DEBUG === ""
938 ? "electron-builder,electron-builder:*"
939 : `${buildEnv.DEBUG},electron-builder,electron-builder:*`;
940 }
893941
894942 yield* Effect.log(
895943 `[desktop-artifact] Building ${options.platform}/${options.target} (arch=${options.arch}, version=${appVersion})...`,
......@@ -898,10 +946,13 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
898946 ChildProcess.make({
899947 cwd: stageAppDir,
900948 env: buildEnv,
901 ...commandOutputOptions(options.verbose),
902949 // Windows needs shell mode to resolve .cmd shims.
903950 shell: process.platform === "win32",
904951 })`vp dlx electron-builder ${platformConfig.cliFlag} --${options.arch} --publish never`,
952 {
953 label: `vp dlx electron-builder ${platformConfig.cliFlag} --${options.arch} --publish never`,
954 verbose: options.verbose,
955 },
905956 );
906957
907958 const stageDistDir = path.join(stageAppDir, "dist");