From 92687b6513492c6fdc839f313d14da632c9d2767 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 22 Jul 2023 00:25:37 +0200 Subject: [PATCH] feat: Add flutter-build.sh --- .gitignore | 2 + LICENSE | 22 +++++ README.md | 12 +++ flake.lock | 61 ++++++++++++ flake.nix | 36 +++++++ src/flutter-build.sh | 228 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 361 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 src/flutter-build.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c1ea03f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# NixOS artifacts +result diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b841600 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2023 Alexander "PapaTutuWawa" + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..2694da2 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# bits-and-bytes + +A collection of small scripts (and other stuff) that I want to reuse across +my personal projects. + +## Nix + +For users of Nix, I also provide a Flake (see `flake.nix`). + +## License + +See `LICENSE`. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6e48773 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1689068808, + "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1689935543, + "narHash": "sha256-6GQ9ib4dA/r1leC5VUpsBo0BmDvNxLjKrX1iyL+h8mc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e43e2448161c0a2c4928abec4e16eae1516571bc", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..1950992 --- /dev/null +++ b/flake.nix @@ -0,0 +1,36 @@ +{ + description = "A collection of reusable scripts for various use-cases"; + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let + pkgs = import nixpkgs { inherit system; }; + + flutterBuildRaw = {stdenv}: stdenv.mkDerivation { + pname = "flutter-build"; + version = "0.1.0"; + src = ./.; + + installPhase = '' + mkdir -p $out/bin + + install --mode 555 src/flutter-build.sh $out/bin/flutter-build + ''; + }; + flutter-build-raw = pkgs.callPackage flutterBuildRaw {}; + in { + packages = { + # The raw flutter-build script + inherit flutter-build-raw; + + # A wrapper around the script that already provides the notify-send option + flutter-build = pkgs.writeScriptBin "flutter-build" '' + ${flutter-build-raw}/bin/flutter-build \ + --notify-send ${pkgs.libnotify}/bin/notify-send \ + $@ + ''; + }; + }); +} diff --git a/src/flutter-build.sh b/src/flutter-build.sh new file mode 100644 index 0000000..a71b4ea --- /dev/null +++ b/src/flutter-build.sh @@ -0,0 +1,228 @@ +#!/bin/bash +set -e + +# Parse CLI arguments +# (Thanks StackOverflow https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash) +while [[ $# -gt 0 ]]; do + case $1 in + --name) + # The name of the application. Only useful for displaying in the script. + # If not specified, will use the "name: " attribute from the pubspec.yaml + NAME=$2 + shift + shift + ;; + --not-signed) + # Tells the script that the APKs are *NOT* signed and have to be manually + # signed. + ALREADY_SIGNED=n + shift + ;; + --zipalign) + # The path to the zipalign binary. If not specified, lets the shell resolve the path + # itself. + # Only useful with --not-signed. + ZIPALIGN=$2 + shift + shift + ;; + --apksigner) + # The path to the apksigner binary. If not specified, lets the shell resolve the path + # itself. + # Only useful with --not-signed. + APKSIGNER=$2 + shift + shift + ;; + --notify-send) + # The path to the notify-send binary. If not specified, lets the shell resolve the path + # itself. + # Only useful with --not-signed. + NOTIFY_SEND=$2 + shift + shift + ;; + --provider-config) + # The path to the provider config passed to apksigner via --provider-arg. + # Only useful with --not-signed. + PROVIDER_CONFIG=$2 + shift + shift + ;; + --no-clean) + # Does not run "flutter clean" before running the build. + CLEAN_BUILD=n + shift + ;; + --min-sdk-version) + # The minimum SDK version that should be able to validate the APK signature. + # Only useful with --not-signed. + MIN_SDK_VERSION=$2 + shift + shift + ;; + --check) + # Prints all parsed options and exits. + JUST_CHECK=y + shift + ;; + --skip-build) + # Skips the entire build but not the signing, if --not-signed is specified. If --not-signed + # is not specified, then the build is skipped, but the APKs are copied into the correct folder. + # This flag expects that the build has run at least once and the correct release directory + # already exists. + SKIP_BUILD=y + shift + ;; + --no-notification) + # Does not create a notification when asking for the PIN of the Yubikey. + # Only useful with --not-signed. + SEND_NOTIFICATION=n + shift + ;; + *) + echo "Unknown argument: $1" + shift + ;; + esac +done + +# Extract the name, if not passed +if [[ -z "$NAME" ]]; then + NAME=$(grep -E "^name: " pubspec.yaml | cut -b 6-) +fi + +# Default values +APKSIGNER=${APKSIGNER:-apksigner} +ZIPALIGN=${ZIPALIGN:-zipalign} +ALREADY_SIGNED=${ALREADY_SIGNED:-y} +MIN_SDK_VERSION=${MIN_SDK_VERSION:-24} +CLEAN_BUILD=${CLEAN_BUILD:-y} +SKIP_BUILD=${SKIP_BUILD:-n} +NOTIFY_SEND=${NOTIFY_SEND:-notify-send} +SEND_NOTIFICATION=${SEND_NOTIFICATION:-y} + +# Parse version info +version=$(grep -E "^version: " pubspec.yaml | cut -b 10-) +IFS="+" read -ra version_parts <<< "$version" +version_code="${version_parts[1]}" +release_dir="./release-${version}" + +# Print a header +echo "===== ${NAME} =====" +echo "Building version ${version}" +echo "Moving APKs into ${release_dir} after build" +echo "Clean build: ${CLEAN_BUILD}" +echo "Skipping build: ${SKIP_BUILD}" +echo "Sending notification: ${SEND_NOTIFICATION}" +echo "APKs already signed: ${ALREADY_SIGNED}" +if [[ "$ALREADY_SIGNED" = "n" ]]; then + echo "Used zipalign: ${ZIPALIGN}" + echo "Used apksigner: ${APKSIGNER}" + echo "Provider config: ${PROVIDER_CONFIG}" + echo "Minimum SDK verifiability: ${MIN_SDK_VERSION}" +fi + +if [[ "${JUST_CHECK}" = "y" ]]; then + echo + echo "Terminating here because --check was used" + exit +fi + +# For better readability, print an empty line +echo + +# Check if we have a changelog file for that version +if [[ ! -f "./fastlane/metadata/android/en-US/changelogs/$version_code.txt" ]]; then + echo "Warning: No changelog item for $version_code" +fi + +if [[ "${SKIP_BUILD}" = "y" ]]; then + echo "Skipping build because of --skip-build" +else + # Create the directory + [[ -d "${release_dir}" ]] && echo "Warning: Release directory ${release_dir} already exists" + mkdir "${release_dir}" || true + + if [[ "${CLEAN_BUILD}" = "y" ]]; then + # Clean flutter build + flutter clean + fi + + # Get dependencies + flutter pub get + + # Build everything again + flutter pub run build_runner build --delete-conflicting-outputs + + # Build the release apk + flutter build apk \ + --release \ + --split-per-abi \ + --split-debug-info="${release_dir}/debug-info" +fi + +# If we have to sign, ask for the PIN upfront +if [[ "$ALREADY_SIGNED" = "n" ]]; then + [[ "${SEND_NOTIFICATION}" = "y" ]] && ${NOTIFY_SEND} \ + --urgency normal \ + --app-name "Flutter Build" \ + --icon dialog-password \ + "Signing PIN required" \ + "The build is done and the PIN for manually signing the APKs must be entered" + + # Thanks https://geoffreymetais.github.io/code/key-signing/#scripting + echo "Please enter Yubikey PIN code " + stty -echo + trap 'stty echo' EXIT + read -p 'PIN: ' YUBI_PIN + stty echo + trap - EXIT +fi + +# Move everything +for platform in arm64-v8a armeabi-v7a x86_64; do + echo "Processing the $platform release..." + + raw_apk="build/app/outputs/flutter-apk/app-${platform}-release.apk" + if [[ "$ALREADY_SIGNED" = "y" ]]; then + # Simply copy artifacts + cp -f "$raw_apk" "${release_dir}/${NAME}-${platform}-release.apk" + else + # https://developer.android.com/build/building-cmdline#sign_cmdline + aligned_apk="${release_dir}/app-${platform}-release-aligned.apk" + signed_apk="${release_dir}/app-${platform}-release.apk" + ${ZIPALIGN} -p 4 "$raw_apk" "${aligned_apk}" + + # NOTE: "-J-add-exports jdk.crypto.cryptoki/sun.security.pkcs11=ALL-UNNAMED" tells the wrapper that is + # `apksigner` to append "--add-exports jdk.[...]" as a parameter to the Java CLI. It is in + # quotation marks such that it is passed with only one (1) dash (it would otherwise be "--add-exports -jdk.[...]", + # which is wrong). + # At least on NixOS, apksigner refuses to use the sun.security.pkcs11.SunPKCS11 provider because + # it is not exported for its unnamed class. So, we have to do a little trickery to "export" the + # class to apksigner. + ${APKSIGNER} "-J-add-exports jdk.crypto.cryptoki/sun.security.pkcs11=ALL-UNNAMED" sign \ + --ks NONE \ + --ks-pass "pass:$YUBI_PIN" \ + --provider-class sun.security.pkcs11.SunPKCS11 \ + --provider-arg "${PROVIDER_CONFIG}" \ + --ks-type PKCS11 \ + --min-sdk-version "${MIN_SDK_VERSION}" \ + --max-sdk-version 34 \ + --in "${aligned_apk}" \ + --out "${signed_apk}" \ + --v1-signing-enabled \ + --v2-signing-enabled \ + --v3-signing-enabled \ + --v4-signing-enabled + + # Safety check + ${APKSIGNER} verify --min-sdk-version "${MIN_SDK_VERSION}" "${signed_apk}" + + # Remove temporary artifact + rm "${aligned_apk}" + fi +done + +# Prevent leaking the PIN +unset YUBI_PIN