authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-01 22:37:45-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-01 22:37:45-07:00
log082358f9ef6feaadfaafd59a7512da26beb00100
tree36dcd51f5de64bb9dda53b5fd2ec30e6bbbdcc3c
parent43bafd4674841fb8782f5174033b684aa0d83f67
signature Signed by PGP key B5690EEEBB952194

fix(desktop): check artifact build prerequisites (#8975)


4 files changed, 566 insertions(+), 21 deletions(-)

CONTRIBUTING.md+5
......@@ -1,5 +1,10 @@
11# Contributing
22
3## Developer Setup
4
5See the [maintainer scripts guide](docs/internals/scripts.md#first-checkout) for the initial checkout,
6development commands, tests, and platform-specific desktop packaging prerequisites.
7
38## Read This First
49
510We are not actively accepting contributions right now.
docs/internals/scripts.md+73
......@@ -74,6 +74,79 @@ authenticated.
7474- `vp run dist:desktop:win`: Builds a Windows NSIS installer into `./release`. `:arm64` and `:x64`
7575 variants exist.
7676
77### Linux AppImage prerequisites
78
79Linux AppImage packaging compiles the Rust resource monitor. Install a Rust toolchain, the standard
80C/C++ build tools, and ImageMagick before running `vp run dist:desktop:linux`.
81
82Ubuntu and Debian:
83
84```bash
85sudo apt-get update
86sudo apt-get install cargo rustc build-essential imagemagick
87```
88
89Fedora:
90
91```bash
92sudo dnf install rust cargo gcc gcc-c++ make ImageMagick
93```
94
95Arch Linux:
96
97```bash
98sudo pacman -S rust base-devel imagemagick
99```
100
101The artifact script checks these capabilities before starting the web and desktop builds. If
102anything is unavailable, it reports every failed check together and prints the Ubuntu/Debian
103installation command. The check compiles and links tiny temporary programs, so it also catches
104installed runtime X11 libraries that are missing their development headers or linker symlinks.
105
106### macOS DMG prerequisites
107
108Install the Xcode Command Line Tools and Rust before building a DMG:
109
110```bash
111xcode-select --install
112```
113
114Install Rust from [rustup.rs](https://rustup.rs). The artifact script checks Cargo, Clang, Make,
115`sips`, and `iconutil`. Universal builds additionally require `lipo`. It also verifies that Rust
116has every requested target; add missing targets with:
117
118```bash
119rustup target add aarch64-apple-darwin x86_64-apple-darwin
120```
121
122Unsigned local builds need no Apple credentials. Builds using `--signed` additionally require the
123certificate, provisioning profile, team ID, and notarization configuration described below.
124
125### Windows installer prerequisites
126
127Install Rust from [rustup.rs](https://rustup.rs), Python 3, and Visual Studio Build Tools. In the
128Visual Studio Installer, select **Desktop development with C++** and include:
129
130- MSVC x64/x86 build tools
131- A Windows 10 or Windows 11 SDK
132- MSVC Spectre-mitigated libraries
133
134ARM64 installers require the corresponding MSVC ARM64 build tools and Spectre-mitigated libraries
135instead of the x64/x86 components.
136
137Add the Rust target matching the installer architecture:
138
139```powershell
140rustup target add x86_64-pc-windows-msvc
141# For an arm64 installer:
142rustup target add aarch64-pc-windows-msvc
143```
144
145Windows supplies `tar.exe`; it is checked when `--wsl-prebuild` makes the artifact include the WSL
146runtime. NSIS is downloaded by electron-builder and does not need a separate installation.
147Unsigned local builds need no Azure credentials. Builds using `--signed` additionally require the
148Azure Trusted Signing configuration described below.
149
77150### Desktop `.dmg` packaging notes
78151
79152- Default build is unsigned/not notarized for local sharing.
scripts/build-desktop-artifact.test.ts+167-2
......@@ -32,9 +32,14 @@ import {
3232 UnsupportedDesktopBuildArchitectureError,
3333 isMacPasskeySigningConfigurationError,
3434 LinuxIconResizeError,
35 LinuxDesktopBuildPrerequisitesMissingError,
36 MacDesktopBuildPrerequisitesMissingError,
3537 MacPasskeySigningConfigurationResolutionError,
3638 MissingMacPasskeyProvisioningProfileError,
3739 packWindowsServerAsar,
40 preflightLinuxDesktopBuild,
41 preflightMacDesktopBuild,
42 preflightWindowsDesktopBuild,
3843 renderMacPasskeyEntitlements,
3944 resolveClerkPasskeyNativeArtifacts,
4045 resolveMacPasskeySigningConfiguration,
......@@ -63,6 +68,7 @@ import {
6368 copyDirectoryPreservingSymlinks,
6469 validateWindowsPackagedPayload,
6570 WindowsPrimaryNativeProbeError,
71 WindowsDesktopBuildPrerequisitesMissingError,
6672 WindowsPackagedPayloadValidationError,
6773 WINDOWS_PACKAGED_PAYLOAD_FILE_LIMIT,
6874 WINDOWS_SERVER_ASAR_IGNORE_GLOBS,
......@@ -102,7 +108,8 @@ const stageWslRuntimeTreeFixture = Effect.fn("stageWslRuntimeTreeFixture")(funct
102108 );
103109});
104110
105function mockProcess(exitCode: number) {
111function mockProcess(exitCode: number, stdout = "") {
112 const encodedStdout = new TextEncoder().encode(stdout);
106113 return ChildProcessSpawner.makeHandle({
107114 pid: ChildProcessSpawner.ProcessId(1),
108115 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(exitCode)),
......@@ -110,7 +117,7 @@ function mockProcess(exitCode: number) {
110117 kill: () => Effect.void,
111118 unref: Effect.succeed(Effect.void),
112119 stdin: Sink.drain,
113 stdout: Stream.empty,
120 stdout: stdout ? Stream.make(encodedStdout) : Stream.empty,
114121 stderr: Stream.empty,
115122 all: Stream.empty,
116123 getInputFd: () => Sink.drain,
......@@ -812,6 +819,164 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
812819 ),
813820 );
814821
822 it.effect("reports every missing Linux desktop build prerequisite with an install command", () =>
823 Effect.scoped(
824 Effect.gen(function* () {
825 const commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> =
826 [];
827 const spawner = Layer.succeed(
828 ChildProcessSpawner.ChildProcessSpawner,
829 ChildProcessSpawner.make((command) => {
830 const childProcess = command as unknown as {
831 readonly command: string;
832 readonly args: ReadonlyArray<string>;
833 };
834 commands.push(childProcess);
835 const fails = childProcess.command === "cargo" || childProcess.command === "rustc";
836 return Effect.succeed(mockProcess(fails ? 1 : 0));
837 }),
838 );
839
840 const error = yield* preflightLinuxDesktopBuild("arm64").pipe(
841 Effect.provide(spawner),
842 Effect.flip,
843 );
844
845 assert.instanceOf(error, LinuxDesktopBuildPrerequisitesMissingError);
846 assert.deepStrictEqual(error.missing, ["cargo", "rust-target"]);
847 assert.include(error.message, "Rust compiler and Cargo (cargo, rustc)");
848 assert.include(error.message, "Requested Rust standard library");
849 assert.include(error.message, "sudo apt-get install cargo rustc");
850 assert.include(error.message, "rustup target add aarch64-unknown-linux-gnu");
851 assert.isTrue(
852 commands.some(
853 (command) =>
854 command.command === "rustc" && command.args.includes("aarch64-unknown-linux-gnu"),
855 ),
856 );
857 }),
858 ),
859 );
860
861 it.effect("reports missing macOS tools and Rust targets before building", () =>
862 Effect.scoped(
863 Effect.gen(function* () {
864 const spawner = Layer.succeed(
865 ChildProcessSpawner.ChildProcessSpawner,
866 ChildProcessSpawner.make((command) => {
867 const childProcess = command as unknown as {
868 readonly command: string;
869 readonly args: ReadonlyArray<string>;
870 };
871 const fails =
872 childProcess.command === "rustc" ||
873 (childProcess.command === "xcrun" && childProcess.args.includes("iconutil"));
874 return Effect.succeed(mockProcess(fails ? 1 : 0));
875 }),
876 );
877 const error = yield* preflightMacDesktopBuild("universal").pipe(
878 Effect.provide(spawner),
879 Effect.flip,
880 );
881
882 assert.instanceOf(error, MacDesktopBuildPrerequisitesMissingError);
883 assert.deepStrictEqual(error.missing, ["rust", "iconutil"]);
884 assert.deepStrictEqual(error.rustTargets, ["aarch64-apple-darwin", "x86_64-apple-darwin"]);
885 assert.include(error.message, "xcode-select --install");
886 assert.include(error.message, "rustup target add aarch64-apple-darwin x86_64-apple-darwin");
887 }),
888 ),
889 );
890
891 it.effect("reports missing Windows toolchain capabilities before building", () =>
892 Effect.scoped(
893 Effect.gen(function* () {
894 const fs = yield* FileSystem.FileSystem;
895 const path = yield* Path.Path;
896 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-windows-preflight-" });
897 const pythonPath = path.join(tempDir, "python.exe");
898 yield* fs.writeFileString(pythonPath, "python");
899 const spawner = Layer.succeed(
900 ChildProcessSpawner.ChildProcessSpawner,
901 ChildProcessSpawner.make((command) => {
902 const childProcess = command as unknown as { readonly command: string };
903 const fails =
904 childProcess.command === "rustc" ||
905 childProcess.command === "powershell.exe" ||
906 childProcess.command === pythonPath;
907 return Effect.succeed(mockProcess(fails ? 1 : 0));
908 }),
909 );
910 const error = yield* preflightWindowsDesktopBuild({
911 arch: "x64",
912 bundlesWslRuntime: true,
913 }).pipe(
914 Effect.provide(
915 Layer.merge(
916 spawner,
917 ConfigProvider.layer(
918 ConfigProvider.fromEnv({ env: { npm_config_python: pythonPath } }),
919 ),
920 ),
921 ),
922 Effect.flip,
923 );
924
925 assert.instanceOf(error, WindowsDesktopBuildPrerequisitesMissingError);
926 assert.deepStrictEqual(error.missing, ["rust", "python", "msvc"]);
927 assert.equal(error.rustTarget, "x86_64-pc-windows-msvc");
928 assert.include(error.message, "Visual Studio Build Tools components");
929 }),
930 ),
931 );
932
933 it.effect("rejects a PATH-discovered Python executable that is not Python 3", () =>
934 Effect.scoped(
935 Effect.gen(function* () {
936 const fs = yield* FileSystem.FileSystem;
937 const path = yield* Path.Path;
938 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-python2-preflight-" });
939 const pythonPath = path.join(tempDir, "python");
940 yield* fs.writeFileString(pythonPath, "python2");
941 const spawner = Layer.succeed(
942 ChildProcessSpawner.ChildProcessSpawner,
943 ChildProcessSpawner.make((command) => {
944 const childProcess = command as unknown as {
945 readonly command: string;
946 readonly args: ReadonlyArray<string>;
947 };
948 if (childProcess.command === "python") {
949 return Effect.succeed(mockProcess(0, `${pythonPath}\n`));
950 }
951 if (childProcess.command === pythonPath) {
952 return Effect.succeed(mockProcess(1));
953 }
954 return Effect.succeed(mockProcess(0));
955 }),
956 );
957 const error = yield* preflightWindowsDesktopBuild({
958 arch: "x64",
959 bundlesWslRuntime: false,
960 }).pipe(
961 Effect.provide(
962 Layer.merge(
963 spawner,
964 ConfigProvider.layer(
965 ConfigProvider.fromEnv({
966 env: { T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR: "true" },
967 }),
968 ),
969 ),
970 ),
971 Effect.flip,
972 );
973
974 assert.instanceOf(error, WindowsDesktopBuildPrerequisitesMissingError);
975 assert.deepStrictEqual(error.missing, ["python"]);
976 }),
977 ),
978 );
979
815980 it.effect("validates every ASAR-unpacked native in the packaged Windows payload", () =>
816981 Effect.scoped(
817982 Effect.gen(function* () {
scripts/build-desktop-artifact.ts+321-19
......@@ -287,6 +287,125 @@ export class BuildCommandFailedError extends Schema.TaggedErrorClass<BuildComman
287287 }
288288}
289289
290export const LINUX_DESKTOP_BUILD_PREREQUISITES = [
291 { id: "cargo", description: "Rust compiler and Cargo", packages: ["cargo", "rustc"] },
292 { id: "rust-target", description: "Requested Rust standard library", packages: [] },
293 { id: "cc", description: "C/C++ build toolchain", packages: ["build-essential"] },
294 { id: "make", description: "Make", packages: ["build-essential"] },
295 { id: "imagemagick", description: "ImageMagick", packages: ["imagemagick"] },
296] as const;
297
298export class LinuxDesktopBuildPrerequisitesMissingError extends Schema.TaggedErrorClass<LinuxDesktopBuildPrerequisitesMissingError>()(
299 "LinuxDesktopBuildPrerequisitesMissingError",
300 {
301 missing: Schema.Array(Schema.String),
302 rustTarget: Schema.String,
303 },
304) {
305 override get message(): string {
306 const missingRequirements = LINUX_DESKTOP_BUILD_PREREQUISITES.filter((requirement) =>
307 this.missing.includes(requirement.id),
308 );
309 const details = missingRequirements
310 .map((requirement) =>
311 requirement.packages.length > 0
312 ? ` - ${requirement.description} (${requirement.packages.join(", ")})`
313 : ` - ${requirement.description}`,
314 )
315 .join("\n");
316 const packages = [
317 ...new Set(missingRequirements.flatMap((requirement) => requirement.packages)),
318 ];
319 return [
320 "Linux desktop build prerequisites are missing:",
321 details,
322 "",
323 ...(packages.length > 0
324 ? ["On Ubuntu/Debian, install them with:", ` sudo apt-get install ${packages.join(" ")}`]
325 : []),
326 ...(this.missing.includes("rust-target")
327 ? ["Add the requested Rust target with:", ` rustup target add ${this.rustTarget}`]
328 : []),
329 "",
330 "For other distributions, see docs/internals/scripts.md#linux-appimage-prerequisites.",
331 "Then rerun `vp run dist:desktop:linux`.",
332 ].join("\n");
333 }
334}
335
336const MAC_DESKTOP_BUILD_PREREQUISITES = [
337 { id: "rust", description: "Rust/Cargo and the requested Rust target" },
338 { id: "clang", description: "Xcode Command Line Tools (clang)" },
339 { id: "make", description: "Xcode Command Line Tools (make)" },
340 { id: "sips", description: "macOS image tool (sips)" },
341 { id: "iconutil", description: "macOS icon tool (iconutil)" },
342 { id: "lipo", description: "Xcode universal-binary tool (lipo)" },
343] as const;
344
345export class MacDesktopBuildPrerequisitesMissingError extends Schema.TaggedErrorClass<MacDesktopBuildPrerequisitesMissingError>()(
346 "MacDesktopBuildPrerequisitesMissingError",
347 {
348 missing: Schema.Array(Schema.String),
349 rustTargets: Schema.Array(Schema.String),
350 },
351) {
352 override get message(): string {
353 const details = MAC_DESKTOP_BUILD_PREREQUISITES.filter((requirement) =>
354 this.missing.includes(requirement.id),
355 )
356 .map((requirement) => ` - ${requirement.description}`)
357 .join("\n");
358 return [
359 "macOS desktop build prerequisites are missing:",
360 details,
361 "",
362 "Install Apple's build tools with:",
363 " xcode-select --install",
364 "Install Rust from https://rustup.rs, then add the requested target(s):",
365 ` rustup target add ${this.rustTargets.join(" ")}`,
366 "",
367 "Then rerun the desktop artifact command.",
368 ].join("\n");
369 }
370}
371
372const WINDOWS_DESKTOP_BUILD_PREREQUISITES = [
373 { id: "rust", description: "Rust/Cargo and the requested MSVC Rust target" },
374 { id: "python", description: "Python 3 for node-gyp" },
375 {
376 id: "msvc",
377 description: "Visual Studio Build Tools with C++, Windows SDK, and Spectre libraries",
378 },
379 { id: "tar", description: "tar for the bundled WSL runtime" },
380] as const;
381
382export class WindowsDesktopBuildPrerequisitesMissingError extends Schema.TaggedErrorClass<WindowsDesktopBuildPrerequisitesMissingError>()(
383 "WindowsDesktopBuildPrerequisitesMissingError",
384 {
385 missing: Schema.Array(Schema.String),
386 rustTarget: Schema.String,
387 },
388) {
389 override get message(): string {
390 const details = WINDOWS_DESKTOP_BUILD_PREREQUISITES.filter((requirement) =>
391 this.missing.includes(requirement.id),
392 )
393 .map((requirement) => ` - ${requirement.description}`)
394 .join("\n");
395 return [
396 "Windows desktop build prerequisites are missing:",
397 details,
398 "",
399 "Install Rust from https://rustup.rs and add the requested target:",
400 ` rustup target add ${this.rustTarget}`,
401 "Install Python 3 and the Visual Studio Build Tools components listed in",
402 "docs/internals/scripts.md#windows-installer-prerequisites.",
403 "",
404 "Then rerun the desktop artifact command.",
405 ].join("\n");
406 }
407}
408
290409export class ResourceMonitorBuildOutputMissingError extends Schema.TaggedErrorClass<ResourceMonitorBuildOutputMissingError>()(
291410 "ResourceMonitorBuildOutputMissingError",
292411 {
......@@ -721,8 +840,18 @@ const resolvePythonForNodeGyp = Effect.fn("resolvePythonForNodeGyp")(function* (
721840 ),
722841 localAppData: Config.string("LOCALAPPDATA").pipe(Config.option),
723842 });
843 const isPython3 = (candidate: string) =>
844 spawnAndCollectOutput(
845 ChildProcess.make(candidate, [
846 "-c",
847 "import sys; raise SystemExit(0 if sys.version_info.major == 3 else 1)",
848 ]),
849 ).pipe(
850 Effect.map((result) => result.exitCode === 0),
851 Effect.orElseSucceed(() => false),
852 );
724853 const configured = Option.getOrUndefined(env.configuredPython);
725 if (configured && (yield* fs.exists(configured))) {
854 if (configured && (yield* fs.exists(configured)) && (yield* isPython3(configured))) {
726855 return configured;
727856 }
728857
......@@ -731,7 +860,7 @@ const resolvePythonForNodeGyp = Effect.fn("resolvePythonForNodeGyp")(function* (
731860 if (localAppData) {
732861 for (const version of ["Python313", "Python312", "Python311", "Python310"]) {
733862 const candidate = path.join(localAppData, "Programs", "Python", version, "python.exe");
734 if (yield* fs.exists(candidate)) {
863 if ((yield* fs.exists(candidate)) && (yield* isPython3(candidate))) {
735864 return candidate;
736865 }
737866 }
......@@ -753,7 +882,7 @@ const resolvePythonForNodeGyp = Effect.fn("resolvePythonForNodeGyp")(function* (
753882 }
754883
755884 const executable = probe.stdout.trim();
756 if (!executable || !(yield* fs.exists(executable))) {
885 if (!executable || !(yield* fs.exists(executable)) || !(yield* isPython3(executable))) {
757886 return undefined;
758887 }
759888
......@@ -1468,6 +1597,164 @@ const runCommand = Effect.fn("runCommand")(function* (
14681597 }
14691598});
14701599
1600const desktopBuildProbeSucceeds = Effect.fn("desktopBuildProbeSucceeds")(function* (
1601 command: ChildProcess.Command,
1602 label: string,
1603) {
1604 return yield* runCommand(command, { label, verbose: false }).pipe(
1605 Effect.as(true),
1606 Effect.orElseSucceed(() => false),
1607 );
1608});
1609
1610const rustTargetIsInstalled = Effect.fn("rustTargetIsInstalled")(function* (target: string) {
1611 const fs = yield* FileSystem.FileSystem;
1612 const result = yield* spawnAndCollectOutput(
1613 ChildProcess.make("rustc", ["--print", "target-libdir", "--target", target]),
1614 ).pipe(Effect.orElseSucceed(() => null));
1615 if (result === null || result.exitCode !== 0) return false;
1616
1617 const targetLibDir = result.stdout.trim();
1618 if (targetLibDir === "") return false;
1619 const entries = yield* fs.readDirectory(targetLibDir).pipe(Effect.orElseSucceed(() => []));
1620 return entries.some((entry) => entry.startsWith("libstd-") && entry.endsWith(".rlib"));
1621});
1622
1623export const preflightLinuxDesktopBuild = Effect.fn("preflightLinuxDesktopBuild")(function* (
1624 arch: typeof BuildArch.Type = "x64",
1625) {
1626 const reuseResourceMonitor = yield* Config.boolean("T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR").pipe(
1627 Config.withDefault(false),
1628 );
1629 const rustTarget = resolveResourceMonitorRustTargets("linux", arch)[0]!;
1630
1631 const checks = yield* Effect.all(
1632 {
1633 cargo: reuseResourceMonitor
1634 ? Effect.succeed(true)
1635 : desktopBuildProbeSucceeds(ChildProcess.make("cargo", ["--version"]), "cargo"),
1636 "rust-target": reuseResourceMonitor
1637 ? Effect.succeed(true)
1638 : rustTargetIsInstalled(rustTarget),
1639 cc: desktopBuildProbeSucceeds(ChildProcess.make("cc", ["--version"]), "cc"),
1640 make: desktopBuildProbeSucceeds(ChildProcess.make("make", ["--version"]), "make"),
1641 imagemagick: Effect.all([
1642 desktopBuildProbeSucceeds(ChildProcess.make("magick", ["-version"]), "magick"),
1643 desktopBuildProbeSucceeds(ChildProcess.make("convert", ["-version"]), "convert"),
1644 ]).pipe(Effect.map(([magick, convert]) => magick || convert)),
1645 },
1646 { concurrency: "unbounded" },
1647 );
1648 const missing = LINUX_DESKTOP_BUILD_PREREQUISITES.filter(
1649 (requirement) => !checks[requirement.id],
1650 ).map((requirement) => requirement.id);
1651
1652 if (missing.length > 0) {
1653 return yield* new LinuxDesktopBuildPrerequisitesMissingError({ missing, rustTarget });
1654 }
1655});
1656
1657export const preflightMacDesktopBuild = Effect.fn("preflightMacDesktopBuild")(function* (
1658 arch: typeof BuildArch.Type,
1659) {
1660 const rustTargets = resolveResourceMonitorRustTargets("mac", arch);
1661 const reuseResourceMonitor = yield* Config.boolean("T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR").pipe(
1662 Config.withDefault(false),
1663 );
1664 const checks = yield* Effect.all(
1665 {
1666 rust: reuseResourceMonitor
1667 ? Effect.succeed(true)
1668 : Effect.all([
1669 desktopBuildProbeSucceeds(ChildProcess.make("cargo", ["--version"]), "cargo"),
1670 Effect.forEach(rustTargets, rustTargetIsInstalled).pipe(
1671 Effect.map((results) => results.every(Boolean)),
1672 ),
1673 ]).pipe(Effect.map(([cargo, targets]) => cargo && targets)),
1674 clang: desktopBuildProbeSucceeds(ChildProcess.make("clang", ["--version"]), "clang"),
1675 make: desktopBuildProbeSucceeds(ChildProcess.make("make", ["--version"]), "make"),
1676 sips: desktopBuildProbeSucceeds(ChildProcess.make("sips", ["--help"]), "sips"),
1677 iconutil: desktopBuildProbeSucceeds(
1678 ChildProcess.make("xcrun", ["--find", "iconutil"]),
1679 "iconutil",
1680 ),
1681 lipo:
1682 arch === "universal"
1683 ? desktopBuildProbeSucceeds(ChildProcess.make("lipo", ["-version"]), "lipo")
1684 : Effect.succeed(true),
1685 },
1686 { concurrency: "unbounded" },
1687 );
1688 const missing = MAC_DESKTOP_BUILD_PREREQUISITES.filter(
1689 (requirement) => !checks[requirement.id],
1690 ).map((requirement) => requirement.id);
1691 if (missing.length > 0) {
1692 return yield* new MacDesktopBuildPrerequisitesMissingError({ missing, rustTargets });
1693 }
1694});
1695
1696function windowsVswherePrerequisiteScript(arch: typeof BuildArch.Type): string {
1697 const components =
1698 arch === "arm64"
1699 ? [
1700 "Microsoft.VisualStudio.Component.VC.Tools.ARM64",
1701 "Microsoft.VisualStudio.Component.VC.Tools.ARM64.Spectre",
1702 ]
1703 : [
1704 "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
1705 "Microsoft.VisualStudio.Component.VC.Tools.x86.x64.Spectre",
1706 ];
1707 return [
1708 "$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\\Installer\\vswhere.exe'",
1709 "if (!(Test-Path $vswhere)) { exit 1 }",
1710 `$install = & $vswhere -latest -products * -requires ${components.join(" ")} -property installationPath`,
1711 "if (!$install) { exit 1 }",
1712 "$kitsRoot = Get-ItemPropertyValue 'HKLM:\\SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots' -Name KitsRoot10 -ErrorAction SilentlyContinue",
1713 "if (!$kitsRoot -or !(Test-Path (Join-Path $kitsRoot 'Lib'))) { exit 1 }",
1714 ].join("; ");
1715}
1716
1717export const preflightWindowsDesktopBuild = Effect.fn("preflightWindowsDesktopBuild")(
1718 function* (input: { readonly arch: typeof BuildArch.Type; readonly bundlesWslRuntime: boolean }) {
1719 const rustTarget = resolveResourceMonitorRustTargets("win", input.arch)[0]!;
1720 const reuseResourceMonitor = yield* Config.boolean(
1721 "T3CODE_DESKTOP_REUSE_RESOURCE_MONITOR",
1722 ).pipe(Config.withDefault(false));
1723 const python = yield* resolvePythonForNodeGyp();
1724 const checks = yield* Effect.all(
1725 {
1726 rust: reuseResourceMonitor
1727 ? Effect.succeed(true)
1728 : Effect.all([
1729 desktopBuildProbeSucceeds(ChildProcess.make("cargo", ["--version"]), "cargo"),
1730 rustTargetIsInstalled(rustTarget),
1731 ]).pipe(Effect.map(([cargo, target]) => cargo && target)),
1732 python: Effect.succeed(python !== undefined),
1733 msvc: desktopBuildProbeSucceeds(
1734 ChildProcess.make("powershell.exe", [
1735 "-NoLogo",
1736 "-NoProfile",
1737 "-NonInteractive",
1738 "-Command",
1739 windowsVswherePrerequisiteScript(input.arch),
1740 ]),
1741 "Visual Studio Build Tools",
1742 ),
1743 tar: input.bundlesWslRuntime
1744 ? desktopBuildProbeSucceeds(ChildProcess.make("tar.exe", ["--version"]), "tar")
1745 : Effect.succeed(true),
1746 },
1747 { concurrency: "unbounded" },
1748 );
1749 const missing = WINDOWS_DESKTOP_BUILD_PREREQUISITES.filter(
1750 (requirement) => !checks[requirement.id],
1751 ).map((requirement) => requirement.id);
1752 if (missing.length > 0) {
1753 return yield* new WindowsDesktopBuildPrerequisitesMissingError({ missing, rustTarget });
1754 }
1755 },
1756);
1757
14711758/**
14721759 * Every `node_modules` directory that would be visible from `startDir`.
14731760 *
......@@ -1714,22 +2001,22 @@ const verifyPackagedBundleIsSelfContained = Effect.fn("verifyPackagedBundleIsSel
17142001 // stdin, a port, a lock) would otherwise hang release CI until the job
17152002 // times out with nothing useful in the log.
17162003 Effect.timeout(BUNDLE_SELF_CHECK_TIMEOUT),
1717 Effect.catchTag("TimeoutError", () =>
1718 Effect.fail(
1719 new BundleNotSelfContainedError({
1720 exitCode: -1,
1721 output: `The packaged bundle did not print its version within ${Duration.toSeconds(BUNDLE_SELF_CHECK_TIMEOUT)}s; it is hanging rather than failing to resolve.`,
1722 }),
1723 ),
1724 ),
1725 Effect.catchTag("BuildCommandFailedError", (error) =>
1726 Effect.fail(
1727 new BundleNotSelfContainedError({
1728 exitCode: error.exitCode,
1729 output: `${error.stderrTail ?? ""}${error.stdoutTail ?? ""}`.trim(),
1730 }),
1731 ),
1732 ),
2004 Effect.catchTags({
2005 TimeoutError: () =>
2006 Effect.fail(
2007 new BundleNotSelfContainedError({
2008 exitCode: -1,
2009 output: `The packaged bundle did not print its version within ${Duration.toSeconds(BUNDLE_SELF_CHECK_TIMEOUT)}s; it is hanging rather than failing to resolve.`,
2010 }),
2011 ),
2012 BuildCommandFailedError: (error) =>
2013 Effect.fail(
2014 new BundleNotSelfContainedError({
2015 exitCode: error.exitCode,
2016 output: `${error.stderrTail ?? ""}${error.stdoutTail ?? ""}`.trim(),
2017 }),
2018 ),
2019 }),
17332020 );
17342021 },
17352022);
......@@ -2899,6 +3186,21 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
28993186 const path = yield* Path.Path;
29003187 const fs = yield* FileSystem.FileSystem;
29013188 const hostPlatform = yield* HostProcessPlatform;
3189 if (hostPlatform === "linux" && options.platform === "linux") {
3190 yield* preflightLinuxDesktopBuild(options.arch);
3191 }
3192 if (hostPlatform === "darwin" && options.platform === "mac") {
3193 yield* preflightMacDesktopBuild(options.arch);
3194 }
3195 if (hostPlatform === "win32" && options.platform === "win") {
3196 yield* preflightWindowsDesktopBuild({
3197 arch: options.arch,
3198 bundlesWslRuntime: bundlesWslRuntime({
3199 arch: options.arch,
3200 prebuildPath: options.wslPrebuild,
3201 }),
3202 });
3203 }
29023204 const workspaceConfig = yield* readWorkspaceConfig();
29033205 const workspaceCatalog = workspaceConfig.catalog ?? {};
29043206 const workspaceOverrides = workspaceConfig.overrides ?? {};