Publish an Android App to F-Droid

I published SwatchIt, a native Android app I have been working on, to the F-Droid Android Store 🎉. Now you can:

Get it on F-Droid

All apps published through F-Droid are free and open source software (FOSS) and F-Droid itself is free and open source as well.

If you don’t already have F-Droid installed, you should go ahead and install it, it is not as complicated as it looks even on certified Android devices (for now.. but that is a topic for another post).

Publishing to F-Droid comes with some efforts, mainly since every app that you download through F-Droid must be built by F-Droid as well, so it is not enough to upload an APK.

Below I outline the steps that I needed to take in order to get the app published. I structured them into things that needed to be done, things that are nice to have and problems I ran into.

You can find the source code to my app SwatchIt on Codeberg.

Things I needed to do

Before you start

The app I have published is a Native Android App that I developed with Android Studio. I am running Ubuntu 24.04 on my local machine. As already mentioned the app you want to publish must be Open Source, so the source code must be publicly available in a version control repository (supported repository types).

The official Submitting to F-Droid Quick Start Guide is a great starting point and source of information.

Licensing

Every FOSS application also needs Licensing information. F-Droid expects a FOSS license file with the Name LICENSE in the root directory of your repository. I consulted the Wikipedia overview of FOSS licenses and picked GNU GPL 3.0 or later. I copied the license text into the LICENSE file.

I got a comment on the Merge Request to add copyright information as well. The License suggests to add copyright and license information to every source file. I added the following block to the README instead:

Copyright (C) 2025  Katharina Damschen

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.

That was accepted as sufficient.

Metadata

The F-Droid store creates a page for each app showing informational texts, screenshots, changelogs and other metadata to the user. This is why you need to add this metadata to your repository.

F-Droid supports two different ways to structure your metadata: Triple-T and Fastlane, both are described in the Metadata guidelines.

I went with the Fastlane structure, because that is how it is done in the F-Droid client repo. I placed the files under metadata/en-US,en-US is the default and fallback value and you need to provide this folder.

You can see my files and structure in my repository on Codeberg.

App Version

Applications need to be versioned in order to be able to update them. Decide on a versioning scheme and add the versionName as well as the versionCode to your project’s build.gradle file. You can read about how to do that in the Android docs.

I use semantic versioning as my versioning scheme. As this was the first version and the app is not stable yet I chose 0.1.0 as the first versionName and 1 as the versionCode. Here is what the defaultConfig block in my project’s build.gradle file looks like:

android{
    defaultConfig {
        applicationId = "net.damschen.swatchit"
        minSdk = 21
        targetSdk = 35
        versionCode = 1
        versionName = "0.1.0"
    }
}

Tag and push

All the necessary code changes in your app have been done now! Commit and tag the commit with v%versionName. So in my case I tagged with v0.1.0.

Make sure to push the commit and the tag (git push --tags).

Create a reproducible build

Enabling reproducible builds is encouraged by F-Droid, especially for new apps. You build and sign your APK with your own key, F-Droid builds the same app unsigned, copies your signature to their build, and if it verifies correctly (proving identical binaries), they publish your originally-signed APK, making installations independent of F-Droid.

You can also chose to let F-Droid sign your APK, but the problem is that a device will not recognize the same app signed with different keys as the same app. If you decide to use your own signing key later, the users will have to uninstall and reinstall your app with the new signing key. That is why reproducible builds are encouraged, so you will be able to distribute your app via different channels, independent of F-Droid’s signing key

Creating two binaries that are exactly the same is not that easy. It’s easier the more your build process resembles F-Droids build process, but more valuable the more the processes differ.

First you need to create a signing key that you store in a keystore. I created a new keystore along with a signing key in a subdirectory of my home directory (~/.keystore/) and made the key valid for 1000 years:

keytool -genkey -v -keystore YOUR_KEYSTORE_PATH -alias YOUR_KEY_ALIAS -keyalg RSA -keysize 2048 -validity 365000

I didn’t find much information about which signing algorithms are supported, both the Android and F-Droid Documentation use RSA, so I used it as well.

The Android Documentation also mentions that “Your key should be valid for at least 25 years, so you can sign app updates with the same key through the lifespan of your app.”

You should choose a long validity for the key, because Android won’t accept your app as the same when signed with a new key.

When I had the key I built the APK via the CLI (make sure that you are building the tagged version of the code. You cannot have any uncommitted changes or the build will not be reproducible):

 ./gradlew assembleRelease

This creates an unsigned APK in app/build/outputs/apk/release. I renamed the APK to swatchit-0.1.0.apk:

mv app/build/outputs/apk/release/app-release-unsigned.apk YOUR_APK_PATH/swatchit-%versionName.apk

and then signed it with the help of the apksigner tool:

apksigner sign --ks-key-alias YOUR_KEY_ALIAS --ks YOUR_KEYSTORE_PATH YOUR_APK_PATH/swatchit-%versionName.apk

Now you need to publish your signed APK somewhere were F-Droid can access it.

I created a release on Codeberg and uploaded the signed APK manually. I haven’t migrated any pipelines to Codeberg yet, but ideally this process would be automated.

Add a manifest to the fdroiddata repository

Now that you have prepared your app, you need to inform F-Droid about its existence. You do that by adding a metadata file to the fdroiddata repository. Create a GitLab account or use your existing one and fork the fdroiddata repository.

Download the repository:

git clone https://gitlab.com/YOUR_FORKS_ADRESS

switch to the root folder and create a branch named after your applicationId:

cd fdroiddata
git switch -c your.app.id

Copy the manifest template file, make sure the file ending is .yml:

cp templates/build-gradle.yml metadata/your.app.id.yml

The values that can be used in the manifest file are documented in F-Droid’s Metadata Reference.

I had a hard time picking a category and went with Internet, and it got changed to Sports & Health by the F-Droid team. F-Droid has actively decided to keep the number of Categories small, the downside to this is that the category for your app might not fit very well.

Here is the file:

Categories:
  - Sports & Health
License: GPL-3.0-or-later
SourceCode: https://codeberg.org/katharinad/SwatchIt
IssueTracker: https://codeberg.org/katharinad/SwatchIt

AutoName: SwatchIt

RepoType: git
Repo: https://codeberg.org/katharinad/SwatchIt
Binaries: https://codeberg.org/katharinad/SwatchIt/releases/download/v%v/swatchit-%v.apk

Builds:
  - versionName: 0.1.0
    versionCode: 1
    commit: v0.1.0
    subdir: app
    gradle:
      - yes

AllowedAPKSigningKeys: 785ce16a7ed68cdf6bd5a0181d0fea6fdf7b8a3813fd573b3748747740583351

AutoUpdateMode: Version
UpdateCheckMode: Tags
CurrentVersion: 0.1.0
CurrentVersionCode: 1

The template does not contain any optional properties, so I added those that I needed for reproducible builds, automatic updates and convenience.

To enable reproducible builds I had to add those two lines:

Binaries: https://codeberg.org/katharinad/SwatchIt/releases/download/v%v/swatchit-%v.apk
AllowedAPKSigningKeys: 785ce16a7ed68cdf6bd5a0181d0fea6fdf7b8a3813fd573b3748747740583351

The Binaries path is the path to the release, with %v getting automatically replaced with the versionName. I added my public signing key to AllowedAPKSigningKeys, after extracting it with this command:

apksigner verify --print-certs YOUR_APK_PATH | grep SHA-256

I also enabled the AutoUpdateMode by Version. As I understand it there’s a daily job running looking for updates on all apps. So all I have to do in the future is to push a new tag and create a new release with the name defined in the Binaries path to publish a new version.

Commit and create a Merge Request

When you are done with filling in the metadata commit your changes to your fork with the message: ‘New App: your.app.id’. There is a Gitlab CI pipeline that checks if F-Droid can build your app and if it can reproduce it along with linting checks. If the pipeline passes create a Merge Request and wait ⏳!

Once your config file is merged it will take a few days until the app shows up in F-Droid. I got this link with additional information after the merge.

You can also take a look at my Merge Request. It took about 3 weeks until the code was being reviewed and 3 days after the merge the app was downloadable in F-Droid! It was even shown on the homescreen in the ‘Latest’ tab!

Nice to Have

Here are some things that I did that weren’t totally necessary but nice to have or know about!

Build locally with the fdroidserver image

The official Submitting to F-Droid Quick Start Guide makes it sound like you have to check if you can build your app locally in a container running fdroidserver, but all the necessary checks are also being run by the fdroiddata CI pipeline.

Running the tasks locally is faster and the linting task fixes your manifest file locally, so I personally would use the Image again next time. It is also kind to not overuse F-Droids CI server.

If you want to have a go, here are the steps I needed to take to get fdroidserver running locally:

Clone the repo:

git clone https://gitlab.com/fdroid/fdroidserver

Install docker, if you haven’t already.

Start the container and map your fdroiddata and fdroidserver repo folders into the container:

sudo docker run --rm -itu vagrant --entrypoint /bin/bash \
  -v FULL_PATH_TO_FDROIDDATA:/build:z \
  -v FULL_PATH_TO_FDROIDSERVER:/home/vagrant/fdroidserver:Z \
  registry.gitlab.com/fdroid/fdroidserver:buildserver

Note that the paths need to be full paths, or you will get permission errors.

Then in the container (following the steps in the guide):

. /etc/profile
export PATH="$fdroidserver:$PATH" PYTHONPATH="$fdroidserver"
export JAVA_HOME=$(java -XshowSettings:properties -version 2>&1 > /dev/null | grep 'java.home' | awk -F'=' '{print $2}' | tr -d ' ')
cd /build
fdroid readmeta
fdroid rewritemeta your.app.id

Now you need to set the serverwebroot environment variable, otherwise you’ll see an error (though the actual tasks might still succeed). This is how it is done in the fdroiddata CI pipeline but not mentioned in the guide:

export serverwebroot=/tmp

I then got:

rsync: [Receiver] mkdir "/tmp/repo/status" failed: No such file or directory

So I created the repo directory:

mkdir /tmp/repo

Then I could resume with the commands in the guide:

fdroid checkupdates --allow-dirty your.app.id
fdroid lint your.app.id
fdroid build your.app.id

Now you can fix any errors you got, or go on and push your changes and create a Merge Request.

Build and sign the APK with Android Studio

You can use Android Studio instead of doing the signing manually as I described above. Android Studio can create a signed APK for you, it can even create a keystore and a signing key.

To avoid the binary representation of build dependencies to be created and signed with a public Google Key when building with AndroidStudio or Intellij you need to add this block to your projects build.gradle file:

android {
    dependenciesInfo {
        includeInApk = false
        includeInBundle = false
    }
}

I saw that this was commented on in some Merge Requests with links to two issues on GitLab (one and two).

You can start your build in Android Studio by clicking Build -> Generate Signed App Bundle or APK then choose the APK-option to obtain a signed APK.

Then you can go on and upload the APK as a release and continue with adding a manifest to the fdroiddata repo

Things that went wrong

Here are some problems I ran into and explanations on how I fixed those.

Dependency verification failed for lint task

You won’t run into this if you don’t use Dependecy verification.

The fdroid build task failed, because the gradle lint task (:app:lintVitalAnalyzeRelease) required some dependencies that were not yet listed in my verification-metadata.xml file. I got the same problem locally when running ./gradlew lint. To fix this, I reran the bootstrapping with the lint task:

./gradlew --write-verification-metadata pgp,sha256 --export-keys lint

I found information about this behavior in the gradle documentation.

This problem was not F-Droid specific, I just had never run the lint task before and wasn’t aware that it might need different dependencies, so it took my a while to understand the issue. If you know, you know!

Build not reproducible

After I activated reproducible builds the build failed, because it was not reproducible 🙃.

F-droid lists common causes for unreproducible builds and suggests fixes.

There are ways of diffing the APKs described there too. I started with the most common and easiest measures without diffing: I made sure that I tagged before I build and that there were no uncommitted changes. I also build the APK and signed it with the command line instead of with Android Studio.

The next time I tried the build was reproducible, but I cannot make a statement on what it was of these three things that fixed the problem.

I left the dependeciesInfo block that I added when I built and signed with Android Studio, although technically it is not needed when building releases with gradle via the CLI.

In the Merge Request the building bot left an annotation that there is a version mismatch between the gradle version and the gradle wrapper version in my project, which might explain the differences as well.

F-Droid Build server does not support AGP >= 8.12.x

You won’t run into this if yu use AGP version 8.11 or lower

The annotation of the bot, that my gradle and gradle wrapper versions mismatched triggered an immediate urge in me to just update EVERYTHING! I hadn’t done that in a while because I wanted to focus on getting the app released. But now I had finally created a Merge Request. What could possibly go wrong?

So I updated the gradle wrapper and gradle and while I was at it, I updated the Android Gradle Plugin (AGP) to version 8.14.3.

The reviewer of the Merge Request then commented that I will need to downgrade AGP to the latest 8.11 version, because right now the build server hardware cannot handle higher AGP versions. You can find the issue on GitLab. So I downgraded to 8.11.3. I kept the new gradle and gradle wrapper versions, though.

F-Droid Build Container and Server do not have Java 21 available

You won’t run into this if you use a Java version 17

I had hoped my Merge Request would get approved without me having to create a new release. But the APG version and the missing Copyright notice made the reviewer ask for a new release.

At least I could benefit from this documentation immediately 🙃! When I tried building with F-Droid locally in the container I got a new (to me) error:

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:compileReleaseJavaWithJavac'.
> Java compilation initialization error
    error: invalid source release: 21

Of course I had also updated the Java version from 17 to 21. I updated everything! And the container (as well as the CI Server) does not support that yet.

Others fortunately had the same problem and one grep later I had found their solution: I needed to add some sudo commands for installing Java 21 before building, and making it available for the build to the configuration file in fdroiddata.

Here are the updated parts:

Builds:
  - versionName: 0.2.0
    versionCode: 2
    commit: v0.2.0
    subdir: app
    sudo:
      - echo "deb https://deb.debian.org/debian trixie main" > /etc/apt/sources.list.d/trixie.list
      - apt-get update
      - apt-get install -y -t trixie openjdk-21-jdk-headless
      - update-alternatives --auto java
    gradle:
      - yes

AllowedAPKSigningKeys: 785ce16a7ed68cdf6bd5a0181d0fea6fdf7b8a3813fd573b3748747740583351

AutoUpdateMode: Version
UpdateCheckMode: Tags
CurrentVersion: 0.2.0
CurrentVersionCode: 2

Note, however, that this did not work in the container running locally on my machine. It ignores the sudo commands:

WARNING: net.damschen.swatchit:0.2.0 runs this on the buildserver with sudo:
        ['echo "deb https://deb.debian.org/debian trixie main" > /etc/apt/sources.list.d/trixie.list', 'apt-get update', 'apt-get install -y -t trixie openjdk-21-jdk-headless', 'update-alternatives --auto java']
These commands were skipped because fdroid build is not running on a dedicated build server.

So I pushed and checked the build pipeline and the build succeeded. 🎉

Conclusion

This post got longer than expected because I describe the details and caveats I ran into, but I hope this helps others to publish to F-Droid. You can do this, too!

The folks there have been very kind and helpful in every Merge Requests I looked at (including my own). If someone missed something, that was not a problem at all.

Despite the few ‘problems’ I encountered, I feel like the process was straight forward overall. And I learned a ton!

Thanks everyone at F-Droid 💚.