| ... | ... | @@ -287,6 +287,125 @@ export class BuildCommandFailedError extends Schema.TaggedErrorClass<BuildComman |
| 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* ( |
| 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* ( |
| 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* ( |
| 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* ( |
| 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 |
| 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* ( |
| 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 ?? {}; |