Package embedded Jerboa programs from typed Android specs
ober
4789f30248a740bf48321e8287322fd015bdca58
--- a/Makefile +++ b/Makefile @@ -60,9 +60,16 @@ test: check-no-raw-kotlin clean generate $(JERBOA) jandroid.ss generate tests/fixtures/fragment-app.ss build/fragment-fixture test -f build/fragment-fixture/app/src/main/java/org/jerboa/fragmentfixture/FragmentValue.kt # gitsafe:ignore grep -q 'fun fragmentValue(): String' build/fragment-fixture/app/src/main/java/org/jerboa/fragmentfixture/FragmentValue.kt # gitsafe:ignore + $(JERBOA) jandroid.ss generate tests/fixtures/native-packaging-app.ss build/native-packaging + grep -q 'android:extractNativeLibs="true"' build/native-packaging/app/src/main/AndroidManifest.xml + grep -q 'useLegacyPackaging = true' build/native-packaging/app/build.gradle.kts + grep -q 'keepDebugSymbols += "\*\*/libembedded_program.so"' build/native-packaging/app/build.gradle.kts $(JERBOA) jandroid.ss generate $(ORIGINAL_TACTICS_EXAMPLE) $(ORIGINAL_TACTICS_BUILD_DIR) test -f $(ORIGINAL_TACTICS_BUILD_DIR)/app/src/main/java/com/jerboa/originaltactics/ShipUiNames.kt grep -q 'label.length <= maxChars' $(ORIGINAL_TACTICS_BUILD_DIR)/app/src/main/java/com/jerboa/originaltactics/ShipUiNames.kt + grep -q 'android:extractNativeLibs="true"' $(ORIGINAL_TACTICS_BUILD_DIR)/app/src/main/AndroidManifest.xml + grep -q 'useLegacyPackaging = true' $(ORIGINAL_TACTICS_BUILD_DIR)/app/build.gradle.kts + grep -q 'keepDebugSymbols += "\*\*/liboriginaltactics_program.so"' $(ORIGINAL_TACTICS_BUILD_DIR)/app/build.gradle.kts $(JERBOA) jandroid.ss generate $(SSD_EXAMPLE) $(SSD_CONTRACT_BUILD_DIR) $(MAKE) check-generated-app-contracts $(MAKE) security --- a/jandroid.ss +++ b/jandroid.ss @@ -230,6 +230,7 @@ (let ([version (entry-value (entries spec) 'ndk-version #f)]) (if version (immutable-version version 'ndk-version) #f))) (def (allow-backup spec) (entry-value (entries spec) 'allow-backup #f)) +(def (extract-native-libs spec) (entry-value (entries spec) 'extract-native-libs #f)) (def (uses-cleartext-traffic spec) (entry-value (entries spec) 'uses-cleartext-traffic #f)) (def (theme spec) (entry-value (entries spec) 'theme "@style/AppTheme")) (def (activity-screen-orientation spec) (entry-value (entries spec) 'activity-screen-orientation #f)) @@ -241,6 +242,8 @@ (def (abi-filters spec) (entry-values (entries spec) 'abi-filter)) (def (asset-dirs spec) (entry-values (entries spec) 'asset-dir)) (def (jni-lib-dirs spec) (entry-values (entries spec) 'jni-lib-dir)) +(def (keep-jni-debug-symbols spec) + (entry-values (entries spec) 'keep-jni-debug-symbol)) (def (kotlin-source-dirs spec) (entry-values (entries spec) 'kotlin-source-dir)) (def (raw-files spec) (entry-forms (entries spec) 'file)) (def (gradle-property-forms spec) (entry-forms (entries spec) 'gradle-property)) @@ -569,7 +572,8 @@ (def (app-build-gradle spec) (let ((ndk (ndk-version spec)) - (abis (abi-filters spec))) + (abis (abi-filters spec)) + (keep-jni (keep-jni-debug-symbols spec))) (string-append "plugins {\n" " id(\"com.android.application\")\n" @@ -594,6 +598,27 @@ " sourceCompatibility = JavaVersion.VERSION_" (format "~a" (jvm-toolchain spec)) "\n" " targetCompatibility = JavaVersion.VERSION_" (format "~a" (jvm-toolchain spec)) "\n" " }\n" + (if (or (extract-native-libs spec) (not (null? keep-jni))) + (string-append + "\n" + " packaging {\n" + " jniLibs {\n" + (if (extract-native-libs spec) + " useLegacyPackaging = true\n" + "") + (apply string-append + (map (lambda (pattern) + (unless (string? pattern) + (error 'keep-jni-debug-symbol + "expected a string pattern" pattern)) + (string-append + " keepDebugSymbols += " + (kotlin-string pattern) + "\n")) + keep-jni)) + " }\n" + " }\n") + "") "}\n" (if (kotlin? spec) (string-append "\n" @@ -643,6 +668,9 @@ "\n" " <application\n" " android:allowBackup=\"" (bool-string (allow-backup spec)) "\"\n" + (if (extract-native-libs spec) + " android:extractNativeLibs=\"true\"\n" + "") " android:label=\"" (xml-escape (app-name spec)) "\"\n" (if (uses-cleartext-traffic spec) " android:usesCleartextTraffic=\"true\"\n" --- a/templates/original-tactics-parts/RulesClient.ss +++ b/templates/original-tactics-parts/RulesClient.ss @@ -10,6 +10,7 @@ (java net URL) (java net URLConnection) (java nio charset Charset) + (java util concurrent locks ReentrantLock) (kotlin text Charsets) (org json JSONObject)) (typed-library (com jerboa originaltactics) @@ -22,11 +23,14 @@ (type Int32) (type JSONObject) (type OutputStream) + (type ReentrantLock) (type Throwable) (type URL) (type URLConnection) (extern (contextCacheDir (context : Context)) : File (kotlin-member-get cacheDir)) + (extern (contextCodeCacheDir (context : Context)) : File + (kotlin-member-get codeCacheDir)) (extern (contextFilesDir (context : Context)) : File (kotlin-member-get filesDir)) (extern (fileAbsolutePath (file : File)) : String @@ -35,6 +39,12 @@ (kotlin-member-call resolve)) (extern (fileMkdirs (file : File)) : Bool (kotlin-member-call mkdirs)) + (extern (fileExists (file : File)) : Bool + (kotlin-member-call exists)) + (extern (fileReadText + (file : File) + (charset : Charset)) : String + (kotlin-member-call readText)) (extern (fileAppendText (file : File) (text : String) @@ -54,6 +64,17 @@ (key : String) (value : String)) : JSONObject (kotlin-member-call put)) + (extern (jsonPutAny + (json : JSONObject) + (key : String) + (value : Any)) : JSONObject + (kotlin-member-call put)) + (extern (jsonNull) : Any + (kotlin-value JSONObject NULL)) + (extern (boolAsAny (value : Bool)) : Any + (kotlin-cast Any)) + (extern (stringAsAny (value : String)) : Any + (kotlin-cast Any)) (extern (jsonText (json : JSONObject)) : String (kotlin-member-call toString)) (extern (urlOpenConnection (url : URL)) : URLConnection @@ -117,15 +138,25 @@ (kotlin-member-call close)) (extern (illegalStateException (message : String)) : Throwable (kotlin-call IllegalStateException)) + (extern (throwableMessage (error : Throwable)) : (Nullable String) + (kotlin-member-get message)) + (extern (lockAcquire (lock : ReentrantLock)) : Unit + (kotlin-member-call lock)) + (extern (lockRelease (lock : ReentrantLock)) : Unit + (kotlin-member-call unlock)) (extern (removeSuffix (text : String) (suffix : String)) : String (kotlin-member-call removeSuffix)) + (extern (stringTakeLast (text : String) (count : Int32)) : String + (kotlin-member-call takeLast)) (extern (embeddedRulesAvailable (cacheDir : String)) : Bool (kotlin-call EmbeddedRulesBridge isAvailable)) (extern (embeddedRulesHandle (body : String)) : String (kotlin-call EmbeddedRulesBridge handleServiceJson)) + (extern (embeddedRulesLastError) : String + (kotlin-call EmbeddedRulesBridge lastError)) (val EMBEDDED_SERVICE_ENDPOINT : String "embedded://sfb-rules/api/service" (modifiers private const)) @@ -135,9 +166,6 @@ (val EMBEDDED_CLIENT_LOG_ENDPOINT : String "embedded://sfb-rules/api/client-log" (modifiers private const)) - (val EXTERNAL_CLIENT_LOG_ENDPOINT : String - "http://10.66.60.2:8799/api/client-log" - (modifiers private const)) (def (httpSuccessCode? (code : Int32)) : Bool (and (>= code (int32 200)) (<= code (int32 299)))) (def (newHttpConnection (text : String)) : HttpURLConnection @@ -176,6 +204,8 @@ (kotlin-call System loadLibrary)) (extern (nativeReady (cacheDir : String)) : Bool (kotlin-external)) + (extern (nativeLastError) : String + (kotlin-external)) (extern (nativeHandleServiceJson (body : String)) : String (kotlin-external)) (var loadError : (Nullable Throwable) @@ -199,12 +229,29 @@ (begin (set! loadError (nullable-some error)) #f))))) + (def (lastError) : String + (if (nullable-null? loadError) + (if loaded + (try + (nativeLastError) + (catch (error : Throwable) + (let ((message (throwableMessage error))) + (if (nullable-null? message) + "native bridge reported unavailable" + (nullable-get message))))) + "native library not loaded") + (let ((message (throwableMessage (nullable-get loadError)))) + (if (nullable-null? message) + "native bridge initialization failed" + (nullable-get message))))) (def (handleServiceJson (body : String)) : String (begin (if (not loaded) (throw (illegalStateException - "Embedded rules bridge is not available: native library not loaded") + (string-append + "Embedded rules bridge is not available: " + (lastError))) Unit) (begin)) (nativeHandleServiceJson body)))) @@ -214,26 +261,65 @@ (modifiers private)) (val fallbackEndpointValue : String fallbackEndpoint (modifiers private)) - (val embeddedAvailable : Bool - (embeddedRulesAvailable - (fileAbsolutePath (contextCacheDir context))) + (var embeddedChecked : Bool #f + (modifiers @Volatile private)) + (var embeddedAvailable : Bool #f + (modifiers @Volatile private)) + (var embeddedError : (Nullable String) + (nullable-none String) + (modifiers @Volatile private)) + (val serviceLock : ReentrantLock (new ReentrantLock) + (modifiers private)) + (val initializationLock : ReentrantLock (new ReentrantLock) (modifiers private)) (def (serviceEndpoint) : String - (if embeddedAvailable - EMBEDDED_SERVICE_ENDPOINT - fallbackEndpointValue)) + EMBEDDED_SERVICE_ENDPOINT) (def (healthEndpoint) : String - (if embeddedAvailable - EMBEDDED_HEALTH_ENDPOINT - (string-append - (removeSuffix - fallbackEndpointValue - "/api/service") - "/health"))) + EMBEDDED_HEALTH_ENDPOINT) (def (clientLogEndpoint) : String - EXTERNAL_CLIENT_LOG_ENDPOINT) + EMBEDDED_CLIENT_LOG_ENDPOINT) (def (modeLabel) : String - (if embeddedAvailable "embedded" "http-fallback")) + (if (not embeddedChecked) + "embedded-pending" + (if embeddedAvailable "embedded" "embedded-error"))) + (def (startupTrace) : JSONObject + (jsonPutString + (jsonPutAny + (jsonPutAny + (jsonPutString + (jsonPutBool + (jsonObject) + "checked?" + embeddedChecked) + "transport" + (modeLabel)) + "available?" + (boolAsAny embeddedAvailable)) + "error" + (if (nullable-null? embeddedError) + (jsonNull) + (stringAsAny (nullable-get embeddedError)))) + "startup-trace" + (nativeStartupTraceText))) + (def (nativeStartupTraceText) : String + (modifiers private) + (try + (let ((trace + (fileResolve + (contextCodeCacheDir contextValue) + "originaltactics-rules-startup.trace"))) + (if (fileExists trace) + (stringTakeLast + (fileReadText trace (utf8Charset)) + (int32 4000)) + "")) + (catch (error : Throwable) + (let ((message (throwableMessage error))) + (string-append + "failed to read native startup trace: " + (if (nullable-null? message) + "unknown error" + (nullable-get message))))))) (def (getHealth (url : String)) : String (if (equal? url EMBEDDED_HEALTH_ENDPOINT) (jsonText @@ -242,7 +328,10 @@ (jsonPutString (jsonPutString (jsonPutString - (jsonPutBool (jsonObject) "ok?" #t) + (jsonPutBool + (jsonObject) + "ok?" + (ensureEmbeddedReady)) "service" "sfb-rules") "engine" @@ -252,17 +341,67 @@ "endpoint" EMBEDDED_SERVICE_ENDPOINT) "client-log-endpoint" - EXTERNAL_CLIENT_LOG_ENDPOINT)) - (httpGet url))) + EMBEDDED_CLIENT_LOG_ENDPOINT)) + (throw + (illegalStateException + (string-append + "External rules server fallback is disabled; requested " + url)) + String))) (def (post (url : String) (body : String) (readTimeoutMillis : Int32)) : String (if (equal? url EMBEDDED_SERVICE_ENDPOINT) - (embeddedRulesHandle body) + (if (not (ensureEmbeddedReady)) + (throw + (illegalStateException + (string-append + "Embedded rules bridge is not ready: " + (if (nullable-null? embeddedError) + "native bridge unavailable" + (nullable-get embeddedError)))) + String) + (runEmbeddedRequest body readTimeoutMillis)) (if (equal? url EMBEDDED_CLIENT_LOG_ENDPOINT) (writeEmbeddedClientLog body) - (httpPost url body readTimeoutMillis)))) + (throw + (illegalStateException + (string-append + "External rules server fallback is disabled; requested " + url)) + String)))) + (def (runEmbeddedRequest + (body : String) + (readTimeoutMillis : Int32)) : String + (modifiers private) + (begin + (lockAcquire serviceLock) + (try-finally + (embeddedRulesHandle body) + (lockRelease serviceLock)))) + (def (ensureEmbeddedReady) : Bool + (modifiers private) + (if embeddedChecked + embeddedAvailable + (begin + (lockAcquire initializationLock) + (try-finally + (begin + (if (not embeddedChecked) + (begin + (set! embeddedAvailable + (embeddedRulesAvailable + (fileAbsolutePath + (contextCodeCacheDir contextValue)))) + (set! embeddedError + (if embeddedAvailable + (nullable-none String) + (nullable-some (embeddedRulesLastError)))) + (set! embeddedChecked #t)) + (begin)) + embeddedAvailable) + (lockRelease initializationLock))))) (def (writeEmbeddedClientLog (body : String)) : String (modifiers private) (let ((dir (fileResolve (contextFilesDir contextValue) "logs"))) new file mode 100644 --- /dev/null +++ b/tests/fixtures/native-packaging-app.ss @@ -0,0 +1,9 @@ +(import (jerboa prelude)) + +(def app + '(android-app + (id "org.jerboa.nativepackaging") + (name "Native Packaging") + (extract-native-libs #t) + (keep-jni-debug-symbol "**/libembedded_program.so") + (screen Main (text "Native Packaging")))) --- a/tests/fixtures/original-tactics-app.ss +++ b/tests/fixtures/original-tactics-app.ss @@ -7,6 +7,8 @@ (compile-sdk 36) (min-sdk 26) (target-sdk 36) + (extract-native-libs #t) + (keep-jni-debug-symbol "**/liboriginaltactics_program.so") (uses-cleartext-traffic #t) (activity-screen-orientation "landscape") (activity-config-changes "keyboardHidden|orientation|screenSize")