authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 18:58:22-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 18:58:22-07:00
log60dc4af916d0a62840d74fe95fe104e8b1c4ac92
treeb4caff699adc17e896a7c3f5c4ead2e9e89f0cd6
parentd389cfd4e39ec1b76a6898a04d1b90190155b618
signature Signed by PGP key B5690EEEBB952194

[codex] Structure desktop build script failures (#3452)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 349 insertions(+), 75 deletions(-)

scripts/build-desktop-artifact.test.ts+37-8
......@@ -9,15 +9,17 @@ import * as Stream from "effect/Stream";
99import { ChildProcessSpawner } from "effect/unstable/process";
1010
1111import {
12 BuildScriptError,
12 BuildCommandFailedError,
1313 createStageWorkspaceConfig,
1414 createStagePnpmConfig,
1515 createBuildConfig,
1616 DESKTOP_ASAR_UNPACK,
1717 InvalidMacPasskeyRpDomainError,
1818 InvalidMacPasskeyPublishableKeyError,
19 InvalidMockUpdateServerPortError,
1920 isMacPasskeySigningConfigurationError,
2021 LinuxIconResizeError,
22 MacPasskeySigningConfigurationResolutionError,
2123 MissingMacPasskeyProvisioningProfileError,
2224 renderMacPasskeyEntitlements,
2325 resolveClerkPasskeyNativeArtifacts,
......@@ -249,10 +251,16 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
249251 const aggregateCause = error.cause as AggregateError;
250252 assert.lengthOf(aggregateCause.errors, 2);
251253 assert.strictEqual(aggregateCause.cause, aggregateCause.errors[0]);
252 assert.instanceOf(aggregateCause.errors[0], BuildScriptError);
253 assert.instanceOf(aggregateCause.errors[1], BuildScriptError);
254 assert.include((aggregateCause.errors[0] as BuildScriptError).message, "magick linux icon");
255 assert.include((aggregateCause.errors[1] as BuildScriptError).message, "convert linux icon");
254 assert.instanceOf(aggregateCause.errors[0], BuildCommandFailedError);
255 assert.instanceOf(aggregateCause.errors[1], BuildCommandFailedError);
256 const primaryError = aggregateCause.errors[0] as BuildCommandFailedError;
257 const fallbackError = aggregateCause.errors[1] as BuildCommandFailedError;
258 assert.equal(primaryError.command, "magick linux icon 512x512");
259 assert.equal(primaryError.exitCode, 1);
260 assert.include(primaryError.message, "magick linux icon");
261 assert.equal(fallbackError.command, "convert linux icon 512x512");
262 assert.equal(fallbackError.exitCode, 2);
263 assert.include(fallbackError.message, "convert linux icon");
256264 assert.deepStrictEqual(
257265 commands.map(({ command }) => command),
258266 ["magick", "convert"],
......@@ -356,7 +364,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
356364 it("preserves known passkey signing configuration errors at the build boundary", () => {
357365 const decodingCause = new Error("publishable-key-decode-failed");
358366 const knownError = new InvalidMacPasskeyPublishableKeyError({ cause: decodingCause });
359 const error = BuildScriptError.fromMacPasskeySigningConfiguration(knownError);
367 const error = MacPasskeySigningConfigurationResolutionError.fromCause(knownError);
360368
361369 assert.strictEqual(error, knownError);
362370 assert.instanceOf(error, InvalidMacPasskeyPublishableKeyError);
......@@ -367,9 +375,9 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
367375 it("wraps unknown passkey signing configuration defects without copying cause text", () => {
368376 const secret = "pk_test_do-not-retain";
369377 const cause = new Error(secret);
370 const error = BuildScriptError.fromMacPasskeySigningConfiguration(cause);
378 const error = MacPasskeySigningConfigurationResolutionError.fromCause(cause);
371379
372 assert.instanceOf(error, BuildScriptError);
380 assert.instanceOf(error, MacPasskeySigningConfigurationResolutionError);
373381 assert.strictEqual(error.cause, cause);
374382 assert.equal(error.message, "Failed to resolve macOS passkey signing configuration.");
375383 assert.notInclude(error.message, secret);
......@@ -453,6 +461,27 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
453461 }),
454462 );
455463
464 it("classifies invalid configured ports with the decoder's number grammar", () => {
465 const cause = new Error("invalid configured port");
466
467 assert.equal(
468 InvalidMockUpdateServerPortError.fromConfigValue("0x10", cause).reason,
469 "not-numeric",
470 );
471 assert.equal(
472 InvalidMockUpdateServerPortError.fromConfigValue("12.5", cause).reason,
473 "not-integer",
474 );
475 assert.equal(
476 InvalidMockUpdateServerPortError.fromConfigValue("65536", cause).reason,
477 "out-of-range",
478 );
479 assert.strictEqual(
480 InvalidMockUpdateServerPortError.fromConfigValue("0x10", cause).cause,
481 cause,
482 );
483 });
484
456485 it.effect("resolves default platform and architecture from host references", () =>
457486 Effect.gen(function* () {
458487 const resolved = yield* resolveBuildOptions({
scripts/build-desktop-artifact.ts+312-67
......@@ -18,7 +18,6 @@ import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts";
1818import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
1919import * as NodeServices from "@effect/platform-node/NodeServices";
2020import * as Config from "effect/Config";
21import * as Data from "effect/Data";
2221import * as Effect from "effect/Effect";
2322import * as FileSystem from "effect/FileSystem";
2423import * as Layer from "effect/Layer";
......@@ -126,19 +125,238 @@ const getDefaultArch = Effect.fn("getDefaultArch")(function* (platform: typeof B
126125 return yield* getDefaultBuildArch(platform, config);
127126});
128127
129export class BuildScriptError extends Data.TaggedError("BuildScriptError")<{
130 readonly message: string;
131 readonly cause?: unknown;
132}> {
133 static fromMacPasskeySigningConfiguration(
128export class MacPasskeySigningConfigurationResolutionError extends Schema.TaggedErrorClass<MacPasskeySigningConfigurationResolutionError>()(
129 "MacPasskeySigningConfigurationResolutionError",
130 {
131 cause: Schema.Defect(),
132 },
133) {
134 static fromCause(
134135 cause: unknown,
135 ): MacPasskeySigningConfigurationError | BuildScriptError {
136 ): MacPasskeySigningConfigurationError | MacPasskeySigningConfigurationResolutionError {
136137 return isMacPasskeySigningConfigurationError(cause)
137138 ? cause
138 : new BuildScriptError({
139 message: "Failed to resolve macOS passkey signing configuration.",
140 cause,
141 });
139 : new MacPasskeySigningConfigurationResolutionError({ cause });
140 }
141
142 override get message(): string {
143 return "Failed to resolve macOS passkey signing configuration.";
144 }
145}
146
147export class ClerkPasskeyNativePackageMissingError extends Schema.TaggedErrorClass<ClerkPasskeyNativePackageMissingError>()(
148 "ClerkPasskeyNativePackageMissingError",
149 {
150 packageName: Schema.String,
151 binaryFileName: Schema.String,
152 packageEntryPath: Schema.String,
153 platform: BuildPlatform,
154 arch: BuildArch,
155 cause: Schema.Defect(),
156 },
157) {
158 override get message(): string {
159 return `Clerk passkey native package is missing: ${this.packageName}`;
160 }
161}
162
163export class UnsupportedHostBuildPlatformError extends Schema.TaggedErrorClass<UnsupportedHostBuildPlatformError>()(
164 "UnsupportedHostBuildPlatformError",
165 {
166 hostPlatform: Schema.String,
167 },
168) {
169 override get message(): string {
170 return `Unsupported host platform '${this.hostPlatform}'.`;
171 }
172}
173
174const InvalidMockUpdateServerPortReason = Schema.Literals([
175 "not-numeric",
176 "not-integer",
177 "out-of-range",
178]);
179
180export class InvalidMockUpdateServerPortError extends Schema.TaggedErrorClass<InvalidMockUpdateServerPortError>()(
181 "InvalidMockUpdateServerPortError",
182 {
183 reason: InvalidMockUpdateServerPortReason,
184 inputLength: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
185 cause: Schema.Defect(),
186 },
187) {
188 override get message(): string {
189 return "Invalid mock update server port.";
190 }
191
192 static fromConfigValue(configuredPort: string, cause: unknown) {
193 return new InvalidMockUpdateServerPortError({
194 reason: invalidMockUpdateServerPortReason(configuredPort),
195 inputLength: configuredPort.length,
196 cause,
197 });
198 }
199}
200
201export class BuildCommandFailedError extends Schema.TaggedErrorClass<BuildCommandFailedError>()(
202 "BuildCommandFailedError",
203 {
204 command: Schema.String,
205 exitCode: Schema.Int,
206 stdoutTail: Schema.optionalKey(Schema.String),
207 stderrTail: Schema.optionalKey(Schema.String),
208 },
209) {
210 override get message(): string {
211 const outputSections = [
212 `Command: ${this.command}`,
213 formatOutputSection("stdout", this.stdoutTail ?? ""),
214 formatOutputSection("stderr", this.stderrTail ?? ""),
215 ].filter((section): section is string => section !== undefined);
216 const outputSuffix = outputSections.length > 0 ? `\n\n${outputSections.join("\n\n")}` : "";
217 return `Command exited with non-zero exit code (${this.exitCode})${outputSuffix}`;
218 }
219}
220
221const desktopIconPlatformNames = {
222 mac: "macOS",
223 linux: "Linux",
224 win: "Windows",
225} satisfies Record<typeof BuildPlatform.Type, string>;
226
227export class DesktopIconSourceMissingError extends Schema.TaggedErrorClass<DesktopIconSourceMissingError>()(
228 "DesktopIconSourceMissingError",
229 {
230 platform: BuildPlatform,
231 sourcePath: Schema.String,
232 },
233) {
234 override get message(): string {
235 return `Desktop ${desktopIconPlatformNames[this.platform]} icon source is missing at ${this.sourcePath}`;
236 }
237}
238
239export class BundledClientAssetsMissingError extends Schema.TaggedErrorClass<BundledClientAssetsMissingError>()(
240 "BundledClientAssetsMissingError",
241 {
242 indexPath: Schema.String,
243 missingFiles: Schema.Array(Schema.String),
244 },
245) {
246 override get message(): string {
247 const preview = this.missingFiles.slice(0, 6).join(", ");
248 const suffix = this.missingFiles.length > 6 ? ` (+${this.missingFiles.length - 6} more)` : "";
249 return `Bundled client references missing files in ${this.indexPath}: ${preview}${suffix}. Rebuild web/server artifacts.`;
250 }
251}
252
253export class UnsupportedDesktopBuildPlatformError extends Schema.TaggedErrorClass<UnsupportedDesktopBuildPlatformError>()(
254 "UnsupportedDesktopBuildPlatformError",
255 {
256 platform: Schema.String,
257 },
258) {
259 override get message(): string {
260 return `Unsupported platform '${this.platform}'.`;
261 }
262}
263
264const dependencyResolutionDescriptions = {
265 "server-production": "production dependencies",
266 "workspace-overrides": "overrides",
267 "desktop-runtime": "desktop runtime dependencies",
268} as const;
269const DependencyResolutionKind = Schema.Literals([
270 "server-production",
271 "workspace-overrides",
272 "desktop-runtime",
273]);
274
275export class DesktopBuildDependencyResolutionError extends Schema.TaggedErrorClass<DesktopBuildDependencyResolutionError>()(
276 "DesktopBuildDependencyResolutionError",
277 {
278 kind: DependencyResolutionKind,
279 manifestPath: Schema.String,
280 cause: Schema.Defect(),
281 },
282) {
283 override get message(): string {
284 return `Could not resolve ${dependencyResolutionDescriptions[this.kind]} from ${this.manifestPath}.`;
285 }
286}
287
288export class MissingServerProductionDependenciesError extends Schema.TaggedErrorClass<MissingServerProductionDependenciesError>()(
289 "MissingServerProductionDependenciesError",
290 {
291 manifestPath: Schema.String,
292 },
293) {
294 override get message(): string {
295 return `Could not resolve production dependencies from ${this.manifestPath}.`;
296 }
297}
298
299const DesktopBuildInputArtifact = Schema.Literals([
300 "desktop-dist",
301 "desktop-resources",
302 "server-dist",
303 "bundled-server-client",
304]);
305type DesktopBuildInputArtifact = typeof DesktopBuildInputArtifact.Type;
306const desktopBuildInputArtifactNames = {
307 "desktop-dist": "desktopDist",
308 "desktop-resources": "desktopResources",
309 "server-dist": "serverDist",
310 "bundled-server-client": "bundled server client",
311} satisfies Record<DesktopBuildInputArtifact, string>;
312
313export class MissingDesktopBuildInputError extends Schema.TaggedErrorClass<MissingDesktopBuildInputError>()(
314 "MissingDesktopBuildInputError",
315 {
316 artifact: DesktopBuildInputArtifact,
317 artifactPath: Schema.String,
318 buildCommand: Schema.Literal("vp run build:desktop"),
319 },
320) {
321 override get message(): string {
322 return `Missing ${desktopBuildInputArtifactNames[this.artifact]} at ${this.artifactPath}. Run '${this.buildCommand}' first.`;
323 }
324}
325
326export class MacProvisioningProfileNotFoundError extends Schema.TaggedErrorClass<MacProvisioningProfileNotFoundError>()(
327 "MacProvisioningProfileNotFoundError",
328 {
329 provisioningProfilePath: Schema.String,
330 },
331) {
332 override get message(): string {
333 return `macOS provisioning profile not found: ${this.provisioningProfilePath}`;
334 }
335}
336
337export class DesktopBuildDistDirectoryMissingError extends Schema.TaggedErrorClass<DesktopBuildDistDirectoryMissingError>()(
338 "DesktopBuildDistDirectoryMissingError",
339 {
340 distPath: Schema.String,
341 platform: BuildPlatform,
342 arch: BuildArch,
343 },
344) {
345 override get message(): string {
346 return `Build completed but dist directory was not found at ${this.distPath}`;
347 }
348}
349
350export class DesktopBuildNoArtifactsProducedError extends Schema.TaggedErrorClass<DesktopBuildNoArtifactsProducedError>()(
351 "DesktopBuildNoArtifactsProducedError",
352 {
353 distPath: Schema.String,
354 platform: BuildPlatform,
355 arch: BuildArch,
356 },
357) {
358 override get message(): string {
359 return `Build completed but no files were produced in ${this.distPath}`;
142360 }
143361}
144362
......@@ -616,8 +834,12 @@ const stageClerkPasskeyNativeBinaries = Effect.fn("stageClerkPasskeyNativeBinari
616834 const sourcePath = yield* Effect.try({
617835 try: () => packageRequire.resolve(artifact.packageName),
618836 catch: (cause) =>
619 new BuildScriptError({
620 message: `Clerk passkey native package is missing: ${artifact.packageName}`,
837 new ClerkPasskeyNativePackageMissingError({
838 packageName: artifact.packageName,
839 binaryFileName: artifact.binaryFileName,
840 packageEntryPath,
841 platform,
842 arch,
621843 cause,
622844 }),
623845 });
......@@ -691,6 +913,18 @@ const MockUpdateServerPortSchema = Schema.NumberFromString.check(
691913);
692914const decodeMockUpdateServerPort = Schema.decodeUnknownEffect(MockUpdateServerPortSchema);
693915
916function invalidMockUpdateServerPortReason(
917 configuredPort: string,
918): typeof InvalidMockUpdateServerPortReason.Type {
919 const parsed = Number(configuredPort);
920 if (!Number.isFinite(parsed)) return "not-numeric";
921 if (!Number.isInteger(parsed)) return "not-integer";
922 if (parsed < 1 || parsed > 65535) return "out-of-range";
923 // This mapper is only called after schema decoding failed. An otherwise
924 // valid integer therefore used a representation the decoder did not accept.
925 return "not-numeric";
926}
927
694928const resolveBooleanFlag = (flag: Option.Option<boolean>, envValue: boolean) =>
695929 Option.getOrElse(flag, () => envValue);
696930const mergeOptions = <A>(a: Option.Option<A>, b: Option.Option<A>, defaultValue: A) =>
......@@ -722,9 +956,7 @@ export const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* (
722956 );
723957
724958 if (!platform) {
725 return yield* new BuildScriptError({
726 message: `Unsupported host platform '${hostPlatform}'.`,
727 });
959 return yield* new UnsupportedHostBuildPlatformError({ hostPlatform });
728960 }
729961
730962 const target = mergeOptions(input.target, env.target, PLATFORM_CONFIG[platform].defaultTarget);
......@@ -745,17 +977,16 @@ export const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* (
745977 const verbose = resolveBooleanFlag(input.verbose, env.verbose);
746978
747979 const mockUpdates = resolveBooleanFlag(input.mockUpdates, env.mockUpdates);
980 const configuredMockUpdateServerPort = Option.getOrUndefined(env.mockUpdateServerPort);
748981 const mockUpdateServerPort =
749982 Option.getOrUndefined(input.mockUpdateServerPort) ??
750 (yield* resolveMockUpdateServerPort(Option.getOrUndefined(env.mockUpdateServerPort)).pipe(
751 Effect.mapError(
752 (cause) =>
753 new BuildScriptError({
754 message: "Invalid mock update server port.",
755 cause,
756 }),
757 ),
758 ));
983 (configuredMockUpdateServerPort === undefined
984 ? undefined
985 : yield* resolveMockUpdateServerPort(configuredMockUpdateServerPort).pipe(
986 Effect.mapError((cause) =>
987 InvalidMockUpdateServerPortError.fromConfigValue(configuredMockUpdateServerPort, cause),
988 ),
989 ));
759990
760991 return {
761992 platform,
......@@ -775,7 +1006,7 @@ export const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* (
7751006const runCommand = Effect.fn("runCommand")(function* (
7761007 command: ChildProcess.Command,
7771008 options: {
778 readonly label?: string;
1009 readonly label: string;
7791010 readonly verbose: boolean;
7801011 },
7811012) {
......@@ -791,14 +1022,11 @@ const runCommand = Effect.fn("runCommand")(function* (
7911022 );
7921023
7931024 if (exitCode !== 0) {
794 const outputSections = [
795 options.label ? `Command: ${options.label}` : undefined,
796 formatOutputSection("stdout", stdout),
797 formatOutputSection("stderr", stderr),
798 ].filter((section): section is string => section !== undefined);
799 const outputSuffix = outputSections.length > 0 ? `\n\n${outputSections.join("\n\n")}` : "";
800 return yield* new BuildScriptError({
801 message: `Command exited with non-zero exit code (${exitCode})${outputSuffix}`,
1025 return yield* new BuildCommandFailedError({
1026 command: options.label,
1027 exitCode,
1028 ...(stdout.trim() ? { stdoutTail: stdout } : {}),
1029 ...(stderr.trim() ? { stderrTail: stderr } : {}),
8021030 });
8031031 }
8041032});
......@@ -845,8 +1073,9 @@ function stageMacIcons(stageResourcesDir: string, sourcePng: string, verbose: bo
8451073 const fs = yield* FileSystem.FileSystem;
8461074 const path = yield* Path.Path;
8471075 if (!(yield* fs.exists(sourcePng))) {
848 return yield* new BuildScriptError({
849 message: `Desktop macOS icon source is missing at ${sourcePng}`,
1076 return yield* new DesktopIconSourceMissingError({
1077 platform: "mac",
1078 sourcePath: sourcePng,
8501079 });
8511080 }
8521081
......@@ -871,8 +1100,9 @@ function stageLinuxIcons(stageResourcesDir: string, sourcePng: string, verbose:
8711100 const fs = yield* FileSystem.FileSystem;
8721101 const path = yield* Path.Path;
8731102 if (!(yield* fs.exists(sourcePng))) {
874 return yield* new BuildScriptError({
875 message: `Desktop Linux icon source is missing at ${sourcePng}`,
1103 return yield* new DesktopIconSourceMissingError({
1104 platform: "linux",
1105 sourcePath: sourcePng,
8761106 });
8771107 }
8781108
......@@ -931,8 +1161,9 @@ function stageWindowsIcons(stageResourcesDir: string, sourceIco: string) {
9311161 const fs = yield* FileSystem.FileSystem;
9321162 const path = yield* Path.Path;
9331163 if (!(yield* fs.exists(sourceIco))) {
934 return yield* new BuildScriptError({
935 message: `Desktop Windows icon source is missing at ${sourceIco}`,
1164 return yield* new DesktopIconSourceMissingError({
1165 platform: "win",
1166 sourcePath: sourceIco,
9361167 });
9371168 }
9381169
......@@ -969,10 +1200,9 @@ function validateBundledClientAssets(clientDir: string) {
9691200 }
9701201
9711202 if (missing.length > 0) {
972 const preview = missing.slice(0, 6).join(", ");
973 const suffix = missing.length > 6 ? ` (+${missing.length - 6} more)` : "";
974 return yield* new BuildScriptError({
975 message: `Bundled client references missing files in ${indexPath}: ${preview}${suffix}. Rebuild web/server artifacts.`,
1203 return yield* new BundledClientAssetsMissingError({
1204 indexPath,
1205 missingFiles: missing,
9761206 });
9771207 }
9781208 });
......@@ -1174,8 +1404,8 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
11741404
11751405 const platformConfig = PLATFORM_CONFIG[options.platform];
11761406 if (!platformConfig) {
1177 return yield* new BuildScriptError({
1178 message: `Unsupported platform '${options.platform}'.`,
1407 return yield* new UnsupportedDesktopBuildPlatformError({
1408 platform: options.platform,
11791409 });
11801410 }
11811411
......@@ -1183,16 +1413,17 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
11831413
11841414 const serverDependencies = serverPackageJson.dependencies;
11851415 if (!serverDependencies || Object.keys(serverDependencies).length === 0) {
1186 return yield* new BuildScriptError({
1187 message: "Could not resolve production dependencies from apps/server/package.json.",
1416 return yield* new MissingServerProductionDependenciesError({
1417 manifestPath: "apps/server/package.json",
11881418 });
11891419 }
11901420
11911421 const resolvedOverrides = yield* Effect.try({
11921422 try: () => resolveCatalogDependencies(workspaceOverrides, workspaceCatalog, "apps/desktop"),
11931423 catch: (cause) =>
1194 new BuildScriptError({
1195 message: "Could not resolve overrides from pnpm-workspace.yaml.",
1424 new DesktopBuildDependencyResolutionError({
1425 kind: "workspace-overrides",
1426 manifestPath: "pnpm-workspace.yaml",
11961427 cause,
11971428 }),
11981429 });
......@@ -1200,16 +1431,18 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
12001431 const resolvedServerDependencies = yield* Effect.try({
12011432 try: () => resolveCatalogDependencies(serverDependencies, workspaceCatalog, "apps/server"),
12021433 catch: (cause) =>
1203 new BuildScriptError({
1204 message: "Could not resolve production dependencies from apps/server/package.json.",
1434 new DesktopBuildDependencyResolutionError({
1435 kind: "server-production",
1436 manifestPath: "apps/server/package.json",
12051437 cause,
12061438 }),
12071439 });
12081440 const resolvedDesktopRuntimeDependencies = yield* Effect.try({
12091441 try: () => resolveDesktopRuntimeDependencies(desktopPackageJson.dependencies, workspaceCatalog),
12101442 catch: (cause) =>
1211 new BuildScriptError({
1212 message: "Could not resolve desktop runtime dependencies from apps/desktop/package.json.",
1443 new DesktopBuildDependencyResolutionError({
1444 kind: "desktop-runtime",
1445 manifestPath: "apps/desktop/package.json",
12131446 cause,
12141447 }),
12151448 });
......@@ -1243,17 +1476,25 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
12431476 );
12441477 }
12451478
1246 for (const [label, dir] of Object.entries(distDirs)) {
1247 if (!(yield* fs.exists(dir))) {
1248 return yield* new BuildScriptError({
1249 message: `Missing ${label} at ${dir}. Run 'vp run build:desktop' first.`,
1479 const requiredBuildInputs = [
1480 { artifact: "desktop-dist", artifactPath: distDirs.desktopDist },
1481 { artifact: "desktop-resources", artifactPath: distDirs.desktopResources },
1482 { artifact: "server-dist", artifactPath: distDirs.serverDist },
1483 ] as const;
1484 for (const input of requiredBuildInputs) {
1485 if (!(yield* fs.exists(input.artifactPath))) {
1486 return yield* new MissingDesktopBuildInputError({
1487 ...input,
1488 buildCommand: "vp run build:desktop",
12501489 });
12511490 }
12521491 }
12531492
12541493 if (!(yield* fs.exists(bundledClientEntry))) {
1255 return yield* new BuildScriptError({
1256 message: `Missing bundled server client at ${bundledClientEntry}. Run 'vp run build:desktop' first.`,
1494 return yield* new MissingDesktopBuildInputError({
1495 artifact: "bundled-server-client",
1496 artifactPath: bundledClientEntry,
1497 buildCommand: "vp run build:desktop",
12571498 });
12581499 }
12591500
......@@ -1285,7 +1526,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
12851526 options.platform === "mac" && options.signed
12861527 ? yield* Effect.try({
12871528 try: () => resolveMacPasskeySigningConfiguration(loadRepoEnv({ repoRoot })),
1288 catch: BuildScriptError.fromMacPasskeySigningConfiguration,
1529 catch: MacPasskeySigningConfigurationResolutionError.fromCause,
12891530 })
12901531 : undefined;
12911532 const macPasskeySigning = configuredMacPasskeySigning
......@@ -1302,8 +1543,8 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
13021543 : undefined;
13031544 if (macPasskeySigning && macEntitlementsPath) {
13041545 if (!(yield* fs.exists(macPasskeySigning.provisioningProfilePath))) {
1305 return yield* new BuildScriptError({
1306 message: `macOS provisioning profile not found: ${macPasskeySigning.provisioningProfilePath}`,
1546 return yield* new MacProvisioningProfileNotFoundError({
1547 provisioningProfilePath: macPasskeySigning.provisioningProfilePath,
13071548 });
13081549 }
13091550 yield* fs.writeFileString(macEntitlementsPath, renderMacPasskeyEntitlements(macPasskeySigning));
......@@ -1442,8 +1683,10 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
14421683
14431684 const stageDistDir = path.join(stageAppDir, "dist");
14441685 if (!(yield* fs.exists(stageDistDir))) {
1445 return yield* new BuildScriptError({
1446 message: `Build completed but dist directory was not found at ${stageDistDir}`,
1686 return yield* new DesktopBuildDistDirectoryMissingError({
1687 distPath: stageDistDir,
1688 platform: options.platform,
1689 arch: options.arch,
14471690 });
14481691 }
14491692
......@@ -1462,8 +1705,10 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
14621705 }
14631706
14641707 if (copiedArtifacts.length === 0) {
1465 return yield* new BuildScriptError({
1466 message: `Build completed but no files were produced in ${stageDistDir}`,
1708 return yield* new DesktopBuildNoArtifactsProducedError({
1709 distPath: stageDistDir,
1710 platform: options.platform,
1711 arch: options.arch,
14671712 });
14681713 }
14691714