| @@ -287,6 +287,125 @@ export class BuildCommandFailedError extends Schema.TaggedErrorClass<BuildComman · 286 unmodified lines |
| 287 | 287 | } |
| 288 | 288 | } |
| 289 | 289 | |
| 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 | + |
| 290 | 409 | export class ResourceMonitorBuildOutputMissingError extends Schema.TaggedErrorClass<ResourceMonitorBuildOutputMissingError>()( |
| 291 | 410 | "ResourceMonitorBuildOutputMissingError", |
| 292 | 411 | { |
| @@ -721,8 +840,18 @@ const resolvePythonForNodeGyp = Effect.fn("resolvePythonForNodeGyp")(function* ( · 428 unmodified lines |
| 721 | 840 | ), |
| 722 | 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 | 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 | 855 | return configured; |
| 727 | 856 | } |
| 728 | 857 | |
| @@ -731,7 +860,7 @@ const resolvePythonForNodeGyp = Effect.fn("resolvePythonForNodeGyp")(function* ( · 2 unmodified lines |
| 731 | 860 | if (localAppData) { |
| 732 | 861 | for (const version of ["Python313", "Python312", "Python311", "Python310"]) { |
| 733 | 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 | 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 | 884 | const executable = probe.stdout.trim(); |
| 756 | | − if (!executable || !(yield* fs.exists(executable))) { |
| 885 | + if (!executable || !(yield* fs.exists(executable)) || !(yield* isPython3(executable))) { |
| 757 | 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 | |
| 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 | + |
| 1471 | 1758 | /** |
| 1472 | 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 | 2001 | // stdin, a port, a lock) would otherwise hang release CI until the job |
| 1715 | 2002 | // times out with nothing useful in the log. |
| 1716 | 2003 | 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 | + }), |
| 1733 | 2020 | ); |
| 1734 | 2021 | }, |
| 1735 | 2022 | ); |
| @@ -2899,6 +3186,21 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( · 1163 unmodified lines |
| 2899 | 3186 | const path = yield* Path.Path; |
| 2900 | 3187 | const fs = yield* FileSystem.FileSystem; |
| 2901 | 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 | 3204 | const workspaceConfig = yield* readWorkspaceConfig(); |
| 2903 | 3205 | const workspaceCatalog = workspaceConfig.catalog ?? {}; |
| 2904 | 3206 | const workspaceOverrides = workspaceConfig.overrides ?? {}; |