diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2d6d56994..97dcecb5d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,6 +89,34 @@ jobs: - name: Provision third-party headers run: RNET_HEADERS_ONLY=1 node scripts/download-libs.js + # third-party/include and the ExecuTorch built below must come from the + # same release. When they drifted, `make_tensor_ptr` gaining a trailing + # `Device` parameter surfaced as an undefined reference nine minutes into + # the dependency build, blamed on whichever code happened to call it. + # Comparing the two up front fails in seconds and names both values. + # Note this only spans releases: ET_VERSION comes from the source tree's + # version.txt, so it cannot tell the fork's patches apart from upstream, + # and it says nothing about TOKENIZERS_COMMIT. + - name: Check the header and test-dependency ExecuTorch versions agree + run: | + headers=$(sed -n 's/^#define ET_VERSION "\(.*\)"$/\1/p' \ + third-party/include/executorch/runtime/core/version.h) + pinned=$(sed -n 's/^EXECUTORCH_VERSION="v\(.*\)"$/\1/p' \ + scripts/build-native-test-deps.sh) + if [ -z "$headers" ] || [ -z "$pinned" ]; then + echo "::error::could not read the ExecuTorch versions" \ + "(headers='$headers' pinned='$pinned')" + exit 1 + fi + if [ "$headers" != "$pinned" ]; then + echo "::error::third-party/include is ExecuTorch $headers but" \ + "scripts/build-native-test-deps.sh pins v$pinned." \ + "Bump EXECUTORCH_VERSION (and TOKENIZERS_COMMIT) to match" \ + "the artifacts release." + exit 1 + fi + echo "ExecuTorch $headers on both sides" + # Hermes and ExecuTorch are pinned to exact tags, so the cache only misses # when scripts/build-native-test-deps.sh changes those pins. # diff --git a/packages/react-native-executorch/scripts/build-native-test-deps.sh b/packages/react-native-executorch/scripts/build-native-test-deps.sh index de118ce439..f5e5e8381e 100755 --- a/packages/react-native-executorch/scripts/build-native-test-deps.sh +++ b/packages/react-native-executorch/scripts/build-native-test-deps.sh @@ -32,11 +32,11 @@ HERMES_REPO="https://github.com/facebook/hermes.git" # errors or, worse, ABI drift at runtime. cpp/extensions/llm additionally reads # private members of TextLLMRunner/MultimodalRunner, so a version skew there # fails to compile rather than silently misbehaving. -EXECUTORCH_VERSION="v1.3.1" +EXECUTORCH_VERSION="v1.4.1" EXECUTORCH_REPO="https://github.com/pytorch/executorch.git" # The shipped native libraries are built from software-mansion-labs/executorch -# @rne-split-build, which is ExecuTorch 1.3.1 with the tokenizers submodule +# @ms/separate-backends-1.4.1, which is ExecuTorch 1.4.1 with the tokenizers submodule # swapped for the fork below (it adds the WordPiece/Unigram models and the NFC # normalizer that upstream has not taken). third-party/include carries that # fork's headers, so linking upstream's libtokenizers.a here would compile @@ -47,7 +47,7 @@ EXECUTORCH_REPO="https://github.com/pytorch/executorch.git" # Keep this commit in sync with the tokenizers submodule of the fork commit that # produced the current headers.tar.gz. TOKENIZERS_REPO="https://github.com/software-mansion-labs/pytorch-tokenizers.git" -TOKENIZERS_COMMIT="56a30afbe2e6b4ca881d0fb7b961b9f9da156be4" +TOKENIZERS_COMMIT="a03231a20a72036bf9a8e4a3b1d63494b90da1a6" cd "$(dirname "$0")/.." PACKAGE_DIR="$(pwd)" diff --git a/packages/react-native-executorch/scripts/download-libs.js b/packages/react-native-executorch/scripts/download-libs.js index 828ee6f6e0..983cce2a0e 100644 --- a/packages/react-native-executorch/scripts/download-libs.js +++ b/packages/react-native-executorch/scripts/download-libs.js @@ -390,9 +390,20 @@ function sha256(filePath) { return result.toString().split(' ')[0].trim(); } -function isCacheValid(artifact) { +async function isCacheValid(artifact) { if (!fs.existsSync(artifact.cacheFile)) return false; - if (!fs.existsSync(artifact.cacheChecksumFile)) return false; + // Refresh the checksum from the release before trusting the cache. Comparing + // a cached tarball against a CACHED checksum lets a stale cache validate + // itself: the cache directory is keyed on the libs version, so a release + // re-cut at the same version is never picked up -- every artifact reports a + // cache hit and the old files are reused indefinitely. + try { + await download(artifact.checksumUrl, artifact.cacheChecksumFile); + } catch { + // Unreachable checksum (offline, rate limited): fall back to the cached + // one so an already-populated cache still builds without a network. + if (!fs.existsSync(artifact.cacheChecksumFile)) return false; + } const expectedChecksum = fs.readFileSync(artifact.cacheChecksumFile, 'utf8').trim(); const actualChecksum = sha256(artifact.cacheFile); return expectedChecksum === actualChecksum; @@ -400,7 +411,15 @@ function isCacheValid(artifact) { function extract(tarball, destDir) { ensureDir(destDir); - execSync(`tar -xzf "${tarball}" -C "${destDir}"`); + // `-m` stamps extracted files with the extraction time instead of the mtime + // recorded in the archive. Without it, headers keep the timestamp they had + // when the release was cut, so upgrading to a NEWER artifacts release can + // hand ninja headers that look OLDER than object files from a previous build. + // Ninja then treats those objects as up to date and never recompiles them, + // and they get archived and linked against the new libraries -- which shows + // up as an undefined symbol for whatever API changed between the two + // releases, far away from the actual cause. + execSync(`tar -xzmf "${tarball}" -C "${destDir}"`); } // ---- Main ------------------------------------------------------------------ @@ -432,7 +451,7 @@ async function main() { for (const artifact of artifacts) { console.log(`[react-native-executorch] Preparing ${artifact.name}...`); - if (isCacheValid(artifact)) { + if (await isCacheValid(artifact)) { console.log(` ✓ Cache hit, skipping download`); } else { console.log(` ↓ Downloading ${artifact.url}`);