Records from 19–20 October 2026, one Windows 11 PC, one real app. Everything here comes from our own build reports.
FoosArena is Claudio's Expo app: SDK 57.0.30, React Native 0.86.3, six config plugins (expo-router, splash-screen, localization, location, notifications and Google Sign-In), a google-services.json and an EAS build profile with three EXPO_PUBLIC_* variables. Its build 17 was accepted by Google Play for testing, built by an older private script. WinBuild is a command-line tool we are writing to produce signed Android App Bundles (AABs) for Expo apps on Windows, without EAS Build. The question we wanted to answer was simple: can it rebuild an app that Play already accepts, with the same key?
The answer so far is yes, on one PC and one emulator. It took five fixes, a build that died after 45 minutes, and a speed-up we first measured wrongly. Here is the whole story, including what we have not tried.
The starting point
Before WinBuild, FoosArena was built by a private script that had already learned some hard lessons: a short build folder and a Gradle home outside the user profile. It worked, but it was a script for one app. We wanted a tool that could be used by other people, and that meant running it on the one app we could not afford to break.
The rules for the test were strict. Claudio, the owner, authorized it, WinBuild worked on a copy of the project in a short folder (C:\wbg\fa1), and FoosArena's repository was only read: its working tree was clean before and after, and the original build 17 AAB was untouched.
The first build and the first failure
We ran the pipeline step by step: plan, prepare, install, prebuild, dependency check, Gradle wrapper pin, native check, bundle, verify, export. Install and prebuild finished in 8.5 minutes and ran no npm script.
The first real failure came after 45 minutes of Gradle, in :app:lintVitalAnalyzeRelease: OutOfMemoryError: Metaspace. Native compilation had already finished. All the work was done, and then the build died on a lint check.
This one cost us the most time, because you only see it at the very end. The explanation, and the fix by hand, are in the Metaspace article. WinBuild now gives each build its own Gradle home with up to 4 GB of heap and 1 GB of Metaspace.
The other gaps a real app showed
The rest of the list only showed up on a real app:
- EAS profile variables were not applied. The first app would have been built without its Supabase URL and Google client ID.
bundle --profilenow applies theEXPO_PUBLIC_*variables of the chosen profile and checks that all three end up in the JavaScript bundle. That check proves the values are inside the bundle, not that the app works with them. - The dependency check stopped on version ranges. 40 of FoosArena's 48 dependencies use
^or~ranges, and our first dependency gate stopped on them. It now compares ranges with npm's rules. - **
plancalled thebetaprofile unsupported**, whilebundleaccepted it. They now share the rules. - The password sweep crashed after the failure. More on that below.
Three older problems came from our test project, not from FoosArena, but they explain the shape of the tool: paths over 260 characters, an apostrophe in the Windows user name (our machine's user folder has one) and the Java 17 toolchain. FoosArena's old script had already worked around the first two with short folders; WinBuild enforces a short workspace and a separate Gradle root from the start.
Signing with the real key, no password on disk
For the second attempt we used FoosArena's real EAS upload key, read from its credentials.json. The passwords go to Gradle only as injected properties and environment variables of the build process, never as a file in the workspace. The way it works, and the manual recipe, are in the signing article.
We do not just trust that. After every signed build, WinBuild scans the files written during the build for the passwords. In the FoosArena run it read 40,220 files and found none. Then we ran an independent scan of 60,990 files (13.6 GB) in the build folders and the user's Temp folder: no occurrence of the keystore or key password. Two Temp files could not be read, and we say so.
The first failed attempt also exposed a bug in that sweep: after the Metaspace failure it threw an unclassified exception, and the CLI answered with a generic BUNDLE_BLOCKED. The state was safe and the lock was released, but the message was useless. The sweep now reads in blocks, lists any file it could not read, and reports SECRET_SWEEP_INCOMPLETE instead of crashing.
Verify, and the same certificate as build 17
The second attempt finished: Gradle took 28 min 40 s and the whole bundle command 2,244 s. Then verify opened the AAB without installing anything and checked it: manifest, target SDK 36, 16 KB alignment of the 64-bit libraries, signature integrity and the signing certificate (what each check does is in the verification article). All passed, and localPlayChecksPassed was true.
Then we compared the result with build 17, the one Google Play accepted:
- Certificate SHA-256:
5B:67:D2:8A:…:46:75:8E:5F, identical. - Manifest:
eu.foosarena.app, versionCode 17, version 1.0.0, minSdk 24, targetSdk 36, not debuggable. Identical. - ABIs, dex files and entries: the same four ABIs, 5 dex files, 1,469 entries.
- 16 KB: 50 64-bit libraries aligned, as in build 17.
- Size: 78,363,594 bytes, against 78,363,503 for build 17.
So the key is the same upload key that Play already accepts for FoosArena. Two limits. First, this proves the file looks and is signed like build 17; it does not prove Play would accept a new upload, because only Play Console decides that. Second, versionCode 17 cannot be uploaded again: a real upload needs a higher number in app.json. We have not uploaded anything.
The time: 2,244 s, then 1,428 s
Our first measurement of a smaller build was wrong, and we want that on the record. The Expo template builds four ABIs. WinBuild's --abi store builds two, arm64-v8a and armeabi-v7a, and leaves out x86 and x86_64, which the Android emulator on a PC and x86 ChromeOS devices use.
On the evening of 19 October we ran FoosArena with --abi store: it took 3,541 s and the AAB shrank from 78.4 MB to 48.9 MB (−38%). We almost wrote that --abi store is slower. It was not: Docker Desktop and WSL were running for another job, so we discarded the time as a measurement.
The next night, with the PC free, the same option took 1,428 s (23.8 minutes), 36% less than the 2,244 s of the 4-ABI build. Gradle itself took 14 min 24 s; about 9.5 minutes were the password sweep of the older version, which read 39,988 freshly written files while Windows Defender scanned them. Caution: the two builds ran on different nights and at different source commits, so this is one data point, not a benchmark. The full breakdown of what we tried, and what did not help, is in the slow-builds article.
The sweep was the part we could fix at once. After a rewrite it lists the files first and reads them in parallel; on a fresh build we measured 282 s for 39,920 files.
Starting the app on the emulator
The last check was running the app. A build with 2 ABIs (arm64-v8a and x86_64) went through device-test on an Android 36 x86_64 emulator:
- The app installed in 37 s.
MainActivitywas in the foreground 8 seconds after launch, with no crash.- The screenshot shows FoosArena's home screen, with "Find a table" and "Sign in", and the message "All quiet here" where open matches would be. That suggests the app reaches its backend with the
betavariables. We did not test any specific feature.
The evening before, the --abi store build had installed but crashed at launch on the same emulator with SoLoaderDSONotFoundError. The emulator runs the app as ARM through translation, and React Native 0.86's loader looked for its libraries under x86_64 inside the ARM split. A test project behaved the same with 2 ABIs and started with 4, so we do not read it as a defect of the AAB. It does mean the store AAB itself has never run on any device.
What we have not proven
- No real phone. The app has only run on an emulator, and only from the 2-ABI build. The
storeAAB needs a test on an ARM phone. - One PC. Everything ran on one machine (8 cores, 31.6 GB of RAM, Defender on). A second PC is the next test we need.
- One real app. FoosArena is a single data point, and
release, the one-command version of the pipeline, was tried on a test project and a second test app, not on FoosArena. - No upload. We did not upload the rebuilt AAB to Google Play.
- Not reproducible byte for byte. Two Gradle builds of the same project give different SHA-256 values.
- Cleaning. The first attempt left 3.6 GB in a Gradle home that we deleted by hand.
Sources
All figures come from our own build records of 19–20 October 2026. No external sources.
Private alpha
Building Expo Android releases on Windows?
WinBuild is in private alpha. Testers get the package directly and help validate different machines and Expo projects.