Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions .github/workflows/check-android-dependency-alignment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: Check Android dependency 16 KB alignment

# check-16k-page-alignment.yml reads every TRACKED artifact. Most native code a
# Codename One Android app ships is not tracked: the builder writes Maven
# coordinates into the generated build.gradle (PlatformFeatureCatalog entries,
# the play-services defaults) and Gradle downloads them on the build machine.
# CameraX 1.3.4 reached every CodeScanner / VisionCameraView app that way with
# a 4 KB aligned libimage_processing_util_jni.so, Google Play refused the
# upload, and the tree gate stayed green because it never saw the file.
#
# This resolves every one of those coordinates with Gradle, each catalog entry
# on its own (Gradle's highest-version-wins would otherwise let one entry's new
# pin hide another's old one), and runs the same ELF check over the result.
# It needs the network, so it runs only when something that decides the set
# changes; AndroidSixteenKbPageFloorTest is the offline half (version floors,
# and a ratchet that fails when the builder sources name a coordinate the
# lister does not list). It runs here first, and in every build-engine test run.
#
# The cloud builder (BuildDaemon) has its own copy of the catalog and the
# defaults and runs this same script over its own list.

permissions:
contents: read

on:
workflow_dispatch:
pull_request:
branches: [ master ]
paths:
- 'maven/platform-feature-catalog/**'
- 'maven/build-engine/src/main/java/com/codename1/builders/**'
- 'maven/build-engine/src/test/java/com/codename1/builders/AndroidDependencyCoordinates.java'
- 'maven/build-engine/src/test/java/com/codename1/builders/AndroidSixteenKbPageFloorTest.java'
- 'maven/build-hint-catalog/**'
- 'scripts/check-android-dependency-alignment.py'
- 'scripts/test_check_android_dependency_alignment.py'
- 'scripts/check-16k-page-alignment.py'
- '.github/workflows/check-android-dependency-alignment.yml'
push:
branches: [ master ]
paths:
- 'maven/platform-feature-catalog/**'
- 'maven/build-engine/src/main/java/com/codename1/builders/**'
- 'maven/build-engine/src/test/java/com/codename1/builders/AndroidDependencyCoordinates.java'
- 'maven/build-engine/src/test/java/com/codename1/builders/AndroidSixteenKbPageFloorTest.java'
- 'maven/build-hint-catalog/**'
- 'scripts/check-android-dependency-alignment.py'
- 'scripts/test_check_android_dependency_alignment.py'
- 'scripts/check-16k-page-alignment.py'
- '.github/workflows/check-android-dependency-alignment.yml'

concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
check-android-dependency-alignment:
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- uses: actions/checkout@v6

- name: Unit-test the gate
# Run before anything is resolved, so a broken gate is reported as a
# broken gate rather than as a clean dependency set.
run: python3 scripts/test_check_android_dependency_alignment.py

# Installed FIRST so the JDK 8 below is the one JAVA_HOME names.
- name: Set up JDK 17 for Gradle
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'temurin'
- run: echo "GRADLE_JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV"
- uses: gradle/actions/setup-gradle@v4
with:
gradle-version: '8.13'
- name: Set up JDK 8
uses: actions/setup-java@v5
with:
java-version: '8'
distribution: 'temurin'
cache: 'maven'

- name: Build build-engine, run the floor test, list its Android dependencies
env:
RETRY_ONLY_MATCHING: transient
RETRY_ATTEMPTS: '5'
RETRY_DELAY_SECONDS: '60'
run: |
set -euo pipefail
# The build runs UpdateCodenameOne.jar from ~/.codenameone, as in
# every other workflow that builds the framework.
mkdir -p ~/.codenameone
cp maven/UpdateCodenameOne.jar ~/.codenameone/
cd maven
bash ../scripts/ci/retry.sh mvn -B -pl build-engine -am install \
-DskipTests -Dmaven.javadoc.skip=true -Plocal-dev-javase
mvn -B -pl build-engine test -Dtest=AndroidSixteenKbPageFloorTest \
-Dsurefire.failIfNoSpecifiedTests=false -Plocal-dev-javase
mvn -B -q -pl build-engine test-compile dependency:build-classpath \
-Dmdep.outputFile="$RUNNER_TEMP/build-engine.cp" -Plocal-dev-javase
java -Dcn1.builders.src=build-engine/src/main/java/com/codename1/builders \
-cp "build-engine/target/classes:build-engine/target/test-classes:$(cat "$RUNNER_TEMP/build-engine.cp")" \
com.codename1.builders.AndroidDependencyCoordinates "$RUNNER_TEMP/android-coordinates.txt"
wc -l "$RUNNER_TEMP/android-coordinates.txt"

- name: Resolve every dependency and check 16 KB page alignment
run: |
JAVA_HOME="$GRADLE_JAVA_HOME" GRADLE="$(command -v gradle)" \
python3 scripts/check-android-dependency-alignment.py \
--coordinates "$RUNNER_TEMP/android-coordinates.txt" \
--work "$RUNNER_TEMP/dependency-alignment" --verbose
22 changes: 22 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,28 @@ scripts/check-16k-page-alignment.py # every tracked artifact
scripts/check-16k-page-alignment.py --verbose # list everything scanned
```

**That gate reads tracked files only, and most of an app's natives are not
tracked.** The builder writes Maven coordinates into the generated
`build.gradle` -- `PlatformFeatureCatalog` entries and the play-services
defaults -- and Gradle downloads them on the build machine. CameraX 1.3.4
reached every `CodeScanner` app that way, 4 KB aligned, and Play refused the
upload with every check green. `scripts/check-android-dependency-alignment.py`
(`check-android-dependency-alignment.yml`) resolves that whole list with Gradle,
**each catalog entry in its own configuration** -- pooled, Gradle's
highest-version-wins upgrades an old pin to another entry's newer one and the
old one reads clean -- and runs the same ELF check. It needs the network;
`AndroidSixteenKbPageFloorTest` in build-engine is the offline half, a table of
measured first-aligned versions. A new native dependency in the catalog needs
both: run the script, then add the floor. The cloud builder (BuildDaemon) keeps
its own catalog and runs the same script over its own list.

```bash
cd maven && mvn -pl build-engine test-compile dependency:build-classpath -Dmdep.outputFile=/tmp/cp
java -cp build-engine/target/classes:build-engine/target/test-classes:$(cat /tmp/cp) \
com.codename1.builders.AndroidDependencyCoordinates /tmp/coords.txt
cd .. && JAVA_HOME=<jdk17> scripts/check-android-dependency-alignment.py --coordinates /tmp/coords.txt
```

#### ParparVM native names are checked, and getting one wrong is silent

ParparVM encodes the **whole Java signature in the C function name**. For
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2652,6 +2652,11 @@ public boolean build(File sourceZip, final BuildRequest request) throws BuildExc
// ("uses-sdk:minSdkVersion 19 cannot be smaller than version 21").
minSDK = maxInt("21", minSDK);
}
if (rootCheck) {
// rootbeer-lib 0.1.0 ships 4 KB aligned arm64/x86_64 natives, which Play
// rejects; 0.1.2 is aligned but declares minSdk 21 in its manifest.
minSDK = maxInt("21", minSDK);
}
if (googleAdUnitId != null && googleAdUnitId.length() > 0) {
minSDK = maxInt("9", minSDK);
if (!userXapplication.contains("com.google.android.gms.version")) {
Expand Down Expand Up @@ -7067,7 +7072,7 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
String rootCheckCall = "";
if (rootCheck) {
if (!request.getArg("gradleDependencies", "").contains("com.scottyab:rootbeer-lib")) {
String rootbeerVersion = request.getArg("android.rootbeerVersion", "0.1.0");
String rootbeerVersion = request.getArg("android.rootbeerVersion", "0.1.2");
request.putArgument(
"gradleDependencies",
request.getArg("gradleDependencies", "") +
Expand Down Expand Up @@ -8119,7 +8124,12 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
additionalDependencies += " "+compile+" 'com.google.android.gms:play-services-identity:"+getDefaultPlayServiceVersion("identity")+"'\n";
}
if(playServicesIndexing){
additionalDependencies += " "+compile+" 'com.google.android.gms:play-services-appindexing:"+getDefaultPlayServiceVersion("appindexing")+"'\n";
// play-services-appindexing ended at 9.8.0 and continued as firebase-appindexing
// under another package. Any newer version names nothing, and 9.8.0 beside a
// current play-services base breaks at run time, so the hint cannot work.
throw new BuildException("android.playService.indexing is no longer supported: "
+ "Google discontinued play-services-appindexing after 9.8.0; use "
+ "com.google.firebase:firebase-appindexing through android.gradleDep instead");
}
if(playServicesInvite){
additionalDependencies += " "+compile+" 'com.google.android.gms:play-services-appinvite:"+getDefaultPlayServiceVersion("appinvite")+"'\n";
Expand Down Expand Up @@ -8155,13 +8165,13 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
additionalDependencies += " "+compile+" 'com.google.android.gms:play-services-nearby:"+getDefaultPlayServiceVersion("nearby")+"'\n";
}
if(playServicesSafetyPanorama){
additionalDependencies += " "+compile+" 'com.google.android.gms:play-services-panaroma:"+getDefaultPlayServiceVersion("panorama")+"'\n";
additionalDependencies += " "+compile+" 'com.google.android.gms:play-services-panorama:"+getDefaultPlayServiceVersion("panorama")+"'\n";
}
if(playServicesGames){
additionalDependencies += " "+compile+" 'com.google.android.gms:play-services-games:"+getDefaultPlayServiceVersion("games")+"'\n";
}
if(playServicesSafetyNet){
additionalDependencies += " "+compile+" 'com.google.android.gms:play-services-safenet:"+getDefaultPlayServiceVersion("safenet")+"'\n";
additionalDependencies += " "+compile+" 'com.google.android.gms:play-services-safetynet:"+getDefaultPlayServiceVersion("safetynet")+"'\n";
}
if(playServicesWallet){
additionalDependencies += " "+compile+" 'com.google.android.gms:play-services-wallet:"+getDefaultPlayServiceVersion("wallet")+"'\n";
Expand Down Expand Up @@ -12398,6 +12408,14 @@ private void createAndroidStudioProject(File dest) throws IOException {
}


/// The play-services defaults, keyed by the name after `play-services-`.
/// Read by `AndroidDependencyCoordinates`, which lists every coordinate
/// this builder can inject so CI can check its native libraries for
/// 16 KB page alignment.
static Map<String, String> defaultPlayServiceVersionsView() {
return Collections.unmodifiableMap(defaultPlayServiceVersions);
}

private String getDefaultPlayServiceVersion(String playService) {
if (playServiceVersions.containsKey(playService)) {
return playServiceVersions.get(playService);
Expand Down
Loading
Loading