From 2dc1f05d11c2315dbc18070318089278354116fe Mon Sep 17 00:00:00 2001 From: Dawid Malecki Date: Fri, 11 Sep 2026 03:46:19 -0700 Subject: [PATCH] Fix builds to not probe Maven for `1000.0.0` versioned artifacts (#58425) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Fixes issue with probing Maven Central repo for artifacts with version `1000.0.0` which are not stored there. The reason is that it creates most of the traffic on Maven Mirror that is forwarded to Maven Central due to artifacts not being cached because they are not found. After this change: - Gradle retains its existing dependency substitutions and forced versions. - Local file-based Maven repositories may still provide 1000.0.0 artifacts. - CocoaPods continues falling back to building React Native from source. - SwiftPM retains its existing 1000.0.0 → nightly resolution behavior. - Published React Native and Hermes versions continue to be queried normally. ## Changelog: [GENERAL][FIXED] - Changed builds to not probe Maven for `1000.0.0` versioned artifacts. Test Plan: Ran `pod install` and build the HelloWorld workspace: ``` [MavenProbe] SKIP https://repo.reactnative.dev/maven2/.../1000.0.0/...reactnative-dependencies-debug.tar.gz [MavenProbe] SKIP https://repo1.maven.org/maven2/.../1000.0.0/...reactnative-dependencies-debug.tar.gz [ReactNativeDependencies] No prebuilt artifacts found, reverting to building from source. [MavenProbe] SKIP https://repo.reactnative.dev/maven2/.../1000.0.0/...reactnative-core-debug.tar.gz [MavenProbe] SKIP https://repo1.maven.org/maven2/.../1000.0.0/...reactnative-core-debug.tar.gz [ReactNativeCore] No prebuilt artifacts found, reverting to building from source. Pod installation complete! There are 85 dependencies from the Podfile and 84 total pods installed. ``` Ran SwiftPM setup on a disposable copy of HelloWorld and built the generated Xcode project. ``` [setup-apple-spm] React Native version: 1000.0.0 [download-spm-artifacts] Detected dev version (1000.0.0), resolving as nightly... [download-spm-artifacts] Resolved nightly: 0.89.0-nightly-20260909-b830082cf [MavenProbe] HEAD https://repo.reactnative.dev/maven2/.../0.89.0-nightly-20260909-b830082cf/...reactnative-core-debug.tar.gz [MavenProbe] HEAD https://repo.reactnative.dev/maven2/.../0.89.0-nightly-20260909-b830082cf/...reactnative-dependencies-debug.tar.gz ``` Published `react-android:1000.0.0` to `/tmp/maven-local`, then ran the repository’s real HelloWorld Android build and a verbose Gradle rebuild. ``` [MavenProbe] SKIP remote Maven https://repo.reactnative.dev/maven2 for React Native 1000.0.0 [MavenProbe] SKIP remote Maven https://repo.maven.apache.org/maven2/ for React Native 1000.0.0 ``` During the HelloWorld build: ``` [MavenProbe] SKIP remote Maven https://repo.maven.apache.org/maven2/ for React Native 1000.0.0 [MavenProbe] SKIP remote Maven https://dl.google.com/dl/android/maven2/ for React Native 1000.0.0 [MavenProbe] SKIP remote Maven https://www.jitpack.io for React Native 1000.0.0 BUILD SUCCESSFUL in 5s ``` Reviewed By: cipolleschi Differential Revision: D119646843 Pulled By: coado --- .../kotlin/com/facebook/react/ReactPlugin.kt | 2 +- .../facebook/react/utils/DependencyUtils.kt | 38 ++++++++++ .../react/utils/DependencyUtilsTest.kt | 73 +++++++++++++++++++ .../__tests__/maven_mirror_flag-test.rb | 29 ++++++++ .../react-native/scripts/cocoapods/rncore.rb | 4 + .../scripts/cocoapods/rndependencies.rb | 4 + .../react-native/scripts/cocoapods/utils.rb | 9 +++ .../ios-prebuild/__tests__/utils-test.js | 52 +++++++++++++ .../scripts/ios-prebuild/hermes.js | 15 +++- .../ios-prebuild/reactNativeDependencies.js | 10 +++ .../scripts/ios-prebuild/utils.js | 13 ++++ .../__tests__/download-spm-artifacts-test.js | 36 +++++++++ .../scripts/spm/download-spm-artifacts.js | 24 ++++++ .../sdks/hermes-engine/hermes-utils.rb | 4 + 14 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 packages/react-native/scripts/ios-prebuild/__tests__/utils-test.js diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt index 890edb58bd86..f3b5072dce93 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt @@ -95,7 +95,7 @@ class ReactPlugin : Plugin { val versionAndGroupStrings = readVersionAndGroupStrings(project, propertiesFile, hermesVersionPropertiesFile) configureDependencies(project, versionAndGroupStrings) - configureRepositories(project, versionAndGroupStrings.isNightly) + configureRepositories(project, versionAndGroupStrings) } configureReactNativeNdk(project, extension) diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/DependencyUtils.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/DependencyUtils.kt index 4b4ffd0c1dd2..eff5ca58f566 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/DependencyUtils.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/DependencyUtils.kt @@ -31,6 +31,7 @@ import org.gradle.api.artifacts.repositories.MavenArtifactRepository internal object DependencyUtils { private const val REACT_NATIVE_MAVEN_MIRROR_URL = "https://repo.reactnative.dev/maven2" private const val REACT_NATIVE_MAVEN_MIRROR_ENABLED_ENV = "RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED" + private const val UNPUBLISHED_MAVEN_VERSION = "1000.0.0" internal data class Coordinates( val versionString: String, @@ -124,6 +125,41 @@ internal object DependencyUtils { } } + /** + * Configures repositories without asking remote repositories for versions that are known to be + * unpublished. + */ + fun configureRepositories(project: Project, coordinates: Coordinates) { + configureRepositories(project, coordinates.isNightly) + + project.rootProject.allprojects { eachProject -> + eachProject.repositories.withType(MavenArtifactRepository::class.java).configureEach { repo -> + if (repo.url.scheme != "file") { + repo.content { content -> + if (!coordinates.versionString.isMavenArtifactVersionPublished()) { + setOf(DEFAULT_INTERNAL_REACT_PUBLISHING_GROUP, coordinates.reactGroupString) + .forEach { group -> + content.excludeVersion(group, "react-native", UNPUBLISHED_MAVEN_VERSION) + content.excludeVersion(group, "react-android", UNPUBLISHED_MAVEN_VERSION) + } + } + if (!coordinates.hermesVersionString.isMavenArtifactVersionPublished()) { + setOf( + DEFAULT_INTERNAL_REACT_PUBLISHING_GROUP, + DEFAULT_INTERNAL_HERMES_PUBLISHING_GROUP, + coordinates.hermesGroupString, + ) + .forEach { group -> + content.excludeVersion(group, "hermes-engine", UNPUBLISHED_MAVEN_VERSION) + content.excludeVersion(group, "hermes-android", UNPUBLISHED_MAVEN_VERSION) + } + } + } + } + } + } + } + /** * This method takes care of configuring the resolution strategy for both the app and all the 3rd * party libraries which are auto-linked. Specifically it takes care of: @@ -303,6 +339,8 @@ internal object DependencyUtils { internal fun String.isNightly(): Boolean = this.startsWith("0.0.0") || "-nightly-" in this + internal fun String.isMavenArtifactVersionPublished(): Boolean = this != UNPUBLISHED_MAVEN_VERSION + internal fun Project.exclusiveEnterpriseRepository() = when { hasProperty(SCOPED_EXCLUSIVE_ENTEPRISE_REPOSITORY) -> diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/DependencyUtilsTest.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/DependencyUtilsTest.kt index fed57c2025c3..620aa4079f71 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/DependencyUtilsTest.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/DependencyUtilsTest.kt @@ -12,13 +12,18 @@ import com.facebook.react.utils.DependencyUtils.configureDependencies import com.facebook.react.utils.DependencyUtils.configureRepositories import com.facebook.react.utils.DependencyUtils.exclusiveEnterpriseRepository import com.facebook.react.utils.DependencyUtils.getDependencySubstitutions +import com.facebook.react.utils.DependencyUtils.isMavenArtifactVersionPublished import com.facebook.react.utils.DependencyUtils.isNightly import com.facebook.react.utils.DependencyUtils.isReactNativeMavenMirrorEnabled import com.facebook.react.utils.DependencyUtils.mavenRepoFromURI import com.facebook.react.utils.DependencyUtils.mavenRepoFromUrl import com.facebook.react.utils.DependencyUtils.readVersionAndGroupStrings import com.facebook.react.utils.DependencyUtils.shouldAddJitPack +import com.sun.net.httpserver.HttpServer +import java.net.InetAddress +import java.net.InetSocketAddress import java.net.URI +import java.util.concurrent.atomic.AtomicInteger import org.assertj.core.api.Assertions.assertThat import org.gradle.api.artifacts.repositories.MavenArtifactRepository import org.gradle.testfixtures.ProjectBuilder @@ -78,6 +83,42 @@ class DependencyUtilsTest { .isNotNull() } + @Test + fun configureRepositories_withUnpublishedVersion_doesNotQueryRemoteRepository() { + val requests = AtomicInteger() + val loopbackAddress = InetAddress.getLoopbackAddress() + val server = HttpServer.create(InetSocketAddress(loopbackAddress, 0), 0) + server.createContext("/") { exchange -> + requests.incrementAndGet() + exchange.sendResponseHeaders(404, -1) + exchange.close() + } + server.start() + + try { + val project = createProject() + project.extensions.extraProperties.set( + "exclusiveEnterpriseRepository", + "http://${loopbackAddress.hostAddress}:${server.address.port}", + ) + configureRepositories(project, DependencyUtils.Coordinates("1000.0.0", "4.5.6")) + (project.repositories.first() as MavenArtifactRepository).isAllowInsecureProtocol = true + + val published = project.configurations.create("published") + project.dependencies.add(published.name, "com.facebook.react:react-android:0.88.0") + assertThat(runCatching { published.resolve() }.isFailure).isTrue() + assertThat(requests.get()).isGreaterThan(0) + + requests.set(0) + val unpublished = project.configurations.create("unpublished") + project.dependencies.add(unpublished.name, "com.facebook.react:react-android:1000.0.0") + assertThat(runCatching { unpublished.resolve() }.isFailure).isTrue() + assertThat(requests.get()).isZero() + } finally { + server.stop(0) + } + } + @Test fun configureRepositories_containsGoogleRepo() { val repositoryURI = URI.create("https://dl.google.com/dl/android/maven2/") @@ -414,6 +455,32 @@ class DependencyUtilsTest { assertThat(project.configurations.first().resolutionStrategy.forcedModules.isEmpty()).isTrue() } + @Test + fun configureDependencies_withUnpublishedVersion_preservesResolutionStrategy() { + val project = createProject() + + configureDependencies(project, DependencyUtils.Coordinates("1000.0.0", "4.5.6")) + + val forcedModules = project.configurations.first().resolutionStrategy.forcedModules + assertThat( + forcedModules.any { + it.toString() == "com.facebook.react:react-android:1000.0.0" + }, + ) + .isTrue() + assertThat(forcedModules.any { it.toString() == "com.facebook.hermes:hermes-android:4.5.6" }) + .isTrue() + + val dependencySubstitutions = + getDependencySubstitutions(DependencyUtils.Coordinates("1000.0.0", "4.5.6")) + assertThat( + dependencySubstitutions.any { + it.second == "com.facebook.react:react-android:1000.0.0" + }, + ) + .isTrue() + } + @Test fun configureDependencies_withVersionString_appliesResolutionStrategy() { val project = createProject() @@ -577,6 +644,12 @@ class DependencyUtilsTest { assertThat(hermesVersionString).isEqualTo("1000.0.0") } + @Test + fun isMavenArtifactVersionPublished_withMainVersion_returnsFalse() { + assertThat("1000.0.0".isMavenArtifactVersionPublished()).isFalse() + assertThat("0.88.0".isMavenArtifactVersionPublished()).isTrue() + } + @Test fun readVersionString_withNightlyVersionString_returnsSnapshotVersion() { val propertiesFile = diff --git a/packages/react-native/scripts/cocoapods/__tests__/maven_mirror_flag-test.rb b/packages/react-native/scripts/cocoapods/__tests__/maven_mirror_flag-test.rb index 9f75f25fd7ae..c5ed599ee714 100644 --- a/packages/react-native/scripts/cocoapods/__tests__/maven_mirror_flag-test.rb +++ b/packages/react-native/scripts/cocoapods/__tests__/maven_mirror_flag-test.rb @@ -5,6 +5,8 @@ require "test/unit" require_relative "../utils.rb" +require_relative "../rncore.rb" +require_relative "../rndependencies.rb" require_relative "../../../sdks/hermes-engine/hermes-utils.rb" class MavenMirrorFlagTests < Test::Unit::TestCase @@ -39,4 +41,31 @@ def test_mavenMirror_isDisabledWhenExplicitlySetToFalse assert_false(ReactNativePodsUtils.react_native_maven_mirror_enabled?) assert_false(react_native_maven_mirror_enabled?) end + + def test_unpublishedVersion_skipsAllArtifactLookups + assert_false(ReactNativePodsUtils.maven_artifact_version_published?('1000.0.0')) + assert_false(ReactNativePodsUtils.artifact_exists?('https://repo.reactnative.dev/maven2/example/1000.0.0/example.tar.gz')) + assert_false(ReactNativePodsUtils.artifact_exists?('https://central.sonatype.com/example/1000.0.0-SNAPSHOT/example.tar.gz')) + assert_false(ReactNativeCoreUtils.release_artifact_exists('1000.0.0')) + assert_false(ReactNativeCoreUtils.nightly_artifact_exists('1000.0.0')) + assert_false(ReactNativeDependenciesUtils.release_artifact_exists('1000.0.0')) + assert_false(ReactNativeDependenciesUtils.nightly_artifact_exists('1000.0.0')) + assert_false(release_artifact_exists('1000.0.0')) + assert_false(hermes_artifact_exists('https://repo.reactnative.dev/maven2/example/1000.0.0/example.tar.gz')) + + assert_equal( + ReactNativeCoreUtils.stable_tarball_urls('1000.0.0', :debug).first, + ReactNativeCoreUtils.stable_tarball_url('1000.0.0', :debug), + ) + assert_equal( + ReactNativeDependenciesUtils.release_tarball_urls('1000.0.0', :debug).first, + ReactNativeDependenciesUtils.release_tarball_url('1000.0.0', :debug), + ) + assert_equal(release_tarball_urls('1000.0.0', :debug).first, release_tarball_url('1000.0.0', :debug)) + end + + def test_releaseVersion_isPublished + assert_true(ReactNativePodsUtils.maven_artifact_version_published?('0.88.0')) + assert_true(ReactNativePodsUtils.maven_artifact_version_published?('1000.0.0-abcdef123')) + end end diff --git a/packages/react-native/scripts/cocoapods/rncore.rb b/packages/react-native/scripts/cocoapods/rncore.rb index e56ce29b8a15..f588ad771f0b 100644 --- a/packages/react-native/scripts/cocoapods/rncore.rb +++ b/packages/react-native/scripts/cocoapods/rncore.rb @@ -367,6 +367,8 @@ def self.stable_tarball_urls(version, build_type, dsyms = false) end def self.nightly_tarball_url(version, configuration, dsyms = false) + return "" if !ReactNativePodsUtils.maven_artifact_version_published?(version) + artefact_coordinate = "react-native-artifacts" artefact_name = "reactnative-core-#{dsyms ? "dSYM-" : ""}#{configuration ? configuration : "debug"}.tar.gz" xml_url = "https://central.sonatype.com/repository/maven-snapshots/com/facebook/react/#{artefact_coordinate}/#{version}-SNAPSHOT/maven-metadata.xml" @@ -469,6 +471,8 @@ def self.release_artifact_exists(version) end def self.nightly_artifact_exists(version) + return false if !ReactNativePodsUtils.maven_artifact_version_published?(version) + return artifact_exists(nightly_tarball_url(version, :debug).gsub("\\", "")) end diff --git a/packages/react-native/scripts/cocoapods/rndependencies.rb b/packages/react-native/scripts/cocoapods/rndependencies.rb index c38755263963..eb4bde3b3cfe 100644 --- a/packages/react-native/scripts/cocoapods/rndependencies.rb +++ b/packages/react-native/scripts/cocoapods/rndependencies.rb @@ -250,6 +250,8 @@ def self.release_tarball_urls(version, build_type) end def self.nightly_tarball_url(version, build_type) + return "" if !ReactNativePodsUtils.maven_artifact_version_published?(version) + artifact_coordinate = "react-native-artifacts" artifact_name = "reactnative-dependencies-#{build_type.to_s}.tar.gz" xml_url = "https://central.sonatype.com/repository/maven-snapshots/com/facebook/react/#{artifact_coordinate}/#{version}-SNAPSHOT/maven-metadata.xml" @@ -377,6 +379,8 @@ def self.release_artifact_exists(version) end def self.nightly_artifact_exists(version) + return false if !ReactNativePodsUtils.maven_artifact_version_published?(version) + return artifact_exists(nightly_tarball_url(version, :debug).gsub("\\", "")) end diff --git a/packages/react-native/scripts/cocoapods/utils.rb b/packages/react-native/scripts/cocoapods/utils.rb index a31cab7442f6..3038897c8545 100644 --- a/packages/react-native/scripts/cocoapods/utils.rb +++ b/packages/react-native/scripts/cocoapods/utils.rb @@ -15,6 +15,7 @@ class ReactNativePodsUtils MAVEN_CENTRAL_REPOSITORY = "https://repo1.maven.org/maven2" REACT_NATIVE_MAVEN_MIRROR_REPOSITORY = "https://repo.reactnative.dev/maven2" + UNPUBLISHED_MAVEN_VERSION = "1000.0.0" # Opt-in removal of the legacy TurboModule and component interop layers. Both are # off by default and will become the default in a future React Native release. @@ -49,6 +50,11 @@ def self.react_native_maven_mirror_enabled?() value.downcase != "false" && value != "0" end + def self.maven_artifact_version_published?(version) + # 1000.0.0 identifies a source checkout on main and is never published to Maven. + return version != UNPUBLISHED_MAVEN_VERSION + end + def self.warn_if_not_on_arm64 if SysctlChecker.new().call_sysctl_arm64() == 1 && !Environment.new().ruby_platform().include?('arm64') Pod::UI.warn 'Do not use "pod install" from inside Rosetta2 (x86_64 emulation on arm64).' @@ -833,6 +839,9 @@ def self.resolve_use_frameworks(spec, header_mappings_dir: nil, module_name: nil # (DNS failure, no route, ...) the probe is left uncached so that a # transient hiccup doesn't permanently mark the artifact as missing. def self.artifact_exists?(tarball_url) + unpublished_version = Regexp.escape(UNPUBLISHED_MAVEN_VERSION) + return false if tarball_url.match?(%r{/#{unpublished_version}(?:-SNAPSHOT)?/}) + unless @@artifact_exists_cache.key?(tarball_url) # -L is used to follow redirects, useful for the nightlies # The url is wrapped in quotes to avoid escaping & and ?. diff --git a/packages/react-native/scripts/ios-prebuild/__tests__/utils-test.js b/packages/react-native/scripts/ios-prebuild/__tests__/utils-test.js new file mode 100644 index 000000000000..7d443bd217b1 --- /dev/null +++ b/packages/react-native/scripts/ios-prebuild/__tests__/utils-test.js @@ -0,0 +1,52 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @noflow + */ + +'use strict'; + +const { + computeNightlyTarballURL, + isMavenArtifactVersionPublished, +} = require('../utils'); + +describe('isMavenArtifactVersionPublished', () => { + it('rejects the unpublished main version', () => { + expect(isMavenArtifactVersionPublished('1000.0.0')).toBe(false); + }); + + it.each([ + '0.88.0', + '0.89.0-nightly-20260909-abcdef123', + '1000.0.0-abcdef123', + ])('accepts published artifact version %s', version => { + expect(isMavenArtifactVersionPublished(version)).toBe(true); + }); +}); + +describe('computeNightlyTarballURL', () => { + it('does not query snapshot metadata for the unpublished main version', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = jest.fn(); + + try { + await expect( + computeNightlyTarballURL( + '1000.0.0', + 'Debug', + 'react', + 'react-native-artifacts', + 'reactnative-dependencies-debug.tar.gz', + ), + ).rejects.toThrow(/artifacts are not published/); + expect(globalThis.fetch).not.toHaveBeenCalled(); + } finally { + globalThis.fetch = originalFetch; + } + }); +}); diff --git a/packages/react-native/scripts/ios-prebuild/hermes.js b/packages/react-native/scripts/ios-prebuild/hermes.js index 7e20ddf4f978..8956337fd3f2 100644 --- a/packages/react-native/scripts/ios-prebuild/hermes.js +++ b/packages/react-native/scripts/ios-prebuild/hermes.js @@ -8,7 +8,11 @@ * @format */ -const {createLogger, getMavenRepositoryUrls} = require('./utils'); +const { + createLogger, + getMavenRepositoryUrls, + isMavenArtifactVersionPublished, +} = require('./utils'); const {execSync} = require('node:child_process'); const fs = require('node:fs'); const path = require('node:path'); @@ -204,6 +208,10 @@ async function findExistingTarballUrl( version /*: string */, buildType /*: BuildFlavor */, ) /*: Promise */ { + if (!isMavenArtifactVersionPublished(version)) { + return null; + } + const candidates = getTarballUrls(version, buildType); for (const url of candidates) { if (await hermesArtifactExists(url)) { @@ -341,6 +349,11 @@ async function downloadHermesTarball( const tmpFile = `${artifactsPath}/hermes-ios.download`; try { fs.mkdirSync(artifactsPath, {recursive: true}); + if (!isMavenArtifactVersionPublished(version)) { + throw new Error( + `Maven artifacts are not published for the development version ${version}`, + ); + } hermesLog(`Downloading Hermes tarball from ${tarballUrl}`); const response /*: Response */ = await fetch(tarballUrl); diff --git a/packages/react-native/scripts/ios-prebuild/reactNativeDependencies.js b/packages/react-native/scripts/ios-prebuild/reactNativeDependencies.js index bcc325ee0ea6..50b279709373 100644 --- a/packages/react-native/scripts/ios-prebuild/reactNativeDependencies.js +++ b/packages/react-native/scripts/ios-prebuild/reactNativeDependencies.js @@ -14,6 +14,7 @@ const { computeNightlyTarballURL, createLogger, getMavenRepositoryUrls, + isMavenArtifactVersionPublished, } = require('./utils'); const {execSync} = require('node:child_process'); const fs = require('node:fs'); @@ -236,6 +237,10 @@ async function findExistingTarballUrl( version /*: string */, buildType /*: BuildFlavor */, ) /*: Promise */ { + if (!isMavenArtifactVersionPublished(version)) { + return null; + } + const candidates = getTarballUrls(version, buildType); for (const url of candidates) { if (await reactNativeDependenciesArtifactExists(url)) { @@ -401,6 +406,11 @@ async function downloadReactNativeDependenciesTarball( const tmpFile = `${artifactsPath}/reactnative-dependencies.download`; try { fs.mkdirSync(artifactsPath, {recursive: true}); + if (!isMavenArtifactVersionPublished(version)) { + throw new Error( + `Maven artifacts are not published for the development version ${version}`, + ); + } dependencyLog( `Downloading ReactNativeDependencies tarball from ${tarballUrl}`, ); diff --git a/packages/react-native/scripts/ios-prebuild/utils.js b/packages/react-native/scripts/ios-prebuild/utils.js index 036b398f750d..13e2aefe2213 100644 --- a/packages/react-native/scripts/ios-prebuild/utils.js +++ b/packages/react-native/scripts/ios-prebuild/utils.js @@ -17,6 +17,7 @@ const path = require('node:path'); const MAVEN_CENTRAL_REPOSITORY = 'https://repo1.maven.org/maven2'; const REACT_NATIVE_MAVEN_MIRROR_REPOSITORY = 'https://repo.reactnative.dev/maven2'; +const UNPUBLISHED_MAVEN_VERSION = '1000.0.0'; /** * Creates a folder if it does not exist @@ -96,6 +97,12 @@ async function computeNightlyTarballURL( artifactCoordinate /*: string */, artifactName /*: string */, ) /*: Promise */ { + if (!isMavenArtifactVersionPublished(version)) { + throw new Error( + `Maven artifacts are not published for the development version ${version}`, + ); + } + const xmlUrl = `https://central.sonatype.com/repository/maven-snapshots/com/facebook/${subGroup}/${artifactCoordinate}/${version}-SNAPSHOT/maven-metadata.xml`; const response = await fetch(xmlUrl); @@ -156,6 +163,11 @@ function isReactNativeMavenMirrorEnabled() /*: boolean */ { return value.toLowerCase() !== 'false' && value !== '0'; } +function isMavenArtifactVersionPublished(version /*: string */) /*: boolean */ { + // 1000.0.0 identifies a source checkout on main and is never published to Maven. + return version !== UNPUBLISHED_MAVEN_VERSION; +} + module.exports = { createFolderIfNotExists, findFirst, @@ -163,4 +175,5 @@ module.exports = { createLogger, computeNightlyTarballURL, getMavenRepositoryUrls, + isMavenArtifactVersionPublished, }; diff --git a/packages/react-native/scripts/spm/__tests__/download-spm-artifacts-test.js b/packages/react-native/scripts/spm/__tests__/download-spm-artifacts-test.js index 3faaf5e8b087..2a24dbff8c5d 100644 --- a/packages/react-native/scripts/spm/__tests__/download-spm-artifacts-test.js +++ b/packages/react-native/scripts/spm/__tests__/download-spm-artifacts-test.js @@ -18,6 +18,7 @@ const { formatBytes, formatSpeed, hermesReleaseUrls, + isMavenArtifactVersionPublished, mavenRepositoryUrls, reactNativeMavenMirrorEnabled, resolveCacheSlotVersion, @@ -377,6 +378,27 @@ describe('mavenRepositoryUrls', () => { // --------------------------------------------------------------------------- describe('release URL builders', () => { + it('preserves Maven URLs for the unpublished main version', () => { + expect(isMavenArtifactVersionPublished('1000.0.0')).toBe(false); + expect(rnCoreReleaseUrls('1000.0.0', 'debug')).toHaveLength(2); + expect(rnDepsReleaseUrls('1000.0.0', 'debug')).toHaveLength(2); + expect(hermesReleaseUrls('1000.0.0', 'debug')).toHaveLength(2); + }); + + it('does not probe Maven for the unpublished main version', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = jest.fn(); + + try { + await expect( + exists(rnCoreReleaseUrls('1000.0.0', 'debug')[0]), + ).resolves.toBe(false); + expect(globalThis.fetch).not.toHaveBeenCalled(); + } finally { + globalThis.fetch = originalFetch; + } + }); + it('rnCoreReleaseUrls builds a candidate per repository for the reactnative-core classifier', () => { const suffix = '/com/facebook/react/react-native-artifacts/0.85.0/' + @@ -558,6 +580,20 @@ describe('resolveSnapshotUrl', () => { ); }); + it('does not query snapshot metadata for the unpublished main version', async () => { + globalThis.fetch = jest.fn(); + + await expect( + resolveSnapshotUrl( + '1000.0.0', + 'react', + 'react-native-artifacts', + 'reactnative-core-debug.tar.gz', + ), + ).rejects.toThrow(/artifacts are not published/); + expect(globalThis.fetch).not.toHaveBeenCalled(); + }); + it('throws when the metadata request fails', async () => { globalThis.fetch = routerFetch({ 'maven-metadata.xml': {ok: false, status: 500}, diff --git a/packages/react-native/scripts/spm/download-spm-artifacts.js b/packages/react-native/scripts/spm/download-spm-artifacts.js index f84dc017b60b..82cb1dfc4ea9 100644 --- a/packages/react-native/scripts/spm/download-spm-artifacts.js +++ b/packages/react-native/scripts/spm/download-spm-artifacts.js @@ -141,9 +141,22 @@ function parseArgs(argv /*: Array */) /*: DownloadArgs */ { const MAVEN_CENTRAL_REPOSITORY = 'https://repo1.maven.org/maven2'; const REACT_NATIVE_MAVEN_MIRROR_REPOSITORY = 'https://repo.reactnative.dev/maven2'; +const UNPUBLISHED_MAVEN_VERSION = '1000.0.0'; const MAVEN_SNAPSHOT = 'https://central.sonatype.com/repository/maven-snapshots'; +function isMavenArtifactVersionPublished(version /*: string */) /*: boolean */ { + // 1000.0.0 identifies a source checkout on main and is never published to Maven. + return version !== UNPUBLISHED_MAVEN_VERSION; +} + +function isMavenArtifactUrlPublished(url /*: string */) /*: boolean */ { + return ( + !url.includes(`/${UNPUBLISHED_MAVEN_VERSION}/`) && + !url.includes(`/${UNPUBLISHED_MAVEN_VERSION}-SNAPSHOT/`) + ); +} + /** * The mirror is ON unless RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED is * explicitly "false"/"0". @@ -219,6 +232,12 @@ async function resolveSnapshotUrl( coordinate /*: string */, artifactName /*: string */, ) /*: Promise */ { + if (!isMavenArtifactVersionPublished(version)) { + throw new Error( + `Maven artifacts are not published for the development version ${version}`, + ); + } + const metadataUrl = `${MAVEN_SNAPSHOT}/com/facebook/${subGroup}/${coordinate}/` + `${version}-SNAPSHOT/maven-metadata.xml`; @@ -353,6 +372,10 @@ async function resolveLatestV1Version() /*: Promise */ { } async function exists(url /*: string */) /*: Promise */ { + if (!isMavenArtifactUrlPublished(url)) { + return false; + } + try { // $FlowFixMe[incompatible-call] global fetch not in Flow stubs const res = await fetch(url, {method: 'HEAD'}); @@ -1510,6 +1533,7 @@ module.exports = { validateArtifactsCache, // Exposed for unit tests (pure / fetch-stubbable helpers). mavenRepositoryUrls, + isMavenArtifactVersionPublished, reactNativeMavenMirrorEnabled, rnCoreReleaseUrls, rnDepsReleaseUrls, diff --git a/packages/react-native/sdks/hermes-engine/hermes-utils.rb b/packages/react-native/sdks/hermes-engine/hermes-utils.rb index 71ef0481bbd4..ee13fc7c9240 100644 --- a/packages/react-native/sdks/hermes-engine/hermes-utils.rb +++ b/packages/react-native/sdks/hermes-engine/hermes-utils.rb @@ -9,6 +9,7 @@ ENV_BUILD_FROM_SOURCE = "RCT_BUILD_HERMES_FROM_SOURCE" MAVEN_CENTRAL_REPOSITORY = "https://repo1.maven.org/maven2" REACT_NATIVE_MAVEN_MIRROR_REPOSITORY = "https://repo.reactnative.dev/maven2" +UNPUBLISHED_HERMES_VERSION = "1000.0.0" # Memoized results of requests to the Maven repositories (mirror or central). # hermes-engine.podspec is evaluated several times during a single @@ -356,6 +357,9 @@ def resolve_url_redirects(url) # Parameters # - tarball_url: the URL of the Hermes artifact to probe def hermes_artifact_exists(tarball_url) + unpublished_version = Regexp.escape(UNPUBLISHED_HERMES_VERSION) + return false if tarball_url.match?(%r{/#{unpublished_version}(?:-SNAPSHOT)?/}) + unless HERMES_ARTIFACT_EXISTS_CACHE.key?(tarball_url) # -L is used to follow redirects, useful for the nightlies # I also needed to wrap the url in quotes to avoid escaping & and ?.