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 @@
11 # Contributing
22
3+## Developer Setup
4+
5+See the [maintainer scripts guide](docs/internals/scripts.md#first-checkout) for the initial checkout,
6+development commands, tests, and platform-specific desktop packaging prerequisites.
7+
38 ## Read This First
49
510 We are not actively accepting contributions right now.
docs/internals/scripts.md+73−0
@@ -74,6 +74,79 @@ authenticated. · 73 unmodified lines
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+
79+Linux AppImage packaging compiles the Rust resource monitor. Install a Rust toolchain, the standard
80+C/C++ build tools, and ImageMagick before running `vp run dist:desktop:linux`.
81+
82+Ubuntu and Debian:
83+
84+```bash
85+sudo apt-get update
86+sudo apt-get install cargo rustc build-essential imagemagick
87+```
88+
89+Fedora:
90+
91+```bash
92+sudo dnf install rust cargo gcc gcc-c++ make ImageMagick
93+```
94+
95+Arch Linux:
96+
97+```bash
98+sudo pacman -S rust base-devel imagemagick
99+```
100+
101+The artifact script checks these capabilities before starting the web and desktop builds. If
102+anything is unavailable, it reports every failed check together and prints the Ubuntu/Debian
103+installation command. The check compiles and links tiny temporary programs, so it also catches
104+installed runtime X11 libraries that are missing their development headers or linker symlinks.
105+
106+### macOS DMG prerequisites
107+
108+Install the Xcode Command Line Tools and Rust before building a DMG:
109+
110+```bash
111+xcode-select --install
112+```
113+
114+Install 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
116+has every requested target; add missing targets with:
117+
118+```bash
119+rustup target add aarch64-apple-darwin x86_64-apple-darwin
120+```
121+
122+Unsigned local builds need no Apple credentials. Builds using `--signed` additionally require the
123+certificate, provisioning profile, team ID, and notarization configuration described below.
124+
125+### Windows installer prerequisites
126+
127+Install Rust from [rustup.rs](https://rustup.rs), Python 3, and Visual Studio Build Tools. In the
128+Visual 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+
134+ARM64 installers require the corresponding MSVC ARM64 build tools and Spectre-mitigated libraries
135+instead of the x64/x86 components.
136+
137+Add the Rust target matching the installer architecture:
138+
139+```powershell
140+rustup target add x86_64-pc-windows-msvc
141+# For an arm64 installer:
142+rustup target add aarch64-pc-windows-msvc
143+```
144+
145+Windows supplies `tar.exe`; it is checked when `--wsl-prebuild` makes the artifact include the WSL
146+runtime. NSIS is downloaded by electron-builder and does not need a separate installation.
147+Unsigned local builds need no Azure credentials. Builds using `--signed` additionally require the
148+Azure 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 { · 31 unmodified lines
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 { · 22 unmodified lines
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 · 33 unmodified lines
102108 );
103109 });
104110
105−function mockProcess(exitCode: number) {
111+function 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) { · 1 unmodified line
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) => { · 695 unmodified lines
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 · 286 unmodified lines
287287 }
288288 }
289289
290+export 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+
298+export 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+
336+const 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+
345+export 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+
372+const 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+
382+export 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+
290409 export class ResourceMonitorBuildOutputMissingError extends Schema.TaggedErrorClass<ResourceMonitorBuildOutputMissingError>()(
291410 "ResourceMonitorBuildOutputMissingError",
292411 {
@@ -721,8 +840,18 @@ const resolvePythonForNodeGyp = Effect.fn("resolvePythonForNodeGyp")(function* ( · 428 unmodified lines
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* ( · 2 unmodified lines
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* ( · 15 unmodified lines
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* ( · 708 unmodified lines
14681597 }
14691598 });
14701599
1600+const 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+
1610+const 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+
1623+export 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+
1657+export 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+
1696+function 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+
1717+export 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 · 240 unmodified lines
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* ( · 1163 unmodified lines
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 ?? {};