build(bazel): add experimental RNTester macOS build#3014
Draft
Saadnajmi wants to merge 25 commits into
Draft
Conversation
Establishes an additive, experimental Bazel build for react-native-macos and proves the hardest, most novel piece end to end: driving aspect-build rules_js from the repo's Yarn 4 (Berry) yarn.lock as the single source of truth, with no committed pnpm-lock. What's green (verified): - `bazel test //tools/bazel/berry/example:verify` — a real Berry yarn.lock is translated to a pnpm-lock v9 by a dependency-free converter (tools/bazel/berry/berry_to_pnpm_lock.mjs) via a one-seam patch to rules_js (tools/bazel/patches/aspect_rules_js_berry.patch, applied with single_version_override), then rules_js fetches + links + runs it. - On the real monorepo lock the converter yields 33 importers / 1333 packages with zero dangling references; rules_js fetches all 1333. Scaffolded (tagged manual, WIP — see docs/bazel.md): - metro_bundle macro (tools/bazel/js/metro.bzl) + rn-tester bundle target. - Prebuilt XCFramework import seam (tools/bazel/apple/xcframeworks.bzl) and a macos_application slice reusing rn-tester's AppDelegate/main. Also: MODULE.bazel/.bazelrc/.bazelversion/.bazelignore, a non-blocking CI workflow that runs the green proof, and a design doc (docs/bazel.md). The generated pnpm-lock.yaml and MODULE.bazel.lock are gitignored; the only published-adjacent change is an inert empty pnpm.onlyBuiltDependencies in the private monorepo root package.json. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…olding
Extends the Bazel slice toward an end-to-end RNTester macOS build.
First-party workspace linking (rules_js):
- Every workspace package now exposes a `:pkg` npm_package target via a generated
BUILD.bazel, so rules_js can link first-party packages (react-native-macos etc.)
from the Berry yarn.lock. `bazel build //packages/react-native:pkg` builds; rn-tester's
node_modules links react-native-macos (consumed as `react-native`).
rn-tester slice:
- packages/rn-tester/BUILD.bazel: npm_link_all_packages + a Metro bundle target
(js_run_binary around bazel/bundle.js, which drives Metro's API with the
react-native -> react-native-macos alias) + the macos_application wiring.
- packages/rn-tester/bazel/bundle.js: sandbox-safe Metro bundling entry.
Verified / documented (docs/bazel.md):
- rn-tester's macOS JS bundle builds outside Bazel (~7MB) after building
@react-native/codegen lib — proving the JS is bundleable.
- Two remaining blockers for a running app, documented:
1) the macOS app can't be built here because the Hermes nightly tarball 404s
(no XCFrameworks); the app target is fully scaffolded for when they exist.
2) the hermetic Bazel Metro bundle needs the strict-deps tooling closure declared
(rules_js does not hoist like Yarn).
Targets that require these follow-ups remain tagged `manual`; the green Berry-fork
proof (//tools/bazel/berry/example:verify) is unaffected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…a 404) The earlier note blamed a "Hermes nightly 404". That was caused by forcing HERMES_VERSION=nightly, which bypasses react-native-macos's Hermes version-resolution patches (scripts/ios-prebuild/microsoft-hermes.js). Invoked correctly, ios-prebuild resolves the right Hermes (merge-base commit on main / published artifact on release branches). Building it from source in this environment fails for toolchain reasons: default CMake is 4.x (use cmake@3.26.4), and Xcode 26 / AppleClang 21 trips LLVM's CheckAtomic. The prebuild CI pins Xcode 16.2/16.1, which is what builds these XCFrameworks. Not a fork gap. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On Xcode 26 / AppleClang 21, clang honors the *_DEPLOYMENT_TARGET environment
variables and mis-targets the *native* host hermesc build to visionOS when
XROS_DEPLOYMENT_TARGET is set in the environment (XROS takes precedence even if
MACOSX_DEPLOYMENT_TARGET is also set). ios-prebuild sets XROS_DEPLOYMENT_TARGET for
the cross-platform target builds and it leaks into build_host_hermesc, producing:
cc: warning: using sysroot for 'MacOSX' but targeting 'XR' [-Wincompatible-sysroot]
error: 'pthread_setname_np' is unavailable: not available on visionOS
which fails llvh's CheckAtomic ("Host compiler appears to require libatomic, but
cannot find it") and aborts the from-source Hermes build.
Build the native host tools in a subshell that unsets the mobile
IOS/XROS/TVOS_DEPLOYMENT_TARGET vars and sets MACOSX_DEPLOYMENT_TARGET, so clang
targets macOS. Verified: HAVE_CXX_ATOMICS_WITHOUT_LIB now succeeds and hermesc
compiles under Xcode 26.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ORMS) Adds an opt-in HERMES_APPLE_PLATFORMS env var to build only selected Apple slices into hermes.xcframework (default: all, unchanged). Enables macOS-only from-source Hermes builds, which is useful on Xcode 26+ (the upstream Hermes Mac Catalyst target triple is rejected by the newer clang) and greatly speeds up macOS-only iteration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
With the Hermes host-build fix + HERMES_APPLE_PLATFORMS subset + CMake 3.x, verified on Xcode 26.4 that ios-prebuild produces hermes.xcframework (from source), ReactNativeDependencies.xcframework (downloaded), and React.xcframework (BUILD SUCCEEDED). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… 26) Adds tools/bazel/apple/prebuilt_xcframeworks.bzl: a repository rule + module extension that symlinks the SPM-prebuilt React/ReactNativeDependencies/hermes XCFrameworks (from packages/react-native/.build and third-party/) into a Bazel repo and exposes apple_*_xcframework_import targets. Verified on Xcode 26.4: bazel build @rn_prebuilt_xcframeworks//:React //:hermes //:ReactNativeDependencies builds. Wires the rn-tester macOS app host deps to these targets. Note: rn-tester's real AppDelegate needs rn-tester-specific native modules + codegen not present in the XCFrameworks, so a green app needs those built or a minimal RN host. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…undle progress
Converter correctness fix (tools/bazel/berry/berry_to_pnpm_lock.mjs):
- Apply Yarn `resolutions` when resolving dependency descriptors. Berry rewrites matching
descriptors (e.g. resolutions:{debug:">=3.1.0"} makes all `debug` resolve to
debug@npm:>=3.1.0) while dependency entries keep their original ranges (debug: "npm:4").
Previously these edges were silently dropped (e.g. https-proxy-agent lost its `debug`
dep), producing an incomplete node_modules graph.
- Trim dependency ranges so a descriptor like "npm:^3.1.0 " (Yarn keeps insignificant
trailing whitespace) matches the trimmed descriptor key.
- Add a single-version fallback and report any dropped edges. Result: 0 dropped edges on
the monorepo lock (previously silently dropping several).
rn-tester JS bundle (Phase A, still WIP/manual):
- Declare the bundler tooling closure on rn-tester (metro, @react-native/metro-config,
@react-native/metro-babel-transformer, react) so rules_js's strict node_modules resolves
it; regenerates yarn.lock.
- bazel/bundle.js: resolve project root/entry/out via env, disable watchman + enable
symlinks for the sandbox. Metro now loads and resolves the full first-party + third-party
graph; remaining blockers are documented in docs/bazel.md (first-party packages must be
consumed in built/dist form, and a Metro file-map vs Bazel file-layout issue).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…dist, Metro file-map) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ers)
Phase A (JS bundle): rules_js drives Metro to emit a full 1.9MB
RNTesterApp.macos.jsbundle. First-party workspace packages are built in
dist form (first_party.bzl) so their src entry points don't escape the
copied node_modules; copy_tree.js stages a symlink-free project dir to
satisfy Metro's Node file-map crawler. Custom Metro resolver handles the
react-native -> react-native-macos alias (incl. subpaths) and directory
index resolution. Fixed the final output-path mismatch: pass bundleOut
(verbatim) instead of out, which Metro rewrites to force a .js suffix.
Phase B (codegen): tools/bazel/react_native builds @react-native/codegen
hermetically (codegen_lib) and generates AppSpecs + RCTAppDependencyProvider
for rn-tester's Native{Component,Module,CxxModule}Example.
All targets are Bazel-only and opt-in; the default yarn/xcodebuild/SPM
flows are untouched.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bazel build //packages/rn-tester:RNTesterMacBazel now produces a launchable macOS RNTester .app entirely with Bazel: the Metro JS bundle, a native RCTReactNativeFactory host, and the prebuilt React/hermes/ReactNativeDependencies XCFrameworks linked + the bundle embedded as an app resource. The SPM prebuild flattens React.xcframework's headers into per-module dirs (Headers/<Module>/x.h) and ships only a partial set, so the framework isn't directly consumable by clang. Two pieces bridge it: * tools/bazel/apple/reconstruct_react_headers.py — a repo-rule script that scans the framework headers' #include directives and rebuilds a canonical -I symlink tree for the Obj-C <React/...> / <RCTReactNativeFactory.h> surface (basename collisions disambiguated by path-segment match). Exposed as :React_headers with the RN RCT_* defines. * //packages/react-native:rn_cxx_headers — the complete, canonically-nested C++ headers (<react/renderer/...>, <ReactCommon/...>, <jsi/...>, <yoga/...>) sourced from the RN tree (same main version as the binary) via the podspec-equivalent include roots, including the react-native-macos view platform/macos override. Third-party <folly/...> etc. come from ReactNativeDependencies' canonical Headers. RN's new-arch C++ needs C++20 (-std=c++20 in .bazelrc). The minimal host omits rn-tester's own native example modules so it links against only the prebuilt binaries; wiring those + the Phase B codegen is the next increment toward the full AppDelegate. App/native targets stay tagged manual so bazel build //... stays green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tive modules) Adds :RNTesterMacBazelFull, linking rn-tester's real RNTester/AppDelegate.mm (via bazel/real_main.m) instead of the minimal host: * Phase B codegen is now compiled: rn_codegen emits explicit file targets and wraps them in cc_library header targets with virtual prefixes so the app can #import <ReactCodegen/...>, <ReactAppDependencyProvider/...> and <react/renderer/components/AppSpecs/...>; rn_tester_appspecs_lib compiles the AppSpecs + providers (RCTAppDependencyProvider is linked into the app). * NativeCxxModuleExample (C++ TurboModule) compiled + exposed as <NativeCxxModuleExample/...>. * Sample TurboModules (SampleTurboCxxModule / RCTSampleTurboModule) built from the RN source samples; +samples include roots on rn_cxx_headers. * RCTLinking + RCTPushNotification built from source (not in the prebuilt React.xcframework) and exposed as <React/...>; UserNotifications.framework linked. * reconstruct_react_headers.py: map the <FBReactNativeSpec/FBReactNativeSpec.h> core-codegen umbrella (referenced by app sources, not framework headers). RN_DISABLE_OSS_PLUGIN_HEADER still skips the Fabric NativeComponentExample (its sources use quoted plugin includes not yet wired). All targets stay tagged manual. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mple Builds NativeComponentExample (RNTMyNativeView Fabric component + legacy/interop views) and links it into :RNTesterMacBazelFull, dropping RN_DISABLE_OSS_PLUGIN_HEADER. The real, unmodified rn-tester AppDelegate.mm now compiles + links with ALL its example modules (codegen providers, C++ TurboModule, Fabric component, sample TurboModules) — no rn-tester source is patched or #ifdef'd out. A small header shim exposes the core <RCTFabricComponentsPlugins.h> (quoted by the component sources; impl lives in React.xcframework). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Applies the JS-ecosystem ergonomic lesson (derive build metadata from package.json rather than hand-authoring per package) to our Bazel setup: * tools/bazel/js/workspace_package.bzl - rn_workspace_package() macro that collapses the repeated npm_link_all_packages()+npm_package boilerplate. * tools/bazel/js/gen_package_builds.mjs - reads the root package.json 'workspaces' globs and (re)writes the boilerplate BUILD files. It only owns generator-owned files (its @generated marker, or pure legacy :pkg boilerplate) and skips anything declaring other targets (react-native, rn-tester, the first_party dist packages, etc.), mirroring Gazelle's '# gazelle:' override model. Supports --all (create missing) and --check (CI staleness guard). Regenerated 23 boilerplate files from 24 -> 6 lines each (net -414 lines). Verified: the generated :pkg targets build and the rn-tester JS bundle still bundles. This is the incremental step toward full gazelle-based generation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…xes app launch) The SPM prebuild ships React.framework and ReactNativeDependencies.framework as dynamic frameworks (dylibs), but they were imported via apple_static_xcframework_import, so rules_apple linked them with @rpath load commands without embedding them. The app crashed at launch in dyld: 'Library not loaded: @rpath/React.framework/Versions/A/React'. Importing all three (React, ReactNativeDependencies, hermes) as dynamic embeds them in Contents/Frameworks. Verified: RNTesterMacBazel.app now launches and stays running (loads the embedded Metro bundle + Hermes offline). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…JS inner loop Captures the lessons from the pow.rs 'Bazel is incompatible with JavaScript' critique: keep the JS dev loop (yarn/Metro/Jest/debugging) off Bazel, consume artifacts at seams rather than re-Bazelifying node_modules, stay a two-way door (additive/manual), and mind the single-server-per-output_base serialization. Grounds the node_modules friction in our own copy_tree.js/first_party.bzl workarounds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- satisfy Flow annotation and import-order lint rules for Bazel JS tools - ignore Bazel output symlinks so eslint remains usable after a Bazel build - verify generated workspace BUILD files in CI and broaden path triggers - preserve hand-owned targets in generated BUILD files - regenerate the translated pnpm lock whenever yarn.lock changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add //packages/rn-tester/RNTester-macOS:app as the stable entry point. Bundle the real native-example JavaScript through package-local js_library targets instead of replacing it with stubs. Resolve RNTester source-relative react-native imports against the Bazel package alias, emit the conventional main.jsbundle expected by RCTBundleURLProvider, and keep the minimal host on the same bundle name. Verified the exact target builds on Xcode 26.4, embeds one 1.9 MB main.jsbundle containing the Fabric/Screenshot/C++ TurboModule implementations, and launches without Metro. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use swift package dump-package as the canonical graph resolver and emit deterministic Starlark metadata for all 56 SwiftPM targets. A single rn_spm_native_graph() call creates prefixed objc_library targets in packages/react-native, preserving source/exclude sets, dependencies, C++ flags, defines, header search paths, and macOS frameworks without BUILD files throughout the source tree. Bridge the SwiftPM binary dependency to both ReactNativeDependencies.framework and its canonical header tree. Verified generated Yoga, React-debug, React-jsi, and React-utils targets compile from source on Xcode 26.4. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a source-derived canonical header repository, scope C++20 to C++/ObjC++ files, preserve Hermes/RNDependencies header seams, and generate/compile FBReactNativeSpec inside Bazel. The Package.swift-derived graph now compiles through spm_React_RCTAppDelegate on Xcode 26.4. Add a default-off --//:rn_from_source flag and consistent source/prebuilt header selections for RNTester, codegen, and native examples. App-level source mode remains experimental and is intentionally not made the default in this commit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Model Package.swift's React product as the aggregate of all generated regular targets, add the missing DevTools runtime settings targets, and preserve required macOS frameworks. The default-off rn_from_source mode now builds and launches RNTester without embedding React.framework while retaining prebuilt Hermes and ReactNativeDependencies as bootstrap inputs. Complete application resources as part of the same end-to-end milestone: save Metro assets returned by runBuild, compile the macOS asset catalog and storyboard, and embed app icons, privacy manifest, and entitlements. Verified both source and prebuilt modes build; source mode launches offline with the real native-example bundle and tab icons. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Default rn_from_source to true and make native bootstrap artifacts available from a clean checkout. ReactNativeDependencies 0.86.0 and the ABI-exact RN 0.81.0 Hermes artifact are downloaded from Maven with pinned SHA-256 values; downloaded Hermes uses the archive's standalone macOS framework. Keep missing prebuilt React isolated behind the false canary branch instead of invalidating every native import. Also fix the case-sensitive Package.swift TypeSafety path surfaced by the fresh-artifact test. Verified the exact command with local React, RNDependencies, and Hermes artifacts hidden: bazel build //packages/rn-tester/RNTester-macOS:app The cold build completed and the resulting source-linked app launched with all resources. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Run the exact canonical build on macos-26 with no local native artifacts. Verify the resulting app is source-linked (no React.framework), signed, and contains pinned Hermes/RNDependencies, the JS bundle, asset catalog, privacy manifest, and all six navbar icons. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
One-command result
From a clean checkout on Xcode 26:
Bazel downloads SHA-pinned Hermes/RNDependencies artifacts, translates the Yarn Berry lock for rules_js, bundles the real RNTester JavaScript + assets, runs app/core codegen, compiles React from source, links/signs the macOS app, and emits
RNTesterMacBazelFull.zip.Verified locally with all local React/RNDependencies/Hermes artifacts hidden: the cold build completed, the app contained no
React.framework(source-linked), and launched offline with the real Fabric/C++ TurboModule examples and all resources.Architecture
yarn.lockremains the package source of truth.:pkgtargets (gen_package_builds.mjs).swift package dump-packageresolvesPackage.swift;generate_spm_targets.jswrites deterministic Starlark metadata for 58 targets; onern_spm_native_graph()call creates the Bazel libraries without BUILD files throughout React source.FBReactNativeSpec; the aggregatespm_Reactmirrors SwiftPM's all-target React product.f6533c…b78f2)45ae8f…22fbe), whose.hermesversionnames the exact fork merge-base Hermes commite0fc671…; the standalone macOShermes.frameworkis imported.--//:rn_from_source=false.RNTester fidelity
AppDelegate.mm.main.jsbundlefor offline fallback.Assets.car,AppIcon.icns, entitlements, andPrivacyInfo.xcprivacy.Xcode 26 work
hermesc.HERMES_APPLE_PLATFORMSfor macOS-only source builds.CI
.github/workflows/bazel.ymlnow has:macos-26clean-checkout RNTester build running the exact command above.See
docs/bazel.mdfor design details, limitations, cache strategy, and the remaining from-source Hermes/RNDependencies roadmap.