CLI tool
cundong/SmartAppUpdates avatar
cundong/SmartAppUpdates

SmartAppUpdates: BSDIFF40 APK Patches in C

APK delta generation and reconstruction for Android

2,650 stars820 forksCApache-2.0

At a glance

What is it?
SmartAppUpdates generates a BSDIFF40 patch from two APKs and rebuilds the newer APK from the exact older one. It is an Android AAR plus a Java/JNI CLI, and it only pays off when your users already hold the byte-identical previous APK.
Who is it for?
Adopt SmartAppUpdates if you ship many Android releases to users who already have the exact previous APK and you control the update path, because the patch only applies to a byte-identical old file. Do not adopt it if you need an HTTP update service, a Windows build host, or a guarantee of smaller downloads; the README states a smaller download is not guaranteed.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 24 days ago.
What is it written in?
Mainly C, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What SmartAppUpdates solves, and for whom

Shipping a full APK on every release wastes bandwidth when most of the file is unchanged. SmartAppUpdates attacks that with a delta: it generates a BSDIFF40 patch from two APKs and reconstructs the new APK from the exact old APK plus that patch. The repository ships three components. `ApkPatchLibrary` is an Android AAR exposing a Java/JNI patch API. `ApkPatchLibraryServer` is a Java/JNI command-line tool for generating and applying patches on macOS and Linux. `ApkPatchLibrarySample` is an offline Android sample that runs the whole flow against a locally supplied APK pair.

The audience is narrow and specific. You need an Android app, a build pipeline on macOS or Linux, and a way to know that the copy of the old APK on the device matches the one used to generate the patch, byte for byte. If your users install through Google Play, Play already handles delivery and this project does not replace it. SmartAppUpdates is for teams that run their own update path or want to study the delta mechanism in isolation.

One expectation to set early: the README states plainly that patch size depends on the differences between the APKs and that a smaller download is not guaranteed. Two releases that touch most of the binary can produce a patch that is not worth the extra reconstruction step.

The BSDIFF40 mechanism and how the CLI isolates it

The core is a BSDIFF40 patch format, the same family used by bsdiff and bspatch, which the repository lists as its topics. The CLI and the Android library share the same patch parser and bzip2 sources, so a patch produced by one can be read by the other. The parser validates patch structure and bounds, and native failures surface as error codes rather than crashes.

The CLI adds a process boundary that the library does not have. Native operations run in worker JVMs with timeouts and temporary-output cleanup. That matters because a malformed patch or an unexpectedly large input should not take down the calling process. Native operations default to a 900-second timeout, and a timeout exits with code 124 after terminating the worker. The CLI reference documents other exit codes and output handling.

On the Android side there is no such isolation. `PatchUtils.patch` returns an `int`, and the caller handles `PatchUtils.ERR_*` codes. The README is explicit that a successful return means reconstruction completed, and that the caller is responsible for validating the resulting APK and initiating installation. That division of responsibility is the honest part of the design: the library rebuilds bytes, it does not decide whether those bytes are safe to install.

Building the AAR and CLI on macOS or Linux

Builds run from the repository root on macOS or Linux. Native Windows builds are not currently supported, and the README says so directly. The toolchain is JDK 17 or 21, Gradle 8.11.1 through the Wrapper, Android Gradle Plugin 8.10.1, Android SDK Platform 36, Android NDK 29.0.14206865, CMake 3.31.6, plus Python 3 and a C compiler for host tools. Configure the SDK through `ANDROID_HOME` or `sdk.dir` in a local, Git-ignored `local.properties` file, and expect the first build to need access to dependency repositories.

These commands build the library and CLI without the sample APKs:

bash
./gradlew :apkPatchLibrary:check :apkPatchLibrary:assembleRelease :server:build

Outputs land in `ApkPatchLibrary/build/outputs/aar/`, at `ApkPatchLibraryServer/build/libs/ApkPatchLibraryServer-2.0-all.jar`, and under `ApkPatchLibraryServer/build/distributions/`. The CLI artifacts include a native library for the build host's OS and JVM architecture, so build separately for each target platform you intend to run on.

Generating and applying your first patch

The CLI is a local tool, not an HTTP update service, so the flow is two commands on your own machine. Use absolute paths for inputs and outputs. The output parent directory must exist, and the output file must not already exist, which means you cannot overwrite a previous patch in place.

bash
java -jar ApkPatchLibraryServer/build/libs/ApkPatchLibraryServer-2.0-all.jar \
  diff /path/to/old.apk /path/to/new.apk /path/to/update.patch

java -jar ApkPatchLibraryServer/build/libs/ApkPatchLibraryServer-2.0-all.jar \
  patch /path/to/old.apk /path/to/rebuilt.apk /path/to/update.patch

The argument order is `diff <old> <new> <patch>` and `patch <old> <new> <patch>`. The CLI accepts any suitable input pair and is not tied to the sample APKs. If the default 900-second timeout is too tight for your largest pair, add `--timeout-seconds 1200` before `diff` or `patch`.

On Android, the sample consumes the library straight from the checkout:

groovy
dependencies {
    implementation project(':apkPatchLibrary')
}

For another project, build the release AAR and add it as a local dependency. The patch call itself is small:

java
import com.cundong.utils.PatchUtils;

// Run on a background thread. Paths must refer to app-accessible files.
int result = PatchUtils.patch(oldApkPath, outputApkPath, patchPath);
if (result == PatchUtils.SUCCESS) {
    // Verify the output against trusted update metadata before installation.
} else {
    // Handle the corresponding PatchUtils.ERR_* code.
}

Run that on a background thread, and keep the paths to app-accessible files. The comment in the example is doing real work: verification and installation are yours to write. The library supports API 21+ and builds `armeabi-v7a`, `arm64-v8a`, and `x86_64` native libraries with 16 KB page alignment.

The byte-identical old APK requirement

This is the constraint that decides whether the project fits. The old APK must match the version used to generate the patch byte for byte. Not the same version code. Not the same signing key. The same bytes. Any re-signing, any zip re-alignment, any repackaging step between the APK you hashed and the APK sitting on the device breaks the reconstruction, and the failure will arrive as a native error code rather than a helpful message about which byte differed.

In practice that means you must control the exact artifact your users received, and you must be able to prove it before you hand them a patch. The offline sample shows what that discipline looks like: preparation verifies both input SHA-256 digests, generates `update.patch`, reconstructs the target with the shared patch implementation, and checks both SHA-256 and byte-for-byte equality, and a failed check stops the build. On device, the sample verifies its inputs, reconstructs `taobao-10.65.20.apk`, checks its digest, package name, and version, and opens the system installer. Installation still requires user confirmation and remains subject to Android's signature and version checks.

The sample also runs on a pinned third-party APK pair, `Apks/淘宝v10.65.10.apk` and `Apks/淘宝v10.65.20.apk`, package `com.taobao.taobao`, version codes 855 and 856. Those APKs are not distributed with the repository, are ignored by Git, and Git LFS is not required. You supply matching files locally to build the sample. The library and CLI build without them, so you can evaluate the mechanism before you find a pair.

Fixture generation, timeouts, and cleanup hazards

The sample's build-time fixture generation is the most operationally interesting part of the repository, and also the easiest to get wrong. Generated resources live under `ApkPatchLibrarySample/app/build/generated/fixtures/current/` and must not be edited by hand. Preparation uses a bounded file lock and publishes complete verified generations atomically, so a failed preparation preserves the previous generation. Cache validation covers inputs, generator code, build configuration, and compiled artifacts.

Both timeouts default to 900 seconds and can be raised:

bash
./gradlew verifyFixtures -PfixtureTimeoutSeconds=1200 -PfixtureLockTimeoutSeconds=1200

The README warns that first-time generation can take several minutes and substantial memory, and that old generations are removed by `:app:clean`, so do not run cleanup concurrently with a build. That last sentence is a real failure mode, not boilerplate. If your CI runs a clean step in parallel with a fixture build, you are racing the atomic publish.

The public-source checks avoid the APK pair entirely:

bash
./gradlew fixtureScriptTest :apkPatchLibrary:check :server:check
python3 scripts/check_repository.py
git diff --check

Full local checks and builds, including the sample, require both APKs. The repository also carries a `VALIDATION.md` and a GitHub Actions workflow that the README says covers native and JNI regressions, fixture-generation infrastructure, and CLI round trips with relocated distributions.

How it differs from bsdiff and bspatch directly

The obvious alternative is calling `bsdiff` and `bspatch` yourself, which is what this project wraps. The difference is not the algorithm. It is everything around it. Plain bsdiff gives you a patch file and no opinion about Android. SmartAppUpdates adds a patch parser that validates structure and bounds, error codes instead of raw native failures, a JNI layer for Android with three ABIs and 16 KB page alignment, and a CLI that runs native work in worker JVMs with timeouts and temporary-output cleanup.

The second alternative is doing nothing and shipping the full APK. That is a legitimate choice here. Because a smaller download is not guaranteed, and because reconstruction costs CPU time and memory on the device, a team whose release diffs are large may get a worse experience from a delta than from a plain download. The README does not promise a size win, and you should not assume one.

The third comparison is against a hosted update service. SmartAppUpdates is not one. The README calls `ApkPatchLibraryServer` a local command-line tool and explicitly says it is not an HTTP update service. If you need delivery, version targeting, and rollout control, this project gives you the patch and nothing else.

Maintenance, licensing, and upgrade cost

The repository is not archived, and the last push was on 2026-09-06. The default branch is `master`, and there are no retrieved releases, so the CLI JAR name `ApkPatchLibraryServer-2.0-all.jar` is currently the version marker you would pin against rather than a published tag.

The licence is Apache-2.0, and the repository carries a `LICENSE`, a `NOTICE`, a `THIRD_PARTY_NOTICES.md`, and a `licenses/` directory. Apache-2.0 permits commercial use and modification and includes a patent grant, but the usual obligations apply: keep the licence and notice files, and state significant changes. I am not a lawyer and this is not legal advice. The part worth checking with your own counsel is the third-party notices file, because the project bundles bzip2 sources and ships native libraries, and those carry their own terms.

Upgrade cost is dominated by the toolchain, not the API. The patch call is one method returning an `int`. The build pins Android Gradle Plugin 8.10.1, Android SDK Platform 36, Android NDK 29.0.14206865, and CMake 3.31.6, all driven by the Gradle 8.11.1 Wrapper. Moving those forward means rebuilding the native libraries for `armeabi-v7a`, `arm64-v8a`, and `x86_64`, and re-verifying that the shared patch parser and bzip2 sources still produce byte-identical reconstructions. The fixture pipeline is what catches a regression there, which is why the atomic publish and the digest checks are worth keeping intact.

Editorial conclusion

Adopt SmartAppUpdates if you ship many Android releases to users who already have the exact previous APK and you control the update path, because the patch only applies to a byte-identical old file. Do not adopt it if you need an HTTP update service, a Windows build host, or a guarantee of smaller downloads; the README states a smaller download is not guaranteed. Before committing, verify the API 21+ AAR path on a real device, confirm the 900-second default timeout fits your largest APK pair, and check that your distribution flow can supply the old APK byte for byte.

Frequently asked questions

What exactly does SmartAppUpdates do?

It generates a BSDIFF40 patch from two APKs and reconstructs the new APK from the exact old APK plus that patch. It ships an Android AAR, a Java/JNI CLI for macOS and Linux, and an offline Android sample.

Does SmartAppUpdates always produce a smaller download?

No. The README states that patch size depends on the differences between the APKs and that a smaller download is not guaranteed.

Can I build SmartAppUpdates on Windows?

No. The README says to run commands from the repository root on macOS or Linux and that native Windows builds are not currently supported.

Is SmartAppUpdates an HTTP update service?

No. The README describes `ApkPatchLibraryServer` as a local command-line tool and states that it is not an HTTP update service. It generates and applies patches; delivery is up to you.

What happens if the old APK does not match the one used to generate the patch?

The old APK must match the version used to generate the patch byte for byte. A mismatch breaks reconstruction, and the library reports native failures through `PatchUtils.ERR_*` codes rather than a byte-level diff.

Official sources

  1. cundong/SmartAppUpdates on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/cundong-smartappupdates.svg)](https://hysenlabs.com/projects/cundong-smartappupdates)