Skip to content

Preparing Recipes

This document describes how to set up your build recipes. Start with simple things: for the „average“ Java/Kotlin app, the first two sections should get you going. They are followed by some more special cases.

The recipe file

The easiest way to get started, is probably to grab an existing recipe as „template“ and adjust it to the app to be build. Most of the fields are self explaining, others need some background. Here's an example template:

---
repository: https://github.com/Owner/App_Name.git
updates: releases
versions:
  - tag: v0.1.2
    apks:
      - apk_pattern: app-release\.apk
        apk_url: https://github.com/Owner/App_Name/releases/download/v0.1.2/app-release.apk
        build:
          - sed -r '/signingConfigs.releaseConfig/d' -i app/build.gradle
          - ### when adding extra_packages from upstream specs, watch the log for 'is already the newest version' to see if they are really needed (remove this line before running the recipe)
          - chmod +x gradlew
          - ./gradlew assembleRelease
          - find . -name '*.apk'
          - mv app/build/outputs/apk/release/*unsigned.apk /outputs/unsigned.apk
        build_cpus:
        build_home_dir: /build
        build_repo_dir: /build/repo
        build_timeout:
        build_user: build
        provisioning:
          android_home: /opt/sdk
          build_tools:
          cmake:
          cmdline_tools:
            version: '12.0'
            url: https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
            sha256: 2d2d50857e4eb553af5a6dc3ad507a17adf43d115264b1afc116f95c92e5e258
          extra_packages: []
          image: debian:bookworm-slim
          jdk: openjdk-17-jdk-headless
          ndk:
          platform:
          platform_tools:
          tools:
          verify_gradle_wrapper: true

You can look up all fields in schemas/recipe.json of course, which is also used to lint recipes. Not all of them are mandatory, and some of them you rarely ever need to change (e.g. the cmdline_tools). Instead of going through each of them here, let's focus on those which usually need adjustment, and leave the others for the special cases they will be used for.

  • repository: the Git repository the source code resides in. Should end in .git.
  • updates: will usually be set to releases, which means update checks will be performed on the latest release (not pre-release, those are ignored with releases). While you usually will want to stick to stable releases, you have other options here:
    • disabled you will use when an app is no longer maintained (e.g. its repository has been archived), but you still want to keep it
    • manual if you want to exclude the app from automated update checks
    • tags:.* can be used to specify a tag name pattern. This will also work with pre-releases. APKs will still be looked at the tag/release (see apk_pattern)
    • you can prefix the update mode with checkonly: if you don't want automated updates to pick up new versions, but just report them in the logs
  • versions: in here will go the version specific things. Each version will have its own tag. The value you have to provide for tag: must match the tag name in the repository.
  • apk_pattern: a regular expression matching the name of the APK file to fetch from the tag. If there are multiple APKs, make sure to match only the one you need: this is the APK used to compare your build against. There can be multiple APK items in a tag block, useful e.g. if you want to confirm multiple architectures/build-flavors.
  • apk_url: where to fetch the upstream APK from
  • build: here goes the build recipe. Above template has a comment in you should of course remove (it's just a hint to the builder's operator when setting up recipes), and the find is intended for the first test runs when you are not yet sure how the output file might be named.
  • build_home_dir:, build_repo_dir:, build_user: you „normally“ do not need to touch these – unless there are e.g. embedded paths in library files you need to match (e.g. with Flutter or Rust components).
  • provisioning: is where to define how your image should be prepared. Most of the fields here you only need to touch in special cases. The most relevant „for starters“ are image: and jdk:, which are explained in the next section. A special flag is verify_gradle_wrapper:, which you in most cases will leave at true to ensure to only use approved gradle binaries. Exceptions to this are e.g. Flutter recipes – and cases where you have to use your own gradle wrapper (details on that further down).
  • extra_packages: is a list of OS packages a build depends on, but which are not provided by default. An example for that would be curl with Flutter apps.

Chosing JDK and image

Our current defaults are to use OpenJDK 17 on Debian bookworm – unless it is clear that something else would be needed. There are different pointers helping with the selection.

JDK version

The build.gradle files usually have hints for this:

  • jvmTarget: „Target version of the generated JVM bytecode“. The linked documentation does not explain, but it most likely means „generate byte code compatible to that Java version“ – which implies you must have at least that version available, as a lower one would not know how to do that
  • sourceCompatibility: code needs at least this Java version to compile
  • targetCompatibility: resulting app needs at least that Java version to run (cannot run with a lower JRE)
  • Github workflows provide different SDKs. Most projects use temurin, which seems to be very close to OpenJDK. adopt seems to be problematic, zulu we had issues with, too (though it's not yet clear if they came from zulu – often it works fine). Then there's oracle, which should be OpenJDK (not sure yet). This needs some more evaluation to be used for recommendations (statistics maybe, how often which is used and we can RB, and how often RB fails?).
  • Java release dates and Gradle/JDK compatibility Matrix (see the distributionUrl in gradle/wrapper/gradle-wrapper.properties):
Java Version class file format released minGradle
11 55 2018-09-25 5.0
17 61 2021-09-14 7.3
21 65 2023-09-19 8.5
25 69 2025-09-16 9.1

Decide for an image

  • Debian:
    • Trixie for JDK-21: debian:trixie-slim
    • Bookworm for JDK-17: debian:bookworm-slim
    • Bullseye for JDK-11 (in extra_packages): debian:bullseye-slim
  • Ubuntu:
    • 22.04 for JDK-21: ubuntu:jammy
    • 24.04: ubuntu:latest in Github actions currently (3/2025) means ubuntu:noble, so this might be worth trying if the defaults fail

An example for JDK 11, as this needs some special adjustments (due to the fact that Gradle itself still needs JDK 17 or newer):

        build:
          - JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 ./gradlew assembleRelease
          ...
        provisioning:
          ...
          extra_packages:
            - openjdk-11-jdk-headless
          image: debian:bullseye-slim
          jdk: openjdk-17-jdk-headless

Gradle wrapper

Most repositories will come prepared with a gradle-wrapper.properties file and the corresponding ./gradlew wrapper script ready, which in above recipe is reflected by ./gradlew assembleRelease. But sometimes your builds might „crash“ before they even start, stating that the gradle version is unknown, or the gradle hash does not match. Of course it would be easy to simply set verify_gradle_wrapper: false in such cases – but for security reasons, that's obviously not advised. And it wouldn't help in cases where the gradle wrapper is missing, or even just parts of it (e.g. the gradlew script).

For such cases, Fay has developed gradlew.py, which we continue maintaining, so you can use like this:

          - git clone https://codeberg.org/IzzyOnDroid/gradlew.py.git
          - gradlew.py/gradlew.py --version 8.4 -v assembleRelease

The --version <version> can be omitted when just the gradlew script was missing, gradlew.py then looks up the gradle version in gradle-wrapper.properties. And it brings its own verifier along; so when using gradlew.py, you even should set verify_gradle_wrapper: false.

Note

as Fay can no longer maintain her gradlew.py, it will probably fail with newer Gradle versions (8.14+). You can use our maintained fork instead.

Recipe specialities

Whenever a project has set up some CI to build their app, you should take a look at that. Not only it usually reveals what system (image) they build on and which JDK they use, but it might also reveal special build steps needed, as well as their order and dependencies. With projects hosted at Github, you usually find the corresponding definitions in the .github/workflows directory. Projects on Codeberg might have them defined in their .woodpecker.yml.

Signing

To confirm a build as reproducible, we will need to build an unsigned APK. But more often than not, instructions in the build.gradle/build.gradle.kts enforce signing. Our build would fail then, as we of course lack the credentials and the keystore. This is why in above example recipe you see the line holding sed -r '/signingConfigs.releaseConfig/d', to remove the signing instruction. This instruction varies between projects, so you need to check what to match exactly (to not accidentally remove to much and e.g. also break the syntax of the gradle file). You usually find the corresponding line in the buildTypes section of the app/build.gradle[.kts] file under release.

To ease your work with this, you can point developers to our chapter on the Signing Config, advising them to make signing optional – depending on whether the keystore exists or not.

Build flavors

If a project offers different build variants (e.g. one for PlayStore, and one Foss), you need to match that in your recipe. We cannot give in-depth instructions on that here, but basically for a foss flavor, you'd change the build instruction to ./gradlew assembleFossFelease – and need to adjust the output file name as well then (the find in the example recipe above takes care to list you resulting APKs to pick from).