nix: build unsigned Android APK, sign separately

This has several benefits:

* Less abuse of `extra-sandbox-paths` Nix option
* Less inputs to the Android release build derivation
* Easier for users to sign the build themselves
* Simplification of `scripts/release-android.sh`
* Preparation for building using Nix Flakes

The only two remaining credentials passed via `extra-sandbox-paths` is
the Infura and OpenSea API keys, and there is no way around that other
than passing them via Nix arguments, but that would cause them to end up
in `/nix/store` as part of `.drv` files.

I'm also renaming `release-fdroid` to `build-fdroid` to be consistent.

Depends on: https://github.com/status-im/status-jenkins-lib/pull/42

Signed-off-by: Jakub Sokołowski <jakub@status.im>
This commit is contained in:
Jakub Sokołowski 2022-04-22 12:20:01 +04:00
parent 8a01d02135
commit acfa73ab43
No known key found for this signature in database
GPG Key ID: 09AA5403E54D9931
20 changed files with 112 additions and 144 deletions

View File

@ -32,10 +32,12 @@ ifndef BUILD_TAG
export BUILD_TAG := $(shell git rev-parse --short HEAD)
endif
# We don't want to use /run/user/$UID because it runs out of space too easilly
# We don't want to use /run/user/$UID because it runs out of space too easilly.
export TMPDIR = /tmp/tmp-status-react-$(BUILD_TAG)
# this has to be specified for both the Node.JS server process and the Qt process
# This has to be specified for both the Node.JS server process and the Qt process.
export REACT_SERVER_PORT ?= 5001
# The path can be anything, but home is usually safest.
export KEYSTORE_PATH ?= $(HOME)/.gradle/status-im.keystore
# Our custom config is located in nix/nix.conf
export NIX_CONF_DIR = $(PWD)/nix
@ -153,11 +155,12 @@ update-fleets: ##@prepare Download up-to-date JSON file with current fleets stat
| jq --indent 4 --sort-keys . \
> resources/config/fleets.json
keystore: export TARGET := keytool
keystore: export KEYSTORE_PATH ?= $(HOME)/.gradle/status-im.keystore
keystore: ##@prepare Generate a Keystore for signing Android APKs
$(KEYSTORE_PATH): export TARGET := keytool
$(KEYSTORE_PATH):
@./scripts/generate-keystore.sh
keystore: $(KEYSTORE_PATH) ##@prepare Generate a Keystore for signing Android APKs
fdroid-max-watches: SHELL := /bin/sh
fdroid-max-watches: ##@prepare Bump max_user_watches to avoid ENOSPC errors
sysctl fs.inotify.max_user_watches=524288
@ -190,23 +193,26 @@ xcode-clean: ##@prepare Clean XCode derived data and archives
#----------------
release: release-android release-ios ##@build Build release for Android and iOS
release-android: export BUILD_ENV ?= prod
release-android: export BUILD_TYPE ?= nightly
release-android: export BUILD_NUMBER ?= $(TMP_BUILD_NUMBER)
release-android: export KEYSTORE_PATH ?= $(HOME)/.gradle/status-im.keystore
release-android: export ANDROID_APK_SIGNED ?= true
release-android: export ANDROID_ABI_SPLIT ?= false
release-android: export ANDROID_ABI_INCLUDE ?= armeabi-v7a;arm64-v8a;x86
release-android: keystore ##@build Build release for Android
scripts/release-android.sh
build-fdroid: export BUILD_ENV = prod
build-fdroid: export BUILD_TYPE = release
build-fdroid: export ANDROID_ABI_SPLIT = false
build-fdroid: export ANDROID_ABI_INCLUDE = armeabi-v7a;arm64-v8a;x86;x86_64
build-fdroid: ##@build Build release for F-Droid
@scripts/build-android.sh
release-fdroid: export BUILD_ENV = prod
release-fdroid: export BUILD_TYPE = release
release-fdroid: export ANDROID_APK_SIGNED = false
release-fdroid: export ANDROID_ABI_SPLIT = false
release-fdroid: export ANDROID_ABI_INCLUDE = armeabi-v7a;arm64-v8a;x86;x86_64
release-fdroid: ##@build Build release for F-Droid
scripts/release-android.sh
build-android: SHELL := /bin/sh
build-android: export BUILD_ENV ?= prod
build-android: export BUILD_TYPE ?= nightly
build-android: export BUILD_NUMBER ?= $(TMP_BUILD_NUMBER)
build-android: export ANDROID_ABI_SPLIT ?= false
build-android: export ANDROID_ABI_INCLUDE ?= armeabi-v7a;arm64-v8a;x86
build-android: ##@build Build unsigned Android APK
@scripts/build-android.sh
release-android: export TARGET := keytool
release-android: export KEYSTORE_PATH ?= $(HOME)/.gradle/status-im.keystore
release-android: keystore build-android ##@build Build signed Android APK
@scripts/sign-android.sh result/app-release-unsigned.apk
release-ios: export TARGET := ios
release-ios: export BUILD_ENV ?= prod
@ -221,7 +227,7 @@ jsbundle-android: export BUILD_ENV ?= prod
jsbundle-android: ##@jsbundle Compile JavaScript and Clojurescript into app directory
# Call nix-build to build the 'targets.mobile.android.jsbundle' attribute and copy the.js files to the project root
nix/scripts/build.sh targets.mobile.android.jsbundle && \
mv result/* ./
mv result/*.js ./
jsbundle-ios: export TARGET := ios
jsbundle-ios: export BUILD_ENV ?= prod

View File

@ -229,23 +229,6 @@ android {
jumboMode true
javaMaxHeapSize "8g"
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
/* Caution! In production, you need to generate your own keystore file.
* See: https://facebook.github.io/react-native/docs/signed-apk-android */
release {
/* environment variables take precedence over gradle.properties file */
storeFile file(getEnvOrConfig('KEYSTORE_PATH').replaceAll("~", System.properties['user.home']))
storePassword getEnvOrConfig('KEYSTORE_PASSWORD')
keyAlias getEnvOrConfig('KEYSTORE_ALIAS')
keyPassword getEnvOrConfig('KEYSTORE_KEY_PASSWORD')
}
}
splits {
abi {
reset()
@ -254,20 +237,26 @@ android {
universalApk true
}
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
buildTypes {
debug {
applicationIdSuffix ".debug"
debuggable true
versionNameSuffix "-SNAPSHOT"
signingConfig signingConfigs.debug
resValue "string", "build_config_package", "im.status.ethereum"
signingConfig signingConfigs.debug
}
release {
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
if (getEnvOrConfig('ANDROID_APK_SIGNED').toBoolean()) {
signingConfig signingConfigs.release
}
signingConfig null
}
pr {
initWith release

View File

@ -42,8 +42,6 @@ KEYSTORE_KEY_PASSWORD=password
ANDROID_ABI_SPLIT=false
# Some platforms are excluded though
ANDROID_ABI_INCLUDE=armeabi-v7a;arm64-v8a;x86
# F-Droid builds need to be unsigned
ANDROID_APK_SIGNED=true
org.gradle.jvmargs=-Xmx8704M

View File

@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.4.2'
library 'status-jenkins-lib@v1.4.3'
pipeline {
agent { label 'linux && x86_64 && nix-2.8' }
@ -81,6 +81,11 @@ pipeline {
script { apks = android.bundle() }
}
}
stage('Sign') {
steps {
script { apks = android.sign(apks) }
}
}
} }
}
}

View File

@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.4.2'
library 'status-jenkins-lib@v1.4.3'
pipeline {
agent { label 'linux' }

View File

@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.4.2'
library 'status-jenkins-lib@v1.4.3'
pipeline {
agent { label 'macos && x86_64 && nix-2.8 && xcode-13.3' }

View File

@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.4.2'
library 'status-jenkins-lib@v1.4.3'
pipeline {
agent { label params.AGENT_LABEL }

View File

@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.4.2'
library 'status-jenkins-lib@v1.4.3'
pipeline {

View File

@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.4.2'
library 'status-jenkins-lib@v1.4.3'
pipeline {
agent { label 'macos' }

View File

@ -1,4 +1,4 @@
library 'status-jenkins-lib@v1.4.2'
library 'status-jenkins-lib@v1.4.3'
pipeline {
agent { label 'linux' }

View File

@ -19,7 +19,6 @@ Here is a sample structure of the `config` attribute set:
build-number = 9999; # Used for versionCode and CFBundleVersion in Android and iOS respectively
android = {
gradle-opts = ""; # Gradle options passed for Android builds
keystore-path = ""; # Path to keystore for signing the APK
abi-split = false; # If APKs should be split based on architectures
abi-include = "x86"; # Android architectures to build for
};

View File

@ -7,7 +7,6 @@
android = {
gradle-opts = null; # Gradle options passed for Android builds
keystore-path = null; # Path to keystore for signing the APK
apk-signed = true; # F-Droid builds aren't signed by us
abi-split = false; # If APKs should be split based on architectures
abi-include = "armeabi-v7a;arm64-v8a;x86"; # Android architectures to build for

View File

@ -2,9 +2,6 @@
, status-go, androidPkgs, androidShell }:
let
# For generating a temporary keystore for local development
keystore = callPackage ./keystore.nix { };
# Import a jsbundle compiled out of clojure codebase
jsbundle = callPackage ./jsbundle { };
@ -13,12 +10,12 @@ let
# TARGETS
release = callPackage ./release.nix {
inherit keystore jsbundle status-go watchmanFactory;
inherit jsbundle status-go watchmanFactory;
};
in {
# TARGETS
inherit keystore release jsbundle;
inherit release jsbundle;
shell = mkShell {
buildInputs = with pkgs; [

View File

@ -1,44 +0,0 @@
#
# Generates an ad-hoc and temporary keystore for signing debug/pr builds.
#
# WARNING: Do NOT use this to make a keystore that needs to be secret!
# Using a derivation will store the inputs in a .drv file.
#
{ stdenv, lib, pkgs }:
let
inherit (lib) getAttr;
gradleProps = pkgs.gradlePropParser ../../../android/gradle.properties;
# Loading defaults from gradle.properties which should be safe.
KEYSTORE_ALIAS = getAttr "KEYSTORE_ALIAS" gradleProps;
KEYSTORE_PASSWORD = getAttr "KEYSTORE_PASSWORD" gradleProps;
KEYSTORE_KEY_PASSWORD = getAttr "KEYSTORE_KEY_PASSWORD" gradleProps;
in stdenv.mkDerivation {
name = "status-react-android-keystore";
buildInputs = [ pkgs.openjdk8 ];
phases = [ "generatePhase" ];
generatePhase = ''
keytool -genkey -v \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-deststoretype pkcs12 \
-dname "CN=, OU=, O=, L=, S=, C=" \
-keystore "$out" \
-alias "${KEYSTORE_ALIAS}" \
-storepass "${KEYSTORE_PASSWORD}" \
-keypass "${KEYSTORE_KEY_PASSWORD}" \
>&2
'';
shellHook = ''
export KEYSTORE_ALIAS="${KEYSTORE_ALIAS}"
export KEYSTORE_PASSWORD="${KEYSTORE_PASSWORD}"
export KEYSTORE_KEY_PASSWORD="${KEYSTORE_KEY_PASSWORD}"
'';
}

View File

@ -1,6 +1,6 @@
{ stdenv, pkgs, deps, lib, config, callPackage,
watchmanFactory, androidPkgs, patchMavenSources,
keystore, jsbundle, status-go }:
jsbundle, status-go }:
{
# Value for BUILD_ENV checked by Clojure code at compile time
@ -28,9 +28,6 @@ let
gradleOpts = getConfig "android.gradle-opts" null;
# Used to detect end-to-end builds
androidAbiInclude = getConfig "android.abi-include" "armeabi-v7a;arm64-v8a;x86";
# Keystore can be provided via config and extra-sandbox-paths.
# If it is not we use an ad-hoc one generated with default password.
keystorePath = getConfig "android.keystore-path" keystore;
baseName = "${buildType}-android";
name = "status-react-build-${baseName}";
@ -76,7 +73,6 @@ in stdenv.mkDerivation rec {
# custom env variables derived from config
STATUS_GO_SRC_OVERRIDE = getConfig "status-go.src-override" null;
ANDROID_APK_SIGNED = getConfig "android.apk-signed" "true";
ANDROID_ABI_SPLIT = getConfig "android.abi-split" "false";
ANDROID_ABI_INCLUDE = androidAbiInclude;
@ -88,8 +84,7 @@ in stdenv.mkDerivation rec {
STATUS_GO_ANDROID_LIBDIR = status-go { inherit secretsFile; };
phases = [
"unpackPhase" "secretsPhase" "keystorePhase"
"buildPhase" "checkPhase" "installPhase"
"unpackPhase" "secretsPhase" "buildPhase" "checkPhase" "installPhase"
];
unpackPhase = ''
@ -120,21 +115,13 @@ in stdenv.mkDerivation rec {
${patchMavenSources} ./android/build.gradle
'';
# if secretsFile is not set we use generate keystore
# Secrets file is passed to sandbox using extra-sandbox-paths.
secretsPhase = if (secretsFile != "") then ''
source "${secretsFile}"
${checkEnvVarSet "KEYSTORE_ALIAS"}
${checkEnvVarSet "KEYSTORE_PASSWORD"}
${checkEnvVarSet "KEYSTORE_KEY_PASSWORD"}
'' else keystore.shellHook;
# if keystorePath is set copy it into build directory
keystorePhase =
assert assertMsg (keystorePath != null) "keystorePath has to be set!";
''
export KEYSTORE_PATH="$PWD/status-im.keystore"
cp -a --no-preserve=ownership "${keystorePath}" "$KEYSTORE_PATH"
'' else ''
echo 'WARNING: No secrets provided!' >&2
'';
buildPhase = let
adhocEnvVars = optionalString stdenv.isLinux
"LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${makeLibraryPath [ pkgs.zlib ]}";

View File

@ -58,7 +58,7 @@ nixOpts=(
${GIT_ROOT}/nix/scripts/gcroots.sh "${TARGET}" "${@}"
# Run the actual build
echo "Running: nix-build "${nixOpts[@]}" "${@}" default.nix"
echo "${GRN}Running:${RST} nix-build "${nixOpts[@]}" "${@}" default.nix"
nixResultPath=$(nix-build "${nixOpts[@]}" "${@}" default.nix)
echo -e "\n${YLW}Extracting result${RST}: ${BLD}${nixResultPath}${RST}"

View File

@ -58,7 +58,7 @@ let
# for 'scripts/generate-keystore.sh'
keytool = mkShell {
buildInputs = with pkgs; [ openjdk8 ];
buildInputs = with pkgs; [ openjdk8 apksigner ];
};
# for targets that need 'adb' and other SDK/NDK tools

View File

@ -26,13 +26,9 @@ config=''
if [[ -n "${STATUS_GO_SRC_OVERRIDE}" ]]; then
config+="status-im.status-go.src-override=\"${STATUS_GO_SRC_OVERRIDE}\";"
fi
if [[ "${ANDROID_APK_SIGNED}" == "true" ]]; then
config+="status-im.android.keystore-path=\"$(must_get_env KEYSTORE_PATH)\";"
fi
config+="status-im.commit-hash=\"$(git rev-parse --verify HEAD)\";"
config+="status-im.build-type=\"$(must_get_env BUILD_TYPE)\";"
config+="status-im.build-number=\"$(must_get_env BUILD_NUMBER)\";"
config+="status-im.android.apk-signed=\"$(must_get_env ANDROID_APK_SIGNED)\";"
config+="status-im.android.abi-split=\"$(must_get_env ANDROID_ABI_SPLIT)\";"
config+="status-im.android.abi-include=\"$(must_get_env ANDROID_ABI_INCLUDE)\";"
nixOpts=()
@ -42,19 +38,10 @@ export SECRETS_FILE_PATH=$(mktemp)
chmod 644 ${SECRETS_FILE_PATH}
# If secrets file was created we want to remove it.
trap "rm -vf ${SECRETS_FILE_PATH}" EXIT ERR INT QUIT
# Secrets like this can't be passed via args or they end up in derivation.
if [[ -n "${KEYSTORE_ALIAS}${KEYSTORE_ALIAS}${KEYSTORE_ALIAS}" ]]; then
# WARNING: All three have to be set!
append_env_export 'KEYSTORE_PASSWORD'
append_env_export 'KEYSTORE_ALIAS'
append_env_export 'KEYSTORE_KEY_PASSWORD'
fi
if [[ -n "${INFURA_TOKEN}" ]]; then
append_env_export 'INFURA_TOKEN'
fi
if [[ -n "${OPENSEA_API_KEY}" ]]; then
append_env_export 'OPENSEA_API_KEY'
fi
if [[ -n "${INFURA_TOKEN}" ]]; then append_env_export 'INFURA_TOKEN'; fi
if [[ -n "${OPENSEA_API_KEY}" ]]; then append_env_export 'OPENSEA_API_KEY'; fi
# If no secrets were passed there's no need to pass the 'secretsFile'.
if [[ -s "${SECRETS_FILE_PATH}" ]]; then

View File

@ -32,7 +32,6 @@ KEYSTORE_PATH=${KEYSTORE_PATH/#\~/$HOME}
if [[ -e "${KEYSTORE_PATH}" ]]; then
echo -e "${YLW}Keystore file already exists:${RST} ${KEYSTORE_PATH}" >&2
echo "${KEYSTORE_PATH}"
exit 0
fi
@ -41,7 +40,7 @@ KEYSTORE_DIR=$(dirname "${KEYSTORE_PATH}")
echo -e "${GRN}Generating keystore...${RST}" >&2
keytool -genkey -v \
exec keytool -genkey -v \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
@ -52,5 +51,3 @@ keytool -genkey -v \
-storepass "${KEYSTORE_PASSWORD}" \
-keypass "${KEYSTORE_KEY_PASSWORD}" \
> /dev/stderr
echo "${KEYSTORE_PATH}"

48
scripts/sign-android.sh Executable file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -e
GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel)
source "${GIT_ROOT}/scripts/colors.sh"
function property() {
grep "${2}" "${1}" | cut -d'=' -f2
}
function gradle_property() {
property ${GIT_ROOT}/android/gradle.properties ${1}
}
function env_var_or_gradle_prop() {
VAR_NAME="${1}"
if [[ -n "${!VAR_NAME}" ]]; then
echo "${!VAR_NAME}"
else
gradle_property "${VAR_NAME}"
fi
}
function must_get_env() {
declare -n VAR_VALUE="$1"
if [[ -n "${VAR_VALUE}" ]]; then
echo "${VAR_VALUE}"
return
fi
echo -e "${RED}No required env variable:${RST} ${BLD}${!VAR_VALUE}${RST}" 1>&2
exit 1
}
# If filename contains string "unsigned" change that to signed.
# Otherwise sign in-place and overwrite the current unsigned file.
if [[ "${1}" =~ unsigned ]]; then
OUTPUT_FLAGS="--out=${1/unsigned/signed}"
fi
echo -e "${GRN}Signing APK:${RST} ${1}" >&2
exec apksigner sign --verbose \
--ks="$(env_var_or_gradle_prop KEYSTORE_PATH)" \
--ks-pass="pass:$(env_var_or_gradle_prop KEYSTORE_PASSWORD)" \
--ks-key-alias="$(env_var_or_gradle_prop KEYSTORE_ALIAS)" \
--key-pass="pass:$(env_var_or_gradle_prop KEYSTORE_KEY_PASSWORD)" \
"${OUTPUT_FLAGS}" \
"${1}"