Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
defe78e
Add a device runtime that runs pushed Codename One apps on a phone
shai-almog Aug 17, 2026
4542c23
Add store metadata and a weekly build for the device runtime
shai-almog Aug 17, 2026
c6c6ada
Device runtime: move interp under impl, generate the shims, authentic…
shai-almog Aug 17, 2026
3118d71
Give the probe programs the standard header
shai-almog Aug 17, 2026
7e92c49
Markdown javadoc and a header the gates asked for
shai-almog Aug 17, 2026
d4d7ad9
Clear the SpotBugs findings in the push tool
shai-almog Aug 17, 2026
1356d05
Answer the second review round
shai-almog Aug 17, 2026
47fd057
Fix the two gates the guide and the CLDC build enforce
shai-almog Aug 17, 2026
3155e0b
Add StackOverflowError to the CLDC11 stub
shai-almog Aug 17, 2026
7a54e8f
Bring the device runtime chapter up to the guide's prose gate
shai-almog Aug 17, 2026
f86bfb2
Restore CRLF in AndroidImplementation
shai-almog Aug 17, 2026
79950c6
Clear the LanguageTool gate for the device runtime chapter
shai-almog Aug 17, 2026
b91272a
Let the vtable spike translate under a strict native-signature check
shai-almog Aug 17, 2026
58d244d
Clear PMD and Checkstyle for the interpreter
shai-almog Aug 17, 2026
fd6744b
Answer the third review round
shai-almog Aug 17, 2026
9131f21
Fix a mangled identifier in the iOS symbol reader
shai-almog Aug 17, 2026
5ce1185
Update the symbol-row test for the interfaces column
shai-almog Aug 17, 2026
5fbf0a3
Answer the fourth review round
shai-almog Aug 18, 2026
9b7d77f
Answer the fifth review round
shai-almog Aug 18, 2026
273783f
Answer the sixth review round
shai-almog Aug 18, 2026
9748ff8
Answer the seventh review round
shai-almog Aug 18, 2026
4f9540b
Answer the eighth review round
shai-almog Aug 18, 2026
110ac22
Answer the ninth review round
shai-almog Aug 18, 2026
612a5af
Answer the tenth review round
shai-almog Aug 18, 2026
26111b3
Answer the eleventh review round
shai-almog Aug 18, 2026
e5cde7d
Carry the native half of the subsystems worth a device
shai-almog Aug 18, 2026
dd981e8
Mock the subsystems that cannot be real, instead of excluding them
shai-almog Aug 18, 2026
a9c30b4
Answer the twelfth review round
shai-almog Aug 18, 2026
0c0a846
Answer the thirteenth review round
shai-almog Aug 18, 2026
f242778
Answer the fourteenth review round
shai-almog Aug 18, 2026
07913a3
Answer the fifteenth review round
shai-almog Aug 18, 2026
1ccc6d7
Name only exceptions the device has
shai-almog Aug 18, 2026
1929de4
Answer the sixteenth review round
shai-almog Aug 18, 2026
525c99d
Only write java.io.File's native bodies where the class survives
shai-almog Aug 18, 2026
44b22f9
Answer the seventeenth review round
shai-almog Aug 18, 2026
c448acc
Answer the eighteenth review round
shai-almog Aug 18, 2026
6508109
Answer the nineteenth review round
shai-almog Aug 18, 2026
20180da
Answer the twentieth review round
shai-almog Aug 18, 2026
0af4958
Answer the twenty-first review round
shai-almog Aug 18, 2026
c260b3a
Answer the twenty-second review round
shai-almog Aug 18, 2026
48fe18a
Answer the twenty-third review round
shai-almog Aug 18, 2026
dff7808
Answer the twenty-fourth review round
shai-almog Aug 18, 2026
a26a610
Answer the twenty-fifth review round
shai-almog Aug 18, 2026
1b5ab70
Answer the twenty-sixth review round
shai-almog Aug 18, 2026
f88e517
Answer the twenty-seventh review round
shai-almog Aug 18, 2026
fcf6635
Answer the twenty-eighth review round
shai-almog Aug 18, 2026
da7dfea
Answer the twenty-ninth review round
shai-almog Aug 18, 2026
d817420
Answer the thirtieth review round
shai-almog Aug 18, 2026
2a98bef
Answer the thirty-first review round
shai-almog Aug 19, 2026
20fa329
Answer the thirty-second review round
shai-almog Aug 19, 2026
fd07b5f
Answer the thirty-third review round
shai-almog Aug 19, 2026
e1f0489
Answer the thirty-fourth review round
shai-almog Aug 19, 2026
8593d2f
Answer the thirty-fifth review round
shai-almog Aug 19, 2026
151e06e
Answer the thirty-sixth review round
shai-almog Aug 19, 2026
1f4396d
Answer the thirty-seventh review round
shai-almog Aug 19, 2026
f0e9a6d
Answer the thirty-eighth review round
shai-almog Aug 19, 2026
4dc4c12
Answer the thirty-ninth review round
shai-almog Aug 19, 2026
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
262 changes: 262 additions & 0 deletions .github/workflows/device-runtime-store.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# Weekly build of the device runtime for testers.
#
# Uploads to Google Play internal testing and to TestFlight. It does not promote
# to production and does not submit for App Store review -- see
# scripts/cn1-device-runtime/store/README.md for why that is deliberate.
#
# Without credentials the job reports what is missing and stops. It never
# publishes half of a release.
name: Device runtime store build

on:
schedule:
# Monday morning, so a failure has a working week in front of it.
- cron: '0 6 * * 1'
workflow_dispatch:
inputs:
dry_run:
description: 'Build and check credentials without uploading'
type: boolean
default: false

concurrency:
group: device-runtime-store
cancel-in-progress: false

jobs:
preflight:
runs-on: ubuntu-latest
outputs:
android: ${{ steps.check.outputs.android }}
ios: ${{ steps.check.outputs.ios }}
steps:
- id: check
name: Which stores are configured
env:
PLAY_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
KEYSTORE: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
IOS_CERT: ${{ secrets.IOS_DIST_CERT_P12 }}
IOS_CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
IOS_PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE }}
ASC_KEY: ${{ secrets.APPSTORE_PRIVATE_KEY }}
ASC_KEY_ID: ${{ secrets.APPSTORE_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }}
run: |
# Every secret the job will actually consume, not just the two that
# name the store. A partial configuration used to pass here, spend
# half an hour building, and then fail in the signing step -- which is
# the opposite of what a preflight is for.
android=true
ios=true
missing=""
for name in PLAY_JSON KEYSTORE KEYSTORE_PASSWORD KEY_ALIAS KEY_PASSWORD; do
if [ -z "${!name}" ]; then android=false; missing="$missing android:$name"; fi
done
for name in IOS_CERT IOS_CERT_PASSWORD IOS_PROFILE ASC_KEY ASC_KEY_ID ASC_ISSUER_ID; do
if [ -z "${!name}" ]; then ios=false; missing="$missing ios:$name"; fi
done
if [ -n "$missing" ]; then
echo "missing secrets:$missing"
fi
echo "android=$android" >> "$GITHUB_OUTPUT"
echo "ios=$ios" >> "$GITHUB_OUTPUT"
echo "### Device runtime store build" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| Store | Configured |" >> "$GITHUB_STEP_SUMMARY"
echo "|---|---|" >> "$GITHUB_STEP_SUMMARY"
echo "| Google Play | $android |" >> "$GITHUB_STEP_SUMMARY"
echo "| App Store | $ios |" >> "$GITHUB_STEP_SUMMARY"
if [ -n "$missing" ]; then
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Secrets that are absent:\`$missing\`" >> "$GITHUB_STEP_SUMMARY"
fi
if [ "$android" = false ] && [ "$ios" = false ]; then
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No publishing credentials are present, so nothing was uploaded." >> "$GITHUB_STEP_SUMMARY"
echo "The secrets each store needs are listed in" >> "$GITHUB_STEP_SUMMARY"
echo "\`scripts/cn1-device-runtime/store/README.md\`." >> "$GITHUB_STEP_SUMMARY"
fi

android:
needs: preflight
if: needs.preflight.outputs.android == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: JDK 8 for the framework, JDK 17 for the Android port
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: |
8
17
Comment thread
shai-almog marked this conversation as resolved.

- name: Build the framework and the Android port
env:
# setup-java makes the LAST version listed the default, so JAVA_HOME
# is 17 here despite the step name. The framework build wants 8 -- the
# newer JDK trips JaCoCo -- and the Android port wants 17, so each
# command names its own.
JAVA8: ${{ env.JAVA_HOME_8_X64 }}
run: |
export JAVA_HOME="$JAVA8"
export PATH="$JAVA_HOME/bin:$PATH"
cd maven
mvn -B -q -DskipTests install -pl core,parparvm -am
mvn -B -q -DskipTests -Pcompile-android install -pl android

- name: Verify the shim generator
# The app build generates the shims; this asserts the properties that
# build takes on faith. It fails if a shim will not compile, if a
# load-bearing shim is missing, or if generating twice differs -- all of
# which are release blockers.
run: scripts/generate-interp-shims.sh

- name: Build the app bundle
env:
JAVA17_HOME: ${{ env.JAVA_HOME_17_X64 }}
run: |
# A store rejects an upload that reuses a version code, and the app's
# version is a fixed 1.0 -- so every scheduled run after the first
# would be refused. The run number is monotonic and unique per repo,
# which is exactly what a build number has to be. It has to travel as
# codename1.arg.*: that is the prefix CN1BuildMojo copies into the
# BuildRequest, and android.versionCode is what the Gradle builder
# reads from it.
cd scripts/cn1-device-runtime
mvn -B -q package -DskipTests \
-Dcodename1.platform=android \
-Dcodename1.buildTarget=android-source \
-Dcodename1.arg.android.versionCode=${{ github.run_number }} \
-Dopen=false

- name: Sign and assemble
env:
KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
set -euo pipefail
gradle_dir="$(find scripts/cn1-device-runtime/android/target -maxdepth 1 \
-name '*-android-source' -type d | head -1)"
[ -n "$gradle_dir" ] || { echo "no gradle project was generated" >&2; exit 1; }
echo "$KEYSTORE_B64" | base64 -d > "$gradle_dir/upload.keystore"
cd "$gradle_dir"
./gradlew --no-daemon bundleRelease \
-Pandroid.injected.signing.store.file=upload.keystore \
-Pandroid.injected.signing.store.password="$KEYSTORE_PASSWORD" \
-Pandroid.injected.signing.key.alias="$KEY_ALIAS" \
-Pandroid.injected.signing.key.password="$KEY_PASSWORD"

- name: Upload to internal testing
if: ${{ !inputs.dry_run }}
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
packageName: com.codenameone.devruntime
releaseFiles: scripts/cn1-device-runtime/android/target/*-android-source/app/build/outputs/bundle/release/*.aab
track: internal
status: completed
whatsNewDirectory: scripts/cn1-device-runtime/fastlane/metadata/android/en-US/changelogs

ios:
needs: preflight
if: needs.preflight.outputs.ios == 'true'
runs-on: macos-14
steps:
- uses: actions/checkout@v4

- name: JDK 8 for the framework, JDK 17 for the translator
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: |
8
17

- name: Build the framework, translator and iOS port
env:
JAVA8: ${{ env.JAVA_HOME_8_X64 }}
run: |
# As above: the last JDK listed to setup-java is the default, and the
# framework build wants 8.
export JAVA_HOME="$JAVA8"
export PATH="$JAVA_HOME/bin:$PATH"
cd maven
mvn -B -q -DskipTests install -pl core,parparvm,ios -am

- name: Verify the shim generator
run: scripts/generate-interp-shims.sh

- name: Translate to Xcode
env:
JAVA17_HOME: ${{ env.JAVA_HOME_17_X64 }}
run: |
cd scripts/cn1-device-runtime
# As on Android: TestFlight refuses a build number it has seen, and
# the app's version stays 1.0 between releases. ios.bundleVersion is
# the argument IPhoneBuilder reads for CFBundleVersion, and it has to
# be dotted numerics, so the run number becomes the third component.
mvn -B -q package -DskipTests \
-Dcodename1.platform=ios \
-Dcodename1.buildTarget=ios-source \
-Dcodename1.arg.ios.interpHost=true \
-Dcodename1.arg.ios.bundleVersion=1.0.${{ github.run_number }} \
-Dopen=false

- name: Import signing material
env:
CERT_P12: ${{ secrets.IOS_DIST_CERT_P12 }}
CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE }}
run: |
set -euo pipefail
keychain=build.keychain
security create-keychain -p actions "$keychain"
security default-keychain -s "$keychain"
security unlock-keychain -p actions "$keychain"
echo "$CERT_P12" | base64 -d > cert.p12
security import cert.p12 -k "$keychain" -P "$CERT_PASSWORD" \
-T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k actions "$keychain"
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
echo "$PROFILE" | base64 -d > \
~/Library/MobileDevice/Provisioning\ Profiles/devruntime.mobileprovision

- name: Archive and export
run: |
set -euo pipefail
src="$(find scripts/cn1-device-runtime/ios/target -maxdepth 1 \
-name '*-ios-source' -type d | head -1)"
[ -n "$src" ] || { echo "no Xcode project was generated" >&2; exit 1; }
# Resolved before the cd: the export options live at a fixed place in
# the repo, and the generated project's depth is not ours to predict.
export_options="$PWD/scripts/cn1-device-runtime/store/ExportOptions.plist"
cd "$src"
xcodebuild -workspace CN1DeviceRuntime.xcworkspace \
-scheme CN1DeviceRuntime -configuration Release \
-archivePath build/CN1DeviceRuntime.xcarchive archive
xcodebuild -exportArchive \
-archivePath build/CN1DeviceRuntime.xcarchive \
-exportPath build/ipa \
-exportOptionsPlist "$export_options"

- name: Upload to TestFlight
if: ${{ !inputs.dry_run }}
env:
ASC_ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }}
ASC_KEY_ID: ${{ secrets.APPSTORE_KEY_ID }}
ASC_PRIVATE_KEY: ${{ secrets.APPSTORE_PRIVATE_KEY }}
run: |
set -euo pipefail
mkdir -p ~/private_keys
echo "$ASC_PRIVATE_KEY" > ~/private_keys/AuthKey_$ASC_KEY_ID.p8
src="$(find scripts/cn1-device-runtime/ios/target -maxdepth 1 \
-name '*-ios-source' -type d | head -1)"
xcrun altool --upload-app -f "$src"/build/ipa/*.ipa -t ios \
--apiKey "$ASC_KEY_ID" --apiIssuer "$ASC_ISSUER_ID"
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,9 @@ scripts/fidelity-app/common/src/main/resources/*ThemeDev.res
# build time (common/pom.xml copy-native-themes); never commit the duplicate.
scripts/fidelity-app/common/src/main/resources/iOSModernTheme.res
scripts/fidelity-app/common/src/main/resources/AndroidMaterialTheme.res

# Local Maven repository used for isolated local builds (see .m2-local)
.m2-local/

# Generated by the device runtime build (see common/pom.xml).
scripts/cn1-device-runtime/common/src/main/java/com/codenameone/devruntime/gen/
54 changes: 54 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,60 @@ removing one can make a previously-used private method dead.

Findings land in each module's `target/spotbugsXml.xml`.

### Device runtime (on-device interpreter)

`scripts/hellocodenameone/` is a device runtime: an app that runs Codename One
programs pushed from a desktop, interpreted rather than compiled. See
`docs/developer-guide/Device-Runtime.asciidoc`. Verify on both platforms --
`scripts/run-device-runtime-android.sh <Program.java>` (minutes) and
`scripts/run-device-runtime-ios.sh <Program.java>` (~30 min).

- **A pushed `main` runs on the EDT**, like an app's `start()`. `callSerially`
is legal there, `callSeriallyAndWait` is not.
- **The EDT budget is per entry into the interpreter, not per session.** Every
framework callback is a fresh entry. Measuring from the start of the run makes
the budget expire once and stay expired, killing every later callback -- i.e.
every button press -- with "ran without yielding".
- **Lambdas and method references are desugared** by `InterpLambdaDesugar` when
the bundle is written; neither target can spin a class at run time. String
concatenation needs `-XDstringConcat=inline` (cn1-push.sh passes it).
- **A linker must dispatch on the receiver's class, not the call site's owner.**
`list.add(x)` compiles to a call naming `java.util.List`; resolving from there
finds `AbstractList.add`, whose body throws `UnsupportedOperationException`.
Android got this free from reflection; iOS resolves the receiver's class id
and walks up from there.
- **`synchronized` uses the real host monitor**, so it interoperates with the
framework and `wait`/`notify` work. A synchronized method wraps the call (on
the peer where there is one); a synchronized block runs its region nested
inside a real `synchronized`, and `monitorexit` returns to the enclosing
level, which is what releases it.
- **A pushed tree's non-`.java` files become resources**, published to
`CodenameOneImplementation` -- not `Display`, which `Resources.openLayered`
never passes through.
- **`java.lang.Enum` has no shim and needs none** -- the interpreter answers
name/ordinal/valueOf itself, since Java forbids naming Enum as a superclass.
- **The device dials out; the desktop listens.** A listening socket inside the
iOS simulator is unreachable from the host. Android needs `adb reverse`, not
`adb forward`. Both runtimes dial the same host port, so a running emulator
app will answer a push meant for the simulator -- the iOS script force-stops
it for exactly this reason.
- **Push a source tree, not a file**: `scripts/cn1-push.sh src/main/java 18234`.
The entry point is discovered -- a `main`, else a `Lifecycle` subclass, which
is what a real app has. `scripts/devruntime-probes/` holds the battery of
programs that found the defects worth knowing about; run it after touching
the interpreter, the linkers or the shims.
- **The shims are generated by the app build** into
`common/target/generated-sources/shims`, never committed. After changing
`GenerateInterpShims`, run `scripts/generate-interp-shims.sh`: it fails if a
shim will not compile (never prune -- that once ate `Interp_ui_Form`), if a
load-bearing shim is missing, or if generating twice differs.
- **The generator reads the device's `java.*` from the `codenameone-java-runtime`
jar with ASM, not by reflecting over the JDK**, and runs under `JAVA17_HOME`.
The two disagree about which methods exist, which are `final`, which
interfaces a class implements, and which constructors exist. Note `javap`
resolves `java.*` from the platform even with `-cp`, so inspect the extracted
`.class` file directly or you will be reading the JDK's copy.

### Never rely on ClassCastException

**ParparVM's `CHECKCAST` is unchecked.** `BC_CHECKCAST` expands to nothing and the
Expand Down
47 changes: 47 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line number Diff line number Diff line change
Expand Up @@ -4545,12 +4545,59 @@ private int scanBackFirst(char[] chars, int ixStart, int ixEnd) {
///
/// input stream for the resource or null if not found
public InputStream getResourceAsStream(Class cls, String resource) {
InputStream local = localResource(resource);
if (local != null) {
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
return local;
}
if (cls != null) {
return cls.getResourceAsStream(resource);
}
return CodenameOneImplementation.class.getResourceAsStream(resource);
}

/// Resources supplied at run time rather than compiled into the app.
///
/// The device runtime pushes a program's own `theme.res`, CSS and images
/// here. They have to be visible from this layer rather than from
/// `Display`, because the calls that matter never pass through `Display`:
/// `Resources.openLayered("/theme")` and `UIManager.initFirstTheme` resolve
/// inside the framework, which asks the implementation directly.
///
/// Empty in an ordinary app, and one emptiness check on a path that
/// already touches the file system. Allocated eagerly rather than lazily:
/// a push arrives on a socket thread while the event thread may be reading,
/// and lazily creating a shared static under that is how entries go missing.
private static final Hashtable localResources = new Hashtable();

/// Publishes a resource under the path an application would load it by,
/// e.g. `/theme.res`. A null value removes it.
public static void setLocalResource(String path, byte[] data) {
if (data == null) {
localResources.remove(path);
} else {
localResources.put(path, data);
}
}

/// Drops every published resource, so a newly pushed program does not
/// inherit the previous one's theme.
public static void clearLocalResources() {
localResources.clear();
}

/// A published resource as a stream, or null. Platform implementations call
/// this before falling back to the classpath.
protected static InputStream localResource(String resource) {
if (resource == null || localResources.isEmpty()) {
return null;
}
byte[] data = (byte[]) localResources.get(resource);
if (data == null && !resource.startsWith("/")) {
data = (byte[]) localResources.get("/" + resource);
}
Comment thread
shai-almog marked this conversation as resolved.
return data == null ? null : new java.io.ByteArrayInputStream(data);
}

/// Animations should return true to allow the native image animation to update
///
/// #### Parameters
Expand Down
Loading
Loading