Skip to content

RB Hints for Developers

If you are the developer of an app (to be) listed at IzzyOnDroid, and you want it to succeed as RB, this page is for you. It will give you some hints on precautions you can do to increase the chances for successful RBs.

First Basic Rule

If you want your app to succeed with Reproducible Builds, here's the

First Basic Rule:

§1: Always build the APK from a clean tree at exactly the commit your release-tag will point to!

Always create a commit with all changes before you start your release build, do not build first and then create the commit after it succeeds. Nor fix some little things after the commit and build with a „dirty tree“, or even after a different commit that's not the one the tag points to. All these will lead to RB failing – sometimes requiring some manual fix up on our end, sometimes not even be fixable at all („dirty tree“ or „unpublished commit“).

A note in this context: by default, your APKs will contain a file named META-INF/version-control-info.textproto, which looks like this:

repositories {
  system: GIT
  local_root_path: "$PROJECT_DIR"
  revision: "e065b1d98d15c18dff2a8d3eb8b124bb31a4e7e8"
}

This is quite helpful in diagnosing failed ones, as it tells which commit (revision) was really the last one when you built your APK. And should it once be the only difference between the APKs, it's rather easily fixed in the build recipe (in our case here, even automatically by rbtlog itself). So we'd suggest to keep it enabled. There are very few cases where you might need to disable it (e.g. when using a different VCS locally, and pushing your code to a public git repository via a bridge). So if it is unavoidable, you can do that in your build.gradle:

    buildTypes {
        release {
           vcsInfo.include false
        }
    }

Speaking of „unpublished commit“ a last time: pointing the tag to a commit in a „private branch“ is not the wisest thing to do either ;)

Further make sure you sign your APKs yourself with a release key (not with a debug key). Letting Google sign your APKs will usually break RB (especially when done via app bundles).

Clean Builds

This overlaps a bit with the first basic rule above, but not entirely. During the process of RB testing, your app will be built from the commit the release tag points to. Cleanly, on a fresh container. So there's no cache from previous runs in this container. If you build from a clean tree, that increases the chances your build and the one of the verification builder will correspond. In Android studio this means for example you should precede your release build with clean project.

Even using "clean project", Android Studio can still sometimes cause non-deterministic results. The "cleanest" build would be e.g. in a fresh container, but building with these options on the command line should be pretty close:

./gradlew clean assembleRelease --no-build-cache --no-configuration-cache --no-daemon

Fixed dependency versions

If you use latest.release (or snapshot versions) instead of a specific release version, which version you get will depend on when you build, which makes the build unreproducible.

No funny build-time-generated IDs

Everything that depends on build-time (timestamps or formatted dates) will make sure RB fails. Timestamps make the biggest source of reproducibility issues. So e.g. generating the versionCode from the current timestamp at build is a bad idea. Same goes for other things like „build IDs“ or UUIDs, and also anything with nondeterministic output like listing locales or files without sorting.

One classic example here is AboutLibraries, which embeds the build timestamp into its res/M7.json. Which can be avoided by

// build.gradle.kts
aboutLibraries {
    excludeFields = arrayOf("generated")
}
// build.gradle
aboutLibraries {
    excludeFields = ["generated"]
}

It can also be avoided by simply updating AboutLibraries to version 11.5.0 or higher, where this generated date is disabled by default.

Another classic are build-ids generated by NDKs. To avoid those, disable insertion of build-id entirely by passing --build-id=none to the linker. For cmake ≥ 3.13, add this to CMakeLists.txt:

add_link_options("-Wl,--build-id=none")

or, if that fails:

set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--build-id=none")
target_link_options(my_target PUBLIC "-Wl,--build-id=none")

For older cmake, add this to gradle files:

android {
    defaultConfig {
        externalNativeBuild.cmake {
          cFlags "-DCMAKE_SHARED_LINKER_FLAGS=-Wl,<linker args>"
        }
    }
}

where for this case, <linker args> would be --build-id=none. Note that disabling build-ids might fail with older NDKs (e.g. 21), but should work with recent NDKs (28 and up at least).

JDK versions

Please don't use „exotic JDKs“ – and also avoid those which are not LTS (like e.g. versions 18-20 and 22). Our builders currently use OpenJDK-17 by default. Where needed, we can relatively easy support OpenJDK-11 or OpenJDK-21, though. Of course supported versions will change over time, with newer Java versions being released. We will see to have this document kept up-to-date.

API keys

Should your app need API keys to work, and you integrate those with your APKs, the verification server must be able to do the same – or again, RB will fail as the APKs differ. This means you cannot just keep them in some „secrets“, but must provide them with the code, so a rebuild succeeds.

Before you complain here that then they'd be visible in the open: yes, true. But they could also be extracted from your APKs if one would want to access them. It just takes a little more work. To make them less visible in your source code, you could e.g. store them base64 encoded. If that does not appeal to you either: reach out to us so we find a solution together.

Compressing images

Better avoid using PNG Crush/Crunch during the build, as that usually is undeterministic – meaning it gives different results on each run. The same applies to PNGs generated from vector graphics. As those images are part of your source repository anyway, better compress them before checking them in, so compression has not to be taken care of during builds at all. You can use tools such as optipng, pngcrush or pngquant for that on PNGs (if needed for JPEGs, there's e.g. jpegoptim). To disable crunching, have this in your build.gradle:

android {
    // disable PNG crunching:
    aaptOptions {
        cruncherEnabled = false
    }
    // disable generating PNGs from vector drawables:
    defaultConfig {
        vectorDrawables.generatedDensities = []
    }
}

Signing Config

To confirm your builds as reproducible, we need to build an unsigned APK from your source. If your build.gradle[.kts] enforces signing, this adds some „complexity“ we have to work around (removing/disabling that in your build.gradle[.kts]). A much cleaner approach is if you make signing optional – e.g. only enable it if the keystore is present (which you hopefully won't check in with your sources). Example implementation:

For build.gradle and build.gradle.kts:

    if (keystorePropertiesFile.exists()) {
        keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
    }

    signingConfigs {
        release {
            if (keystorePropertiesFile.exists()) {
                storeFile = file("$rootDir/keystore.jks")
                storePassword = keystoreProperties["storePassword"]
                keyAlias = keystoreProperties["keyAlias"]
                keyPassword = keystoreProperties["keyPassword"]
            } else {
                println "Keystore properties file not found. No signing configuration will be applied."
            }
        }
    }

then in buildTypes, for build.gradle.kts:

    buildTypes {
        release {
            signingConfig = signingConfigs.findByName("release")
        }
    }

for build.gradle instead:

    buildTypes {
        release {
            if (keystorePropertiesFile.exists()) {
                signingConfig signingConfigs.release
            }
        }
    }

Additional resources

The above should cover the most frequent cases. But are a lot more. If you want to dive in deeper, you can use e.g. the following resources: