diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..0d7ebe6 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,114 @@ +name: Build Linux and Android + +on: + push: + branches: + - "**" + tags: + - "**" + pull_request: + workflow_dispatch: + +env: + QT_VERSION: "6.11.1" + ANDROID_NDK_VERSION: "27.2.12479018" + BUILD_IMAGE: "evcharger-ci:qt-6.11.1-${{ gitea.sha }}" + BUILD_CACHE_IMAGE: "evcharger-ci:qt-6.11.1-cache" + +defaults: + run: + shell: bash + +jobs: + build: + # Register a trusted host-mode runner with this label. It only needs + # Docker, Bash, Git, and Node.js for the actions used below. + runs-on: linux_amd64 + timeout-minutes: 180 + + steps: + - name: Check out source and submodules + uses: https://gitea.com/actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 1 + + - name: Restore compiler and Gradle caches + uses: https://gitea.com/actions/cache@v4 + with: + path: | + .ccache + .gradle/caches + .gradle/wrapper + key: evcharger-${{ runner.os }}-qt-6.11.1-${{ gitea.ref_name }} + restore-keys: | + evcharger-${{ runner.os }}-qt-6.11.1- + + - name: Build the pinned Qt toolchain image + run: | + set -Eeuo pipefail + docker image inspect "$BUILD_CACHE_IMAGE" >/dev/null 2>&1 || true + docker build --pull \ + --cache-from "$BUILD_CACHE_IMAGE" \ + --tag "$BUILD_IMAGE" \ + . + docker tag "$BUILD_IMAGE" "$BUILD_CACHE_IMAGE" + + - name: Build Linux development executable + run: | + set -Eeuo pipefail + docker run --rm \ + --user "$(id -u):$(id -g)" \ + --env HOME=/tmp/ci-home \ + --env CI_WORKSPACE=/workspace \ + --env CCACHE_DIR=/workspace/.ccache \ + --volume "$GITHUB_WORKSPACE:/workspace" \ + --workdir /workspace \ + "$BUILD_IMAGE" \ + bash ci/build-linux.sh + + - name: Build Android APK and Play bundle + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_KEYSTORE_ALIAS }} + ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_KEYSTORE_STORE_PASS }} + ANDROID_KEYSTORE_KEY_PASS: ${{ secrets.ANDROID_KEYSTORE_KEY_PASS }} + run: | + set -Eeuo pipefail + docker run --rm \ + --user "$(id -u):$(id -g)" \ + --env HOME=/tmp/ci-home \ + --env CI_WORKSPACE=/workspace \ + --env CI_RUN_NUMBER="$GITHUB_RUN_NUMBER" \ + --env CI_REF_NAME="$GITHUB_REF_NAME" \ + --env CI_REF_TYPE="$GITHUB_REF_TYPE" \ + --env CCACHE_DIR=/workspace/.ccache \ + --env GRADLE_USER_HOME=/workspace/.gradle \ + --env ANDROID_KEYSTORE_BASE64 \ + --env ANDROID_KEYSTORE_ALIAS \ + --env ANDROID_KEYSTORE_STORE_PASS \ + --env ANDROID_KEYSTORE_KEY_PASS \ + --volume "$GITHUB_WORKSPACE:/workspace" \ + --workdir /workspace \ + "$BUILD_IMAGE" \ + bash ci/build-android.sh + + - name: Upload Linux build + uses: https://gitea.com/actions/upload-artifact@v4 + with: + name: evcharger-linux + path: dist/linux/ + if-no-files-found: error + retention-days: 30 + + - name: Upload Android APK and AAB + uses: https://gitea.com/actions/upload-artifact@v4 + with: + name: evcharger-android + path: dist/android/ + if-no-files-found: error + retention-days: 30 + + - name: Remove the commit-specific image tag + if: always() + run: docker image rm "$BUILD_IMAGE" >/dev/null 2>&1 || true diff --git a/.gitignore b/.gitignore index bf3241b..c35be0e 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,18 @@ compile_commands.json *creator.user* *_qmlcache.qrc -build/ \ No newline at end of file +build/ +/build-ci/ +/dist/ +/.ccache/ +/.gradle/ + +# The generic *build-* rule above would otherwise hide these CI entrypoints. +!/ci/ +!/ci/build-linux.sh +!/ci/build-android.sh + +# Android release signing material must only live in CI/CD secrets. +/android_release.keystore +/*.keystore +/*.jks diff --git a/Dockerfile b/Dockerfile index a538d61..eb19eeb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,76 +1,101 @@ -FROM archlinux +FROM ubuntu:24.04 -RUN pacman -Syu --noconfirm sudo git jdk11-openjdk base-devel ninja \ - && rm -R /var/cache/pacman \ - && git clone https://github.com/KDAB/android_openssl /opt/android-sdk/android_openssl \ - && echo '%sudo ALL=(ALL:ALL) NOPASSWD: ALL' >> /etc/sudoers \ - && useradd -m feedc0de \ - && groupadd sudo \ - && usermod -a -G sudo feedc0de \ - && sed -i 's/ debug / !debug /g' /etc/makepkg.conf +ARG QT_VERSION=6.11.1 +ARG AQTINSTALL_VERSION=3.3.0 +ARG ANDROID_COMMAND_LINE_TOOLS_VERSION=15859902 +ARG ANDROID_COMMAND_LINE_TOOLS_SHA256=4e4c464f145a7512b57d088ac6c278c03c9eea610886b35a5e0804e74eedf583 +ARG ANDROID_NDK_VERSION=27.2.12479018 +ARG ANDROID_OPENSSL_COMMIT=32ebe304ff064a9affb699b2185af78e3494f49a -USER feedc0de +ENV DEBIAN_FRONTEND=noninteractive \ + QT_ROOT=/opt/Qt \ + ANDROID_SDK_ROOT=/opt/android-sdk \ + ANDROID_HOME=/opt/android-sdk \ + ANDROID_NDK_ROOT=/opt/android-sdk/ndk/${ANDROID_NDK_VERSION} \ + JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 \ + PATH=/opt/aqt/bin:/opt/android-sdk/cmdline-tools/latest/bin:/opt/android-sdk/platform-tools:${PATH} -RUN cd \ - && git clone https://aur.archlinux.org/trizen.git \ - && cd trizen \ - && makepkg -si --noconfirm \ - && cd .. \ - && sudo rm -R trizen /var/cache/pacman +RUN apt-get update \ + && apt-get install --yes --no-install-recommends \ + ca-certificates \ + ccache \ + cmake \ + curl \ + file \ + git \ + libavahi-client-dev \ + libdbus-1-3 \ + libegl1 \ + libfontconfig1 \ + libgl1 \ + libx11-6 \ + libx11-xcb1 \ + libxcb-cursor0 \ + libxcb-glx0 \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-randr0 \ + libxcb-render-util0 \ + libxcb-shape0 \ + libxcb-shm0 \ + libxcb-sync1 \ + libxcb-util1 \ + libxcb-xfixes0 \ + libxcb-xinerama0 \ + libxcb-xkb1 \ + libxcb1 \ + libxkbcommon-x11-0 \ + ninja-build \ + openjdk-21-jdk-headless \ + p7zip-full \ + pkg-config \ + python3 \ + python3-venv \ + unzip \ + && rm -rf /var/lib/apt/lists/* -RUN trizen -S --noconfirm android-environment \ - && sudo rm -R ~/.cache /var/cache/pacman +RUN python3 -m venv /opt/aqt \ + && /opt/aqt/bin/pip install --no-cache-dir "aqtinstall==${AQTINSTALL_VERSION}" \ + && aqt install-qt linux desktop "${QT_VERSION}" linux_gcc_64 \ + -m qtcharts qtwebsockets -O "${QT_ROOT}" \ + && aqt install-qt linux android "${QT_VERSION}" android_arm64_v8a \ + -m qtcharts qtwebsockets -O "${QT_ROOT}" \ + && aqt install-qt linux android "${QT_VERSION}" android_armv7 \ + -m qtcharts qtwebsockets -O "${QT_ROOT}" -RUN trizen -S --noconfirm android-sdk-build-tools \ - && sudo rm -R ~/.cache /var/cache/pacman +RUN mkdir -p "${ANDROID_SDK_ROOT}/cmdline-tools" \ + && curl --fail --location --retry 5 \ + --output /tmp/android-command-line-tools.zip \ + "https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_COMMAND_LINE_TOOLS_VERSION}_latest.zip" \ + && echo "${ANDROID_COMMAND_LINE_TOOLS_SHA256} /tmp/android-command-line-tools.zip" | sha256sum --check \ + && unzip -q /tmp/android-command-line-tools.zip -d "${ANDROID_SDK_ROOT}/cmdline-tools" \ + && mv "${ANDROID_SDK_ROOT}/cmdline-tools/cmdline-tools" "${ANDROID_SDK_ROOT}/cmdline-tools/latest" \ + && rm /tmp/android-command-line-tools.zip \ + && yes | sdkmanager --licenses >/dev/null \ + && sdkmanager \ + "platform-tools" \ + "platforms;android-36" \ + "build-tools;36.0.0" \ + "ndk;${ANDROID_NDK_VERSION}" -RUN trizen -S --noconfirm android-sdk \ - && sudo rm -R ~/.cache /var/cache/pacman +RUN git init "${ANDROID_SDK_ROOT}/android_openssl" \ + && git -C "${ANDROID_SDK_ROOT}/android_openssl" remote add origin https://github.com/KDAB/android_openssl.git \ + && git -C "${ANDROID_SDK_ROOT}/android_openssl" fetch --depth 1 origin "${ANDROID_OPENSSL_COMMIT}" \ + && git -C "${ANDROID_SDK_ROOT}/android_openssl" checkout --detach FETCH_HEAD \ + && rm -rf "${ANDROID_SDK_ROOT}/android_openssl/.git" -RUN trizen -S --noconfirm android-ndk \ - && sudo rm -R ~/.cache /var/cache/pacman +# Build dependencies needed by applications using the prebuilt desktop Qt. +# Keep these in a separate layer so changes here do not invalidate the large +# Qt and Android SDK downloads above. +RUN apt-get update \ + && apt-get install --yes --no-install-recommends \ + build-essential \ + libgl1-mesa-dev \ + libglib2.0-0t64 \ + && rm -rf /var/lib/apt/lists/* -ENV ANDROID_NDK_ROOT=/opt/android-ndk +ENV LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 -RUN trizen -S --noconfirm android-pkg-config \ - && sudo rm -R ~/.cache /var/cache/pacman - -RUN trizen -S --noconfirm android-cmake \ - && sudo rm -R ~/.cache /var/cache/pacman - -RUN trizen -S --noconfirm android-sdk-platform-tools \ - && sudo rm -R ~/.cache /var/cache/pacman - -RUN trizen -S --noconfirm android-platform \ - && sudo rm -R ~/.cache /var/cache/pacman - -RUN trizen -S --noconfirm android-aarch64-libjpeg-turbo \ - && sudo rm -R ~/.cache /var/cache/pacman - -RUN trizen -S --noconfirm android-configure \ - && sudo rm -R ~/.cache /var/cache/pacman - -RUN trizen -S --noconfirm android-aarch64-zlib \ - && sudo rm -R ~/.cache /var/cache/pacman - -RUN trizen -S --noconfirm android-aarch64-libpng \ - && sudo rm -R ~/.cache /var/cache/pacman - -RUN gpg2 --recv-keys D894E2CE8B3D79F5 \ - && trizen -S --noconfirm android-aarch64-openssl \ - && sudo rm -R ~/.cache /var/cache/pacman - -RUN trizen -S --noconfirm android-aarch64-qt6-base \ - && sudo rm -R ~/.cache /var/cache/pacman - -RUN trizen -S --noconfirm android-aarch64-qt6-shadertools \ - && sudo rm -R ~/.cache /var/cache/pacman - -RUN trizen -S --noconfirm android-aarch64-qt6-declarative \ - && sudo rm -R ~/.cache /var/cache/pacman - -RUN trizen -S --noconfirm android-aarch64-qt6-websockets \ - && sudo rm -R ~/.cache /var/cache/pacman - -RUN trizen -S --noconfirm android-aarch64-qt6-tools \ - && sudo rm -R ~/.cache /var/cache/pacman +WORKDIR /workspace diff --git a/README.md b/README.md index 4cec62f..80fe88d 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,15 @@ This app aims to be functional first and pretty second. * You can also select any color you like in all color pickers * Has an actual start / stop button that doesnt interfere with any other settings * Has an actual helpful cellular information and settings page + +## Building + +The application is built with CMake and Qt 6.11.1. A reproducible build +container and Gitea Actions workflow are included. The workflow compiles a Linux +development build and produces an Android APK plus an Android App Bundle for +Google Play. Release credentials make both Android artifacts signed; pipelines +without credentials produce unsigned validation packages. + +See [docs/gitea-actions.md](docs/gitea-actions.md) for runner requirements, signing +secret setup, artifact downloads, local container builds, and release +versioning. diff --git a/ci/build-android.sh b/ci/build-android.sh new file mode 100755 index 0000000..77a04ad --- /dev/null +++ b/ci/build-android.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +readonly project_dir="${CI_WORKSPACE:-${GITHUB_WORKSPACE:-$(pwd)}}" +readonly build_dir="${project_dir}/build-ci/android" +readonly output_dir="${project_dir}/dist/android" +readonly qt_root="${QT_ROOT:-/opt/Qt}" +readonly qt_version="${QT_VERSION:-6.11.1}" +readonly android_ndk_version="${ANDROID_NDK_VERSION:-27.2.12479018}" +readonly run_number="${CI_RUN_NUMBER:-${GITHUB_RUN_NUMBER:-1}}" +readonly ref_name="${CI_REF_NAME:-${GITHUB_REF_NAME:-}}" +readonly ref_type="${CI_REF_TYPE:-${GITHUB_REF_TYPE:-}}" +readonly version_code="${ANDROID_VERSION_CODE:-${run_number}}" + +if [[ -n "${ANDROID_VERSION_NAME:-}" ]]; then + version_name="${ANDROID_VERSION_NAME}" +elif [[ "${ref_type}" == tag && -n "${ref_name}" ]]; then + version_name="${ref_name#v}" +else + version_name="${PROJECT_VERSION:-0.1.0}-ci.${run_number}" +fi +readonly version_name + +sign_android=OFF +keystore_path="" +if [[ -n "${ANDROID_KEYSTORE_BASE64:-}" ]]; then + keystore_path="${build_dir}/release.keystore" + mkdir -p "${build_dir}" + printf '%s' "${ANDROID_KEYSTORE_BASE64}" | base64 --decode > "${keystore_path}" + trap 'rm -f -- "${keystore_path}"' EXIT +elif [[ -n "${ANDROID_KEYSTORE_FILE:-}" ]]; then + keystore_path="${ANDROID_KEYSTORE_FILE}" +fi + +if [[ -n "${keystore_path}" ]]; then + : "${ANDROID_KEYSTORE_ALIAS:?ANDROID_KEYSTORE_ALIAS is required when a keystore is provided}" + : "${ANDROID_KEYSTORE_STORE_PASS:?ANDROID_KEYSTORE_STORE_PASS is required when a keystore is provided}" + : "${ANDROID_KEYSTORE_KEY_PASS:?ANDROID_KEYSTORE_KEY_PASS is required when a keystore is provided}" + export QT_ANDROID_KEYSTORE_PATH="${keystore_path}" + export QT_ANDROID_KEYSTORE_ALIAS="${ANDROID_KEYSTORE_ALIAS}" + export QT_ANDROID_KEYSTORE_STORE_PASS="${ANDROID_KEYSTORE_STORE_PASS}" + export QT_ANDROID_KEYSTORE_KEY_PASS="${ANDROID_KEYSTORE_KEY_PASS}" + sign_android=ON +else + echo "No Android keystore configured; producing unsigned CI packages." +fi + +"${qt_root}/${qt_version}/android_arm64_v8a/bin/qt-cmake" \ + --fresh \ + -S "${project_dir}" \ + -B "${build_dir}" \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DQT_HOST_PATH="${qt_root}/${qt_version}/gcc_64" \ + -DANDROID_SDK_ROOT="${ANDROID_SDK_ROOT}" \ + -DANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${android_ndk_version}" \ + -DQT_ANDROID_ABIS="arm64-v8a;armeabi-v7a" \ + -DEVCHARGER_ANDROID_VERSION_CODE="${version_code}" \ + -DEVCHARGER_ANDROID_VERSION_NAME="${version_name}" \ + -DQT_ANDROID_SIGN_APK="${sign_android}" \ + -DQT_ANDROID_SIGN_AAB="${sign_android}" + +# Both package targets use the same android-build staging directory. Running +# them in one parallel Ninja invocation makes androiddeployqt race with itself. +cmake --build "${build_dir}" --parallel +cmake --build "${build_dir}" --parallel --target apk +cmake --build "${build_dir}" --parallel --target aab + +mkdir -p "${output_dir}" + +if [[ "${sign_android}" == ON ]]; then + apk_path="$(find "${build_dir}" -type f -path '*/outputs/apk/release/*.apk' ! -name '*unsigned*' -print -quit)" +else + apk_path="$(find "${build_dir}" -type f -path '*/outputs/apk/release/*.apk' -print -quit)" +fi +aab_path="$(find "${build_dir}" -type f -path '*/outputs/bundle/release/*.aab' -print -quit)" + +: "${apk_path:?The Android build did not produce an APK}" +: "${aab_path:?The Android build did not produce an AAB}" + +cp "${apk_path}" "${output_dir}/evcharger-${version_name}-universal.apk" +cp "${aab_path}" "${output_dir}/evcharger-${version_name}.aab" + +readonly output_apk="evcharger-${version_name}-universal.apk" +readonly output_aab="evcharger-${version_name}.aab" + +for abi in arm64-v8a armeabi-v7a; do + unzip -Z1 "${output_dir}/${output_apk}" "lib/${abi}/*" >/dev/null +done + +if [[ "${sign_android}" == ON ]]; then + "${ANDROID_SDK_ROOT}/build-tools/36.0.0/apksigner" verify --verbose --print-certs "${output_dir}/${output_apk}" + jarsigner -verify "${output_dir}/${output_aab}" +fi + +( + cd "${output_dir}" + sha256sum "${output_apk}" "${output_aab}" > SHA256SUMS +) diff --git a/ci/build-linux.sh b/ci/build-linux.sh new file mode 100755 index 0000000..6b294b7 --- /dev/null +++ b/ci/build-linux.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +readonly project_dir="${CI_WORKSPACE:-${GITHUB_WORKSPACE:-$(pwd)}}" +readonly build_dir="${project_dir}/build-ci/linux" +readonly output_dir="${project_dir}/dist/linux" +readonly qt_root="${QT_ROOT:-/opt/Qt}" +readonly qt_version="${QT_VERSION:-6.11.1}" + +"${qt_root}/${qt_version}/gcc_64/bin/qt-cmake" \ + --fresh \ + -S "${project_dir}" \ + -B "${build_dir}" \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_COMPILE_WARNING_AS_ERROR=OFF + +cmake --build "${build_dir}" --parallel + +mkdir -p "${output_dir}" +cp "${build_dir}/evcharger-app/evcharger-app" "${output_dir}/evcharger-app" +file "${output_dir}/evcharger-app" diff --git a/docs/gitea-actions.md b/docs/gitea-actions.md new file mode 100644 index 0000000..e59fd33 --- /dev/null +++ b/docs/gitea-actions.md @@ -0,0 +1,152 @@ +# Gitea Actions builds + +The workflow in `.gitea/workflows/build.yml` builds the pinned toolchain image, +then compiles the Linux and Android packages. Its toolchain is: + +- Qt 6.11.1 from Qt's public binary repository, installed with `aqtinstall`; +- Ubuntu 24.04 and JDK 21; +- Android API and Build Tools 36; +- Android NDK 27.2.12479018; +- a pinned KDAB Android OpenSSL revision; +- `arm64-v8a` and `armeabi-v7a` device ABIs. + +The runner's Docker daemon retains the large image layers. The first run must +download Qt and the Android SDK, but ordinary later runs reuse that local cache. +No Gitea Container Registry credentials are required. A runner replacement or +Docker cache cleanup causes the toolchain to be downloaded again. + +Qt's current Gradle template is intentionally not copied into the source tree. +`androiddeployqt` overlays the custom manifest and resources from +`evcharger-app/android` onto the template supplied by Qt 6.11.1. This keeps Qt, +Gradle, and the Android Gradle Plugin in sync. + +## Runner requirements + +Enable Actions in the repository settings and register a trusted Gitea Runner +with the workflow label `linux_amd64`. Use host mode because the workflow itself +starts build containers: + +```yaml +runner: + labels: + - "linux_amd64:host" +``` + +The runner host needs Docker, Bash, Git, and Node.js. Its user must be allowed +to access the Docker daemon. Keep this runner restricted to repositories you +trust: Docker daemon access is effectively host-level access. + +For example, after downloading `act_runner`, obtain a repository runner token +from **Settings > Actions > Runners**, then run: + +```sh +./act_runner register --no-interactive \ + --instance https://your-gitea.example \ + --token YOUR_REGISTRATION_TOKEN \ + --name evcharger-builder \ + --labels linux_amd64:host + +./act_runner daemon +``` + +The workflow timeout is three hours because the uncached first image build is +large. The toolchain image is cached independently from the ccache and Gradle +caches stored by Gitea Actions. + +## Signing secrets + +Without a keystore, the workflow deliberately produces unsigned validation +packages. This allows pull requests to test Android compilation without release +credentials. + +In **Settings > Actions > Secrets**, add these repository secrets: + +| Secret | Value | +| --- | --- | +| `ANDROID_KEYSTORE_BASE64` | Release keystore encoded as one base64 line | +| `ANDROID_KEYSTORE_ALIAS` | Private-key alias in the keystore | +| `ANDROID_KEYSTORE_STORE_PASS` | Keystore password | +| `ANDROID_KEYSTORE_KEY_PASS` | Private-key password | + +Encode the keystore locally with: + +```sh +base64 -w0 android_release.keystore +``` + +Never commit the keystore. Root-level `.keystore` and `.jks` files are ignored. +The workflow's decoded temporary copy is deleted when the Android script exits. + +### Check an uncertain password and alias safely + +`keytool` is included in a JDK. This command prompts for the store password and +lists all aliases; the password is not placed in shell history or the process +list: + +```sh +keytool -list -v -keystore android_release.keystore +``` + +That only proves the store password. To verify the selected alias and its +private-key password without modifying the keystore, sign a disposable JAR: + +```sh +probe_dir="$(mktemp -d)" +trap 'rm -rf -- "$probe_dir"' EXIT +printf 'keystore test\n' > "$probe_dir/probe.txt" +jar --create --file "$probe_dir/probe.jar" -C "$probe_dir" probe.txt +jarsigner -keystore android_release.keystore "$probe_dir/probe.jar" YOUR_ALIAS +jarsigner -verify "$probe_dir/probe.jar" +``` + +`jarsigner` prompts first for the store password and, when different, for the +private-key password. A successful signing followed by `jar verified` confirms +all three values. Do not use `-storepass` or `-keypass` on an interactive shell, +because command-line arguments can leak through history and process listings. + +## Artifacts and Android versions + +Each successful workflow exposes two downloads in the Gitea Actions run: + +```text +evcharger-linux/ + evcharger-app + +evcharger-android/ + evcharger--universal.apk + evcharger-.aab + SHA256SUMS +``` + +Artifacts are retained for 30 days. A signed universal APK is suitable for +direct download and sideloading; an unsigned one is only a validation artifact. +Upload the signed AAB to Google Play, which produces device-specific APKs. + +The Android `versionCode` defaults to the monotonically increasing Gitea Actions +run number. Tags become the `versionName` after an optional leading `v` is +removed; other builds use `0.1.0-ci.`. Override these with workflow +environment values `ANDROID_VERSION_CODE` and `ANDROID_VERSION_NAME` if needed. + +## Local container builds + +```sh +docker build -t evcharger-build:qt-6.11.1 . + +docker run --rm -v "$PWD:/workspace" -w /workspace \ + evcharger-build:qt-6.11.1 bash ci/build-linux.sh + +docker run --rm -v "$PWD:/workspace" -w /workspace \ + evcharger-build:qt-6.11.1 bash ci/build-android.sh +``` + +Pass the signing variables through `--env` or an environment file for a signed +local Android build. Do not add secrets to the Dockerfile or image. + +## Qt open-source licensing note + +The public Qt binaries do not require a commercial account. Distribution must +still follow every linked module's license. Qt Charts is GPLv3-only for +open-source users (or commercially licensed), so distributing this application +with Qt Charts requires GPLv3-compatible distribution of the combined program +and corresponding source. Review and document that licensing choice before +publishing the APK/AAB, or use a commercial Qt license. diff --git a/evcharger-app/CMakeLists.txt b/evcharger-app/CMakeLists.txt index e6281e1..f62b817 100644 --- a/evcharger-app/CMakeLists.txt +++ b/evcharger-app/CMakeLists.txt @@ -23,9 +23,29 @@ qt6_add_translations(evcharger-app ) if (ANDROID) - set_property(TARGET evcharger-app APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android) + set(EVCHARGER_ANDROID_VERSION_CODE "1" CACHE STRING "Monotonically increasing Android version code") + set(EVCHARGER_ANDROID_VERSION_NAME "0.1.0" CACHE STRING "Human-readable Android version") + + set_property(TARGET evcharger-app APPEND PROPERTY + QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android + ) include(${ANDROID_SDK_ROOT}/android_openssl/CMakeLists.txt) - set_target_properties(evcharger-app PROPERTIES QT_ANDROID_EXTRA_LIBS "${ANDROID_EXTRA_LIBS}") + set_target_properties(evcharger-app PROPERTIES + QT_ANDROID_EXTRA_LIBS "${ANDROID_EXTRA_LIBS}" + QT_ANDROID_PACKAGE_NAME "ninja.brunner.evcharger" + QT_ANDROID_MIN_SDK_VERSION 28 + QT_ANDROID_COMPILE_SDK_VERSION 36 + QT_ANDROID_TARGET_SDK_VERSION 36 + QT_ANDROID_VERSION_CODE "${EVCHARGER_ANDROID_VERSION_CODE}" + QT_ANDROID_VERSION_NAME "${EVCHARGER_ANDROID_VERSION_NAME}" + ) + + # Custom cache variables are not automatically inherited by Qt's + # secondary ABI builds. + list(APPEND QT_ANDROID_MULTI_ABI_FORWARD_VARS + EVCHARGER_ANDROID_VERSION_CODE + EVCHARGER_ANDROID_VERSION_NAME + ) endif() qt_add_qml_module(evcharger-app diff --git a/evcharger-app/android/AndroidManifest.xml b/evcharger-app/android/AndroidManifest.xml index 78eff48..444af63 100644 --- a/evcharger-app/android/AndroidManifest.xml +++ b/evcharger-app/android/AndroidManifest.xml @@ -14,23 +14,6 @@ - - - - - - - - - - - - - - - - - diff --git a/evcharger-app/android/build.gradle b/evcharger-app/android/build.gradle deleted file mode 100644 index 4a93923..0000000 --- a/evcharger-app/android/build.gradle +++ /dev/null @@ -1,84 +0,0 @@ -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:8.4.0' - } -} - -repositories { - google() - mavenCentral() -} - -apply plugin: qtGradlePluginType - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation 'androidx.core:core:1.13.1' -} - -android { - /******************************************************* - * The following variables: - * - androidBuildToolsVersion, - * - androidCompileSdkVersion - * - qtAndroidDir - holds the path to qt android files - * needed to build any Qt application - * on Android. - * - qtGradlePluginType - whether to build an app or a library - * - * are defined in gradle.properties file. This file is - * updated by QtCreator and androiddeployqt tools. - * Changing them manually might break the compilation! - *******************************************************/ - - namespace androidPackageName - compileSdkVersion androidCompileSdkVersion - buildToolsVersion androidBuildToolsVersion - ndkVersion androidNdkVersion - - // Extract native libraries from the APK - packagingOptions.jniLibs.useLegacyPackaging true - - sourceSets { - main { - manifest.srcFile 'AndroidManifest.xml' - java.srcDirs = [qtAndroidDir + '/src', 'src', 'java'] - aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl'] - res.srcDirs = [qtAndroidDir + '/res', 'res'] - resources.srcDirs = ['resources'] - renderscript.srcDirs = ['src'] - assets.srcDirs = ['assets'] - jniLibs.srcDirs = ['libs'] - } - } - - tasks.withType(JavaCompile) { - options.incremental = true - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - lintOptions { - abortOnError false - } - - // Do not compress Qt binary resources file - aaptOptions { - noCompress 'rcc' - } - - defaultConfig { - resConfig "en" - minSdkVersion qtMinSdkVersion - targetSdkVersion qtTargetSdkVersion - ndk.abiFilters = qtTargetAbiList.split(",") - } -} diff --git a/evcharger-app/android/gradle.properties b/evcharger-app/android/gradle.properties deleted file mode 100644 index 4fe1674..0000000 --- a/evcharger-app/android/gradle.properties +++ /dev/null @@ -1,18 +0,0 @@ -# Project-wide Gradle settings. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2500m -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 - -# Enable building projects in parallel -org.gradle.parallel=true - -# Gradle caching allows reusing the build artifacts from a previous -# build with the same inputs. However, over time, the cache size will -# grow. Uncomment the following line to enable it. -#org.gradle.caching=true -#org.gradle.configuration-cache=true - -# Allow AndroidX usage -android.useAndroidX=true diff --git a/evcharger-app/android/gradle/wrapper/gradle-wrapper.jar b/evcharger-app/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 7f93135..0000000 Binary files a/evcharger-app/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/evcharger-app/android/gradle/wrapper/gradle-wrapper.properties b/evcharger-app/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index b82aa23..0000000 --- a/evcharger-app/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/evcharger-app/android/gradlew b/evcharger-app/android/gradlew deleted file mode 100755 index c22a517..0000000 --- a/evcharger-app/android/gradlew +++ /dev/null @@ -1,249 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/evcharger-app/android/gradlew.bat b/evcharger-app/android/gradlew.bat deleted file mode 100644 index 3624bce..0000000 --- a/evcharger-app/android/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/evcharger-app/android/res/values/libs.xml b/evcharger-app/android/res/values/libs.xml deleted file mode 100644 index fe63866..0000000 --- a/evcharger-app/android/res/values/libs.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - -