Updating .so files
Jugg can update already generated native library / .so files. For C/C++ modules managed by Gradle, a source change first runs the native build task for the current variant. Native libraries produced by a Flutter add-to-app project enter the same incremental artifact flow. Jugg then chooses an overlay or an APK update according to the “SO hot update” setting.
Supported scope
| Scenario | Current support | User-visible result |
|---|---|---|
Update an existing .so under an ABI directory in the project | Supported | Uses the target APK overlay or updates, re-signs, and installs the APK according to the SO hot update setting |
| Change C/C++ source managed by Gradle | Supported | Runs the native build task for the current variant, then updates the generated .so |
| Share one C/C++ source across multiple Native modules | Supported | Runs all distinct native tasks for matching modules once, then updates the .so files produced by each module |
Flutter Profile/Release produces app.so or native assets | Supported | Runs the Flutter native output task for the current variant, then deploys the resulting native libraries according to the setting |
| Flutter Debug produces assets only, without native libraries | Supported | Updates flutter_assets only and does not require native output; an empty native directory still lets the compilation succeed, and no APK is repackaged, re-signed, or installed |
| Update native libraries for multiple ABIs in the same run | Supported according to target APK ownership | Each target APK receives only its own native libraries |
Update an existing .so of at least 256 MiB | Conditionally supported | Does not load the complete file into the IDE heap; APK updates stream-replace the same-path baseline entry, while SO hot update prefers delta transfer and sends the full file when no baseline is available |
Update an .so with “SO hot update” enabled | Ordinary .so files are supported | Delivers them in the target APK overlay alongside DEX, resources, and assets from the same run; skips APK re-signing and installation, then loads them after an app restart |
Delete an .so | Does not produce a removal result | The installed APK continues containing the old native library |
Change CMakeLists.txt, project *.cmake, Android.mk, or Application.mk | Supported | Runs the native task for the current variant and updates that module's external build information before the same Gradle invocation finishes; new .so files enter the overlay or update the APK according to the setting |
| Change NDK, ABI, native source sets, or packaging rules | Not treated as a source incremental input | Uses a complete Gradle build to refresh the project model and APK baseline |
Change packaging.jniLibs.keepDebugSymbols | Supported | Keeps debug symbols for those .so files according to the app packaging semantics without running the app native build or strip task |
Trigger and result
C/C++ source changes
-> Find every Native module that shares the source
-> Run all distinct native Gradle tasks for the current variant in one invocation
-> Read the APK owner strip configuration and produce stripped .so files in this invocation directory
Flutter Dart source changes
-> Run the Flutter native output task for the current variant
-> Collect .so files under <abi> from that task's own native output, an archive or a directory
An existing .so in the project changes
-> Determine the target path from its ABI and APK ownership
-> Setting enabled: deliver it to the target APK overlay and restart the app
-> Setting disabled: write it into the target APK, re-sign, and installAfter restart, the app loads the native library from the committed overlay or updated APK. Gradle, CMake, and NDK remain responsible for producing .so files from C/C++ source. Jugg starts the corresponding task after detecting a source change and passes the new artifacts into the existing native library deployment flow.
How Debug Dart code takes effect
Flutter Debug/JIT Dart code is not a native library but an asset such as assets/flutter_assets/kernel_blob.bin. Jugg delivers it as an asset overlay and never writes it back into the APK, so changing Dart in Debug never repackages, re-signs, or installs an APK.
The Flutter Android embedding extracts flutter_assets into the app private directory app_flutter and uses app_flutter/res_timestamp-<versionCode>-<lastUpdateTime> to decide whether it has to extract again. An overlay update does not change the APK lastUpdateTime, so after all overlay slices succeed Jugg deletes that timestamp and fully restarts the app, letting Flutter re-extract from the overlay that already took effect. The deployment type for such a run is Hot Fix.
After the overlay takes effect, the Jugg runtime also refreshes the AssetManager retained by Flutter engines so subsequent asset reads use the current overlay. See Assets and native library internals for the mechanism.
Profile/Release use the AOT artifact libapp.so, which is a native library and follows the setting to use an overlay or APK update.
Boundaries
- With “SO hot update” enabled,
System.loadLibrary()on the app ClassLoader can find the updated library in the overlay first.System.load()still opens the old library when given a path in the original installation directory or APK; it can load the update when given a patch path returned byfindLibrary(). Nativedlopen()/android_dlopen_ext()and ELFDT_NEEDEDdependencies do not use this Java search path and may continue to load old libraries. Libraries already loaded in the process are not replaced. - The direct file-change entry point recognizes only existing
.sofiles under the project directory whose parent directory isarmeabi,armeabi-v7a,arm64-v8a,x86, orx86_64. - The C/C++ source entry requires Android Gradle configuration with a CMake or ndk-build file and a discoverable native task for the current variant. Jugg does not watch generated files under
.cxx,.externalNativeBuild, or Gradlebuilddirectories. A project may declare Gradle extrajuggExternalBuildPrerequisitesso matching file changes run a codegen task in the same invocation before native merge; Kotlin/Java files in the declared output directories then enter Jugg's incremental source compile only when their size or timestamp changed during that codegen task. Projects without that extra keep the current behavior. - Each detected C/C++ source change runs the native task. For each target APK, Jugg compares an output
.sowith the last successfully deployed version first, or with the baseline APK when no deployment record exists. It does not resend an unchanged library, and it still delivers a hot-updated library that returns to its baseline content. These checks do not skip native compilation. - What gets deployed is the
.sostripped with the app packaging semantics, not the unstripped file in the module intermediate directory. Jugg reads thestrip<Variant>DebugSymbolsconfiguration of the APK owner (base app or dynamic feature) inside the collector process and reproduces the AGP single-file strip behavior without executing that strip task. The invocation runs only the module merge tasks selected by the C/C++ changes; it does not additionally run other native merge tasks from the APK owner. When the strip tool is missing or returns a non-zero exit code, the library is packaged as is, following the AGP contract. - Only a NativeLib of at least 256 MiB (268,435,456 bytes) uses the file-backed path. Ordinary
.sofiles, Dex files, resources, and assets keep the existing in-memory path. Jugg rechecks that a file-backed source still exists and has the same size and timestamp before writing it into an APK or pushing it to a device; a change fails the current round explicitly. - Updating a large
.soinside an APK requires an entry at the same path in the baseline APK and inherits that entry'sSTOREDorDEFLATEDcompression method; Jugg does not force a large DEFLATED.soto become STORED. The round fails and preserves the original APK if an entry reaches the classic ZIP 4 GiB boundary, the baseline entry is missing, disk space is insufficient, or zipalign, signing, verification, or installation rejects the result. CRC calculation, compression, and temporary APKs increase runtime and disk usage for large files. - File size never enables “SO hot update” automatically. With the setting enabled, Jugg prefers delta transfer for a large
.so, reconstructs and verifies the complete new file on the device, and places it in the same target APK overlay as ordinary.sofiles. If sandbox access or the push fails, the round fails explicitly. With the setting disabled, it uses the APK update path. - The first large
.soupdate, or a missing local or device baseline, uses full transfer and prints an info message that it may take longer. A mismatched baseline, an unavailable delta tool, or a patch no smaller than the full file also uses full transfer. A recoverable patch reconstruction or content verification failure falls back at most once. Device disconnection, insufficient storage, or cancellation ends the round. Debug logs include patch size and stage timings such as generation, transfer, and device reconstruction/verification. The timing summary is reported once at info level after successful large SO deployment, from transfer preparation through publication and cleanup, excluding compilation and app restart. Delta transfer still reconstructs the complete file and requires an app restart; no fixed completion time is promised. - A native library module can be built without the APK owner being configured, for example when Gradle configuration on demand is enabled, so the owner strip task is not readable in that invocation. A complete Gradle build therefore caches the owner strip configuration and a copy of each strip executable under
build/jugg/classpath/native_strip, and the collector uses that cache first. The cache is matched by the exact module root and variant, and the collected configuration is only reused when its recorded tool is still executable. A missing, malformed or unusable cache falls back to reading the owner task once; if the owner is not configured and no cache is available, the round fails and asks for a complete Gradle build instead of deploying an unstripped library. Because the tool copies travel with the cache, a CI baseline stays usable on another worker whose NDK is at a different path — keep the wholenative_stripdirectory when copying a baseline. - When one physical source matches multiple Native modules, every matching task must be supported and succeed, and every module output must be collectable. Otherwise that source falls back or fails as a whole instead of marking a partially successful result as compiled.
- Each detected Dart source change runs the Flutter native output task for the current variant. Jugg reads only the native output that task declares and resolves the
.sofiles under each ABI from it, whichever form that output takes. It does not recursively guess native output from Flutter intermediate directories, and it does not assemble artifact locations from fixed paths. - If Jugg recognizes a Flutter source root but cannot find the compile task, the assets output directory, or native output metadata, it falls back to a full Gradle build. If the native output cannot be read, or an archive contains unsafe or duplicate native entries, the current compilation fails. A build mode that legitimately produces no native library, such as Debug, succeeds as long as the assets output is valid.
- If Jugg recognizes a C/C++ source root but cannot find its task or output metadata, it falls back to a full Gradle build. The current compilation fails if the external task fails or the declared output directory is missing or unreadable. If the task succeeds and the output directory is accessible but contains no valid
.so, the round succeeds without native deployment output. - CMake and ndk-build configuration files inside the project (
CMakeLists.txt,*.cmake,Android.mk,Application.mk) are configuration inputs of the current variant's native build. After you change them, Jugg runs the existing native task and collects and merges only that module's latest external build information in the same Gradle invocation instead of asynchronously refreshing the full project model. Newly added out-of-project shared sources, assembly files, and include roots immediately enter the subsequent watch scope. Configuration the task cannot cover, such as NDK, ABI, native source sets, or packaging rules, still needs a full Gradle build to refresh the APK baseline. - C/C++ inputs trigger recursively by directory. Configuration roots, parent directories of metadata sources, and include roots are merged into the smallest practical set of watched directories. Non-excluded files in those directories may cause a small number of false triggers. A broad shared directory may also trigger multiple modules, including one with a large output; Gradle and Ninja use up-to-date checks and real dependencies to decide whether native work actually runs.
.cxx,.externalNativeBuild, and build outputs remain excluded. - When a recognized C/C++ source or native configuration file is deleted, Jugg falls back to a full Gradle build. If a native artifact collected in the previous round is no longer produced after an external build succeeds, the round still succeeds without a removal result, and the old
.soremains until a full Gradle build refreshes the APK baseline. - When not all external inputs in one round can be resolved, such as a multi-module project where only one module configures a native build, the whole round falls back to a full Gradle build instead of building only the recognizable part.
- Deleting an
.sodoes not produce data that removes the file from the APK and does not fail incremental compilation by itself. The installed APK continues containing the old native library. Run a full Gradle build only when the deletion must actually take effect. - Multi-APK projects update native libraries according to target APK ownership instead of writing the same native library into every APK by default.
- After a project reopens, Jugg restores previously updated
.sofiles from local records of successful deployments. If device recovery reinstalls the app, Jugg replays those files for their target APK when “SO hot update” is enabled; when disabled, it uses the content of the reinstalled APK. Older.sofiles absent from local records cannot be replayed and must be deployed again. - With “SO hot update” disabled,
.sofiles still use APK updates; missing or invalid signing configuration makes that update fail. Switching the setting from enabled to disabled makes the next Run perform a full Gradle build and install an APK containing the latest.sofiles. Enabling the setting still schedules app data clearing and reinstallation.