History

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

Julius Marminge committed 2 months ago

Commit 082358f1 parent 43bafd44 files · +566 −21Browse files

Unified · Split

4 files
  1. CONTRIBUTING.md +5 −0
  2. docs/internals/scripts.md +73 −0
  3. scripts/build-desktop-artifact.test.ts +167 −2
  4. scripts/build-desktop-artifact.ts +321 −19
CONTRIBUTING.md+5−0
@@ -1,5 +1,10 @@
1# Contributing1# Contributing
2 2
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
3## Read This First8## Read This First
4 9
5We are not actively accepting contributions right now.10We are not actively accepting contributions right now.
docs/internals/scripts.md+73−0
@@ -74,6 +74,79 @@ authenticated. · 73 unmodified lines
74- `vp run dist:desktop:win`: Builds a Windows NSIS installer into `./release`. `:arm64` and `:x64`74- `vp run dist:desktop:win`: Builds a Windows NSIS installer into `./release`. `:arm64` and `:x64`
75 variants exist.75 variants exist.
76 76
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
77### Desktop `.dmg` packaging notes150### Desktop `.dmg` packaging notes
78 151
79- Default build is unsigned/not notarized for local sharing.152- Default build is unsigned/not notarized for local sharing.
scripts/build-desktop-artifact.test.ts+167−2
@@ -32,9 +32,14 @@ import { · 31 unmodified lines
32 UnsupportedDesktopBuildArchitectureError,32 UnsupportedDesktopBuildArchitectureError,
33 isMacPasskeySigningConfigurationError,33 isMacPasskeySigningConfigurationError,
34 LinuxIconResizeError,34 LinuxIconResizeError,
35 LinuxDesktopBuildPrerequisitesMissingError,
36 MacDesktopBuildPrerequisitesMissingError,
35 MacPasskeySigningConfigurationResolutionError,37 MacPasskeySigningConfigurationResolutionError,
36 MissingMacPasskeyProvisioningProfileError,38 MissingMacPasskeyProvisioningProfileError,
37 packWindowsServerAsar,39 packWindowsServerAsar,
40 preflightLinuxDesktopBuild,
41 preflightMacDesktopBuild,
42 preflightWindowsDesktopBuild,
38 renderMacPasskeyEntitlements,43 renderMacPasskeyEntitlements,
39 resolveClerkPasskeyNativeArtifacts,44 resolveClerkPasskeyNativeArtifacts,
40 resolveMacPasskeySigningConfiguration,45 resolveMacPasskeySigningConfiguration,
@@ -63,6 +68,7 @@ import { · 22 unmodified lines
63 copyDirectoryPreservingSymlinks,68 copyDirectoryPreservingSymlinks,
64 validateWindowsPackagedPayload,69 validateWindowsPackagedPayload,
65 WindowsPrimaryNativeProbeError,70 WindowsPrimaryNativeProbeError,
71 WindowsDesktopBuildPrerequisitesMissingError,
66 WindowsPackagedPayloadValidationError,72 WindowsPackagedPayloadValidationError,
67 WINDOWS_PACKAGED_PAYLOAD_FILE_LIMIT,73 WINDOWS_PACKAGED_PAYLOAD_FILE_LIMIT,
68 WINDOWS_SERVER_ASAR_IGNORE_GLOBS,74 WINDOWS_SERVER_ASAR_IGNORE_GLOBS,
@@ -102,7 +108,8 @@ const stageWslRuntimeTreeFixture = Effect.fn("stageWslRuntimeTreeFixture")(funct · 33 unmodified lines
102 );108 );
103});109});
104 110
105function mockProcess(exitCode: number) {111function mockProcess(exitCode: number, stdout = "") {
112 const encodedStdout = new TextEncoder().encode(stdout);
106 return ChildProcessSpawner.makeHandle({113 return ChildProcessSpawner.makeHandle({
107 pid: ChildProcessSpawner.ProcessId(1),114 pid: ChildProcessSpawner.ProcessId(1),
108 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(exitCode)),115 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(exitCode)),
@@ -110,7 +117,7 @@ function mockProcess(exitCode: number) { · 1 unmodified line
110 kill: () => Effect.void,117 kill: () => Effect.void,
111 unref: Effect.succeed(Effect.void),118 unref: Effect.succeed(Effect.void),
112 stdin: Sink.drain,119 stdin: Sink.drain,
113 stdout: Stream.empty,120 stdout: stdout ? Stream.make(encodedStdout) : Stream.empty,
114 stderr: Stream.empty,121 stderr: Stream.empty,
115 all: Stream.empty,122 all: Stream.empty,
116 getInputFd: () => Sink.drain,123 getInputFd: () => Sink.drain,
@@ -812,6 +819,164 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { · 695 unmodified lines
812 ),819 ),
813 );820 );
814 821
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
815 it.effect("validates every ASAR-unpacked native in the packaged Windows payload", () =>980 it.effect("validates every ASAR-unpacked native in the packaged Windows payload", () =>
816 Effect.scoped(981 Effect.scoped(
817 Effect.gen(function* () {982 Effect.gen(function* () {
scripts/build-desktop-artifact.ts+321−19
@@ -287,6 +287,125 @@ export class BuildCommandFailedError extends Schema.TaggedErrorClass<BuildComman · 286 unmodified lines
287 }287 }
288}288}
289 289
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
290export class ResourceMonitorBuildOutputMissingError extends Schema.TaggedErrorClass<ResourceMonitorBuildOutputMissingError>()(409export class ResourceMonitorBuildOutputMissingError extends Schema.TaggedErrorClass<ResourceMonitorBuildOutputMissingError>()(
291 "ResourceMonitorBuildOutputMissingError",410 "ResourceMonitorBuildOutputMissingError",
292 {411 {
@@ -721,8 +840,18 @@ const resolvePythonForNodeGyp = Effect.fn("resolvePythonForNodeGyp")(function* ( · 428 unmodified lines
721 ),840 ),
722 localAppData: Config.string("LOCALAPPDATA").pipe(Config.option),841 localAppData: Config.string("LOCALAPPDATA").pipe(Config.option),
723 });842 });
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 );
724 const configured = Option.getOrUndefined(env.configuredPython);853 const configured = Option.getOrUndefined(env.configuredPython);
725 if (configured && (yield* fs.exists(configured))) {854 if (configured && (yield* fs.exists(configured)) && (yield* isPython3(configured))) {
726 return configured;855 return configured;
727 }856 }
728 857
@@ -731,7 +860,7 @@ const resolvePythonForNodeGyp = Effect.fn("resolvePythonForNodeGyp")(function* ( · 2 unmodified lines
731 if (localAppData) {860 if (localAppData) {
732 for (const version of ["Python313", "Python312", "Python311", "Python310"]) {861 for (const version of ["Python313", "Python312", "Python311", "Python310"]) {
733 const candidate = path.join(localAppData, "Programs", "Python", version, "python.exe");862 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))) {
735 return candidate;864 return candidate;
736 }865 }
737 }866 }
@@ -753,7 +882,7 @@ const resolvePythonForNodeGyp = Effect.fn("resolvePythonForNodeGyp")(function* ( · 15 unmodified lines
753 }882 }
754 883
755 const executable = probe.stdout.trim();884 const executable = probe.stdout.trim();
756 if (!executable || !(yield* fs.exists(executable))) {885 if (!executable || !(yield* fs.exists(executable)) || !(yield* isPython3(executable))) {
757 return undefined;886 return undefined;
758 }887 }
759 888
@@ -1468,6 +1597,164 @@ const runCommand = Effect.fn("runCommand")(function* ( · 708 unmodified lines
1468 }1597 }
1469});1598});
1470 1599
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
1471/**1758/**
1472 * Every `node_modules` directory that would be visible from `startDir`.1759 * Every `node_modules` directory that would be visible from `startDir`.
1473 *1760 *
@@ -1714,22 +2001,22 @@ const verifyPackagedBundleIsSelfContained = Effect.fn("verifyPackagedBundleIsSel · 240 unmodified lines
1714 // stdin, a port, a lock) would otherwise hang release CI until the job2001 // stdin, a port, a lock) would otherwise hang release CI until the job
1715 // times out with nothing useful in the log.2002 // times out with nothing useful in the log.
1716 Effect.timeout(BUNDLE_SELF_CHECK_TIMEOUT),2003 Effect.timeout(BUNDLE_SELF_CHECK_TIMEOUT),
1717 Effect.catchTag("TimeoutError", () =>2004 Effect.catchTags({
1718 Effect.fail(2005 TimeoutError: () =>
1719 new BundleNotSelfContainedError({2006 Effect.fail(
1720 exitCode: -1,2007 new BundleNotSelfContainedError({
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.`,2008 exitCode: -1,
1722 }),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.`,
1723 ),2010 }),
1724 ),2011 ),
1725 Effect.catchTag("BuildCommandFailedError", (error) =>2012 BuildCommandFailedError: (error) =>
1726 Effect.fail(2013 Effect.fail(
1727 new BundleNotSelfContainedError({2014 new BundleNotSelfContainedError({
1728 exitCode: error.exitCode,2015 exitCode: error.exitCode,
1729 output: `${error.stderrTail ?? ""}${error.stdoutTail ?? ""}`.trim(),2016 output: `${error.stderrTail ?? ""}${error.stdoutTail ?? ""}`.trim(),
1730 }),2017 }),
1731 ),2018 ),
1732 ),2019 }),
1733 );2020 );
1734 },2021 },
1735);2022);
@@ -2899,6 +3186,21 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( · 1163 unmodified lines
2899 const path = yield* Path.Path;3186 const path = yield* Path.Path;
2900 const fs = yield* FileSystem.FileSystem;3187 const fs = yield* FileSystem.FileSystem;
2901 const hostPlatform = yield* HostProcessPlatform;3188 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 }
2902 const workspaceConfig = yield* readWorkspaceConfig();3204 const workspaceConfig = yield* readWorkspaceConfig();
2903 const workspaceCatalog = workspaceConfig.catalog ?? {};3205 const workspaceCatalog = workspaceConfig.catalog ?? {};
2904 const workspaceOverrides = workspaceConfig.overrides ?? {};3206 const workspaceOverrides = workspaceConfig.overrides ?? {};