#!/usr/bin/env bash # # KeePassXC Release Preparation Helper # Copyright (C) 2021 KeePassXC team # # 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 2 or (at your option) # version 3 of the License. # # 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 . printf "\e[1m\e[32mKeePassXC\e[0m Release Preparation Helper\n" printf "Copyright (C) 2021 KeePassXC Team \n\n" set -eE -o pipefail if [ "$(uname -s)" == "Linux" ]; then OS_LINUX="1" elif [ "$(uname -s)" == "Darwin" ]; then OS_MACOS="1" elif [ "$(uname -o)" == "Msys" ]; then OS_WINDOWS="1" fi # ----------------------------------------------------------------------- # global default values # ----------------------------------------------------------------------- RELEASE_NAME="" APP_NAME="KeePassXC" SRC_DIR="." GPG_KEY="CFB4C2166397D0D2" GPG_GIT_KEY="" OUTPUT_DIR="release" SOURCE_BRANCH="" TAG_NAME="" DOCKER_IMAGE="" DOCKER_CONTAINER_NAME="keepassxc-build-container" CMAKE_GENERATOR="Unix Makefiles" CMAKE_OPTIONS="" CPACK_GENERATORS="WIX;ZIP" COMPILER="g++" MAKE_OPTIONS="-j$(getconf _NPROCESSORS_ONLN)" BUILD_PLUGINS="all" INSTALL_PREFIX="/usr/local" ORIG_BRANCH="" ORIG_CWD="$(pwd)" MACOSX_DEPLOYMENT_TARGET=10.15 TIMESTAMP_SERVER="http://timestamp.sectigo.com" # ----------------------------------------------------------------------- # helper functions # ----------------------------------------------------------------------- printUsage() { local cmd if [ -z "$1" ] || [ "help" == "$1" ]; then cmd="COMMAND" elif [ "check" == "$1" ] || [ "merge" == "$1" ] || [ "build" == "$1" ] || [ "gpgsign" == "$1" ] || \ [ "appsign" == "$1" ] || [ "notarize" == "$1" ] || [ "appimage" == "$1" ] || [ "i18n" == "$1" ]; then cmd="$1" else logError "Unknown command: '$1'\n" cmd="COMMAND" fi printf "\e[1mUsage:\e[0m $(basename "$0") $cmd [OPTIONS, ...]\n" if [ "COMMAND" == "$cmd" ]; then cat << EOF Commands: check Perform a dry-run check, nothing is changed merge Merge release branch into main branch and create release tags build Build and package binary release from sources gpgsign Sign previously compiled release packages with GPG appsign Sign binaries with code signing certificates on Windows and macOS notarize Submit macOS application DMG for notarization help Show help for the given command i18n Update translation files and pull from or push to Transifex EOF elif [ "merge" == "$cmd" ]; then cat << EOF Merge release branch into main branch and create release tags Options: -v, --version Release version number or name (required) -a, --app-name Application name (default: '${APP_NAME}') -s, --source-dir Source directory (default: '${SRC_DIR}') -k, --key GPG key used to sign the merge commit and release tag, leave empty to let Git choose your default key (default: '${GPG_GIT_KEY}') -r, --release-branch Source release branch to merge from (default: 'release/VERSION') -t, --tag-name Override release tag name (defaults to version number) -h, --help Show this help EOF elif [ "build" == "$cmd" ]; then cat << EOF Build and package binary release from sources Options: -v, --version Release version number or name (required) -a, --app-name Application name (default: '${APP_NAME}') -s, --source-dir Source directory (default: '${SRC_DIR}') -o, --output-dir Output directory where to build the release (default: '${OUTPUT_DIR}') -t, --tag-name Release tag to check out (defaults to version number) -b, --build Build sources after exporting release -d, --docker-image Use the specified Docker image to compile the application. The image must have all required build dependencies installed. This option has no effect if --build is not set. --container-name Docker container name (default: '${DOCKER_CONTAINER_NAME}') The container must not exist already --snapcraft Create and use docker image to build snapcraft distribution. This option has no effect if --docker-image is not set. --appimage Build a Linux AppImage after compilation. If this option is set, --install-prefix has no effect --appsign Perform platform specific App Signing before packaging --timestamp Explicitly set the timestamp server to use for appsign (default: '${TIMESTAMP_SERVER}') --vcpkg Specify VCPKG toolchain file (example: ~/vcpkg/scripts/buildsystems/vcpkg.cmake) -k, --key Specify the App Signing Key/Identity --cmake-generator Override the default CMake generator (Default: Ninja) -c, --cmake-options Additional CMake options for compiling the sources --compiler Compiler to use (default: '${COMPILER}') -m, --make-options Make options for compiling sources (default: '${MAKE_OPTIONS}') -g, --generators Additional CPack generators (default: '${CPACK_GENERATORS}') -i, --install-prefix Install prefix (default: '${INSTALL_PREFIX}') -p, --plugins Space-separated list of plugins to build (default: ${BUILD_PLUGINS}) --snapshot Don't checkout the release tag -n, --no-source-tarball Don't build source tarball -h, --help Show this help EOF elif [ "gpgsign" == "$cmd" ]; then cat << EOF Sign previously compiled release packages with GPG Options: -f, --files Files to sign (required) -k, --key GPG key used to sign the files (default: '${GPG_KEY}') -h, --help Show this help EOF elif [ "appsign" == "$cmd" ]; then cat << EOF Sign binaries with code signing certificates on Windows and macOS Options: -f, --files Files to sign (required) -k, --key, -i, --identity Signing Key or Apple Developer ID (required) --timestamp Explicitly set the timestamp server to use for appsign (default: '${TIMESTAMP_SERVER}') -u, --username Apple username for notarization (required on macOS) -h, --help Show this help EOF elif [ "notarize" == "$cmd" ]; then cat << EOF Submit macOS application DMG for notarization Options: -f, --files Files to notarize (required) -u, --username Apple username for notarization (required) -c, --keychain Apple keychain entry name storing the notarization app password (default: 'AC_PASSWORD') -h, --help Show this help EOF elif [ "appimage" == "$cmd" ]; then cat << EOF Generate Linux AppImage from 'make install' AppDir Options: -a, --appdir Input AppDir (required) -v, --version KeePassXC version -o, --output-dir Output directory where to build the AppImage (default: '${OUTPUT_DIR}') -d, --docker-image Use the specified Docker image to build the AppImage. The image must have all required build dependencies installed. --container-name Docker container name (default: '${DOCKER_CONTAINER_NAME}') The container must not exist already --appsign Embed a PGP signature into the AppImage -k, --key The PGP Signing Key --verbosity linuxdeploy verbosity (default: 3) -h, --help Show this help EOF elif [ "i18n" == "$cmd" ]; then cat << EOF Update translation files and pull from or push to Transifex Subcommands: tx-push Push source translation file to Transifex tx-pull Pull updated translations from Transifex lupdate Update source translation file from C++ sources EOF fi } logInfo() { printf "\e[1m[ \e[34mINFO\e[39m ]\e[0m $1\n" } logWarn() { printf "\e[1m[ \e[33mWARNING\e[39m ]\e[0m $1\n" } logError() { printf "\e[1m[ \e[31mERROR\e[39m ]\e[0m $1\n" >&2 } init() { if [ -z "$RELEASE_NAME" ]; then logError "Missing arguments, --version is required!\n" printUsage "check" exit 1 fi if [ -z "$TAG_NAME" ]; then TAG_NAME="$RELEASE_NAME" fi if [ -z "$SOURCE_BRANCH" ]; then SOURCE_BRANCH="release/${RELEASE_NAME}" fi ORIG_CWD="$(pwd)" SRC_DIR="$(realpath "$SRC_DIR")" cd "$SRC_DIR" > /dev/null 2>&1 ORIG_BRANCH="$(git rev-parse --abbrev-ref HEAD 2> /dev/null)" cd "$ORIG_CWD" } cleanup() { logInfo "Checking out original branch..." if [ "" != "$ORIG_BRANCH" ]; then git checkout "$ORIG_BRANCH" > /dev/null 2>&1 fi logInfo "Leaving source directory..." cd "$ORIG_CWD" } exitError() { cleanup logError "$1" exit 1 } cmdExists() { command -v "$1" &> /dev/null } checkSourceDirExists() { if [ ! -d "$SRC_DIR" ]; then exitError "Source directory '${SRC_DIR}' does not exist!" fi } checkOutputDirDoesNotExist() { if [ -e "$OUTPUT_DIR" ]; then exitError "Output directory '$OUTPUT_DIR' already exists. Please choose a different location!" fi } checkGitRepository() { if [ ! -d .git ] || [ ! -f CHANGELOG.md ]; then exitError "Source directory is not a valid Git repository!" fi } checkReleaseDoesNotExist() { if [ $(git tag -l $TAG_NAME) ]; then exitError "Release '$RELEASE_NAME' (tag: '$TAG_NAME') already exists!" fi } checkWorkingTreeClean() { if ! git diff-index --quiet HEAD --; then exitError "Current working tree is not clean! Please commit or unstage any changes." fi } checkSourceBranchExists() { if ! git rev-parse "$SOURCE_BRANCH" > /dev/null 2>&1; then exitError "Source branch '$SOURCE_BRANCH' does not exist!" fi } checkVersionInCMake() { local app_name_upper="$(echo "$APP_NAME" | tr '[:lower:]' '[:upper:]')" local major_num="$(echo ${RELEASE_NAME} | cut -f1 -d.)" local minor_num="$(echo ${RELEASE_NAME} | cut -f2 -d.)" local patch_num="$(echo ${RELEASE_NAME} | cut -f3 -d. | cut -f1 -d-)" if ! grep -q "${app_name_upper}_VERSION_MAJOR \"${major_num}\"" CMakeLists.txt; then exitError "${app_name_upper}_VERSION_MAJOR not updated to '${major_num}' in CMakeLists.txt!" fi if ! grep -q "${app_name_upper}_VERSION_MINOR \"${minor_num}\"" CMakeLists.txt; then exitError "${app_name_upper}_VERSION_MINOR not updated to '${minor_num}' in CMakeLists.txt!" fi if ! grep -q "${app_name_upper}_VERSION_PATCH \"${patch_num}\"" CMakeLists.txt; then exitError "${app_name_upper}_VERSION_PATCH not updated to '${patch_num}' in CMakeLists.txt!" fi } checkChangeLog() { if [ ! -f CHANGELOG.md ]; then exitError "No CHANGELOG file found!" fi if ! grep -qEzo "## ${RELEASE_NAME} \([0-9]{4}-[0-9]{2}-[0-9]{2}\)" CHANGELOG.md; then exitError "'CHANGELOG.md' has not been updated to the '${RELEASE_NAME}' release!" fi } checkAppStreamInfo() { if [ ! -f share/linux/org.keepassxc.KeePassXC.appdata.xml ]; then exitError "No AppStream info file found!" fi if ! grep -qEzo "" share/linux/org.keepassxc.KeePassXC.appdata.xml; then exitError "'share/linux/org.keepassxc.KeePassXC.appdata.xml' has not been updated to the '${RELEASE_NAME}' release!" fi } checkTransifexCommandExists() { if ! cmdExists tx; then exitError "Transifex tool 'tx' not installed! Please install it using 'pip install transifex-client'." fi } checkSigntoolCommandExists() { if ! cmdExists signtool; then exitError "signtool command not found on the PATH! Add the Windows SDK binary folder to your PATH." fi } checkXcodeSetup() { if ! cmdExists xcrun; then exitError "xcrun command not found on the PATH! Please check that you have correctly installed Xcode." fi if ! xcrun -f codesign > /dev/null 2>&1; then exitError "codesign command not found. You may need to run 'sudo xcode-select -r' to set up Xcode." fi if ! xcrun -f altool > /dev/null 2>&1; then exitError "altool command not found. You may need to run 'sudo xcode-select -r' to set up Xcode." fi if ! xcrun -f stapler > /dev/null 2>&1; then exitError "stapler command not found. You may need to run 'sudo xcode-select -r' to set up Xcode." fi } checkQt5LUpdateExists() { if cmdExists lupdate && ! $(lupdate -version | grep -q "lupdate version 5\."); then if ! cmdExists lupdate-qt5; then exitError "Qt Linguist tool (lupdate-qt5) is not installed! Please install using 'apt install qttools5-dev-tools'" fi fi } performChecks() { logInfo "Performing basic checks..." checkSourceDirExists logInfo "Changing to source directory..." cd "${SRC_DIR}" logInfo "Validating toolset and repository..." checkTransifexCommandExists checkQt5LUpdateExists checkGitRepository checkReleaseDoesNotExist checkWorkingTreeClean checkSourceBranchExists logInfo "Checking out '${SOURCE_BRANCH}'..." git checkout "$SOURCE_BRANCH" > /dev/null 2>&1 logInfo "Attempting to find '${RELEASE_NAME}' in various files..." checkVersionInCMake checkChangeLog checkAppStreamInfo logInfo "\e[1m\e[32mAll checks passed!\e[0m" } # re-implement realpath for OS X (thanks mschrag) # https://superuser.com/questions/205127/ if ! cmdExists realpath; then realpath() { pushd . > /dev/null if [ -d "$1" ]; then cd "$1" dirs -l +0 else cd "$(dirname "$1")" cur_dir=$(dirs -l +0) if [ "$cur_dir" == "/" ]; then echo "$cur_dir$(basename "$1")" else echo "$cur_dir/$(basename "$1")" fi fi popd > /dev/null } fi trap 'exitError "Exited upon user request."' SIGINT SIGTERM trap 'exitError "Error occurred!"' ERR # ----------------------------------------------------------------------- # check command # ----------------------------------------------------------------------- check() { while [ $# -ge 1 ]; do local arg="$1" case "$arg" in -v|--version) RELEASE_NAME="$2" shift ;; esac shift done init performChecks cleanup logInfo "Congrats! You can successfully merge, build, and sign KeepassXC." } # ----------------------------------------------------------------------- # merge command # ----------------------------------------------------------------------- merge() { while [ $# -ge 1 ]; do local arg="$1" case "$arg" in -v|--version) RELEASE_NAME="$2" shift ;; -a|--app-name) APP_NAME="$2" shift ;; -s|--source-dir) SRC_DIR="$2" shift ;; -k|--key|-g|--gpg-key) GPG_GIT_KEY="$2" shift ;; --timestamp) TIMESTAMP_SERVER="$2" shift ;; -r|--release-branch) SOURCE_BRANCH="$2" shift ;; -t|--tag-name) TAG_NAME="$2" shift ;; -h|--help) printUsage "merge" exit ;; *) logError "Unknown option '$arg'\n" printUsage "merge" exit 1 ;; esac shift done init performChecks # Update translations i18n lupdate i18n tx-pull if [ 0 -ne $? ]; then exitError "Updating translations failed!" fi if ! git diff-index --quiet HEAD --; then git add -A ./share/translations/ logInfo "Committing changes..." if [ -z "$GPG_GIT_KEY" ]; then git commit -m "Update translations" else git commit -m "Update translations" -S"$GPG_GIT_KEY" fi fi local flags="-Pzo" if [ -n "$OS_MACOS" ]; then flags="-Ezo" fi CHANGELOG=$(grep ${flags} "## ${RELEASE_NAME} \([0-9]{4}-[0-9]{2}-[0-9]{2}\)\n\n(.|\n)+?\n\n## " CHANGELOG.md \ | tail -n+3 | sed '$d' | sed 's/^### //') COMMIT_MSG="Release ${RELEASE_NAME}" logInfo "Creating tag '${TAG_NAME}'..." if [ -z "$GPG_GIT_KEY" ]; then git tag -a "$TAG_NAME" -m "$COMMIT_MSG" -m "${CHANGELOG}" -s else git tag -a "$TAG_NAME" -m "$COMMIT_MSG" -m "${CHANGELOG}" -s -u "$GPG_GIT_KEY" fi logInfo "Advancing 'latest' tag..." if [ -z "$GPG_GIT_KEY" ]; then git tag -sf -a "latest" -m "Latest stable release" else git tag -sf -u "$GPG_GIT_KEY" -a "latest" -m "Latest stable release" fi cleanup logInfo "All done!" logInfo "Don't forget to push the tags using \e[1mgit push --tags\e[0m." } # ----------------------------------------------------------------------- # appimage command # ----------------------------------------------------------------------- appimage() { local appdir local build_appsign=false local build_key local verbosity="1" while [ $# -ge 1 ]; do local arg="$1" case "$arg" in -v|--version) RELEASE_NAME="$2" shift ;; -a|--appdir) appdir="$2" shift ;; -o|--output-dir) OUTPUT_DIR="$2" shift ;; -d|--docker-image) DOCKER_IMAGE="$2" shift ;; --container-name) DOCKER_CONTAINER_NAME="$2" shift ;; --appsign) build_appsign=true ;; --verbosity) verbosity=$2 shift ;; -k|--key) build_key="$2" shift ;; -h|--help) printUsage "appimage" exit ;; *) logError "Unknown option '$arg'\n" printUsage "appimage" exit 1 ;; esac shift done if [ -z "${appdir}" ]; then logError "Missing arguments, --appdir is required!\n" printUsage "appimage" exit 1 fi if [ ! -d "${appdir}" ]; then exitError "AppDir does not exist, please create one with 'make install'!" elif [ -e "${appdir}/AppRun" ]; then exitError "AppDir has already been run through linuxdeploy, please create a fresh AppDir with 'make install'." fi appdir="$(realpath "$appdir")" local out="${OUTPUT_DIR}" if [ -z "$out" ]; then out="." fi mkdir -p "$out" local out_real="$(realpath "$out")" cd "$out" local linuxdeploy="linuxdeploy" local linuxdeploy_cleanup local linuxdeploy_plugin_qt="linuxdeploy-plugin-qt" local linuxdeploy_plugin_qt_cleanup local appimagetool="appimagetool" local appimagetool_cleanup logInfo "Testing for AppImage tools..." local docker_test_cmd if [ "" != "$DOCKER_IMAGE" ]; then docker_test_cmd="docker run -it --user $(id -u):$(id -g) --rm ${DOCKER_IMAGE}" fi # Test if linuxdeploy and linuxdeploy-plugin-qt are installed # on the system or inside the Docker container if ! ${docker_test_cmd} which ${linuxdeploy} > /dev/null; then logInfo "Downloading linuxdeploy..." linuxdeploy="./linuxdeploy" linuxdeploy_cleanup="rm -f ${linuxdeploy}" if ! curl -Lf "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" > "$linuxdeploy"; then exitError "linuxdeploy download failed." fi chmod +x "$linuxdeploy" fi if ! ${docker_test_cmd} which ${linuxdeploy_plugin_qt} > /dev/null; then logInfo "Downloading linuxdeploy-plugin-qt..." linuxdeploy_plugin_qt="./linuxdeploy-plugin-qt" linuxdeploy_plugin_qt_cleanup="rm -f ${linuxdeploy_plugin_qt}" if ! curl -Lf "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" > "$linuxdeploy_plugin_qt"; then exitError "linuxdeploy-plugin-qt download failed." fi chmod +x "$linuxdeploy_plugin_qt" fi # appimagetool is always run outside a Docker container, so we can access our GPG keys if ! cmdExists ${appimagetool}; then logInfo "Downloading appimagetool..." appimagetool="./appimagetool" appimagetool_cleanup="rm -f ${appimagetool}" if ! curl -Lf "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" > "$appimagetool"; then exitError "appimagetool download failed." fi chmod +x "$appimagetool" fi # Create custom AppRun wrapper cat << 'EOF' > "${out_real}/KeePassXC-AppRun" #!/usr/bin/env bash export PATH="$(dirname $0)/usr/bin:${PATH}" export LD_LIBRARY_PATH="$(dirname $0)/usr/lib:${LD_LIBRARY_PATH}" if [ "$1" == "cli" ]; then shift exec keepassxc-cli "$@" elif [ "$1" == "proxy" ]; then shift exec keepassxc-proxy "$@" elif [ -v CHROME_WRAPPER ] || [ -v MOZ_LAUNCHED_CHILD ]; then exec keepassxc-proxy "$@" else exec keepassxc "$@" fi EOF chmod +x "${out_real}/KeePassXC-AppRun" # Find .desktop files, icons, and binaries to deploy local desktop_file="$(find "$appdir" -name "org.keepassxc.KeePassXC.desktop" | head -n1)" local icon="$(find "$appdir" -path '*/application/256x256/apps/keepassxc.png' | head -n1)" local executables="$(find "$appdir" -type f -executable -path '*/bin/keepassxc*' -print0 | xargs -0 -i printf " --executable={}")" logInfo "Collecting libs and patching binaries..." if [ -z "$DOCKER_IMAGE" ]; then "$linuxdeploy" --verbosity=${verbosity} --plugin=qt --appdir="$appdir" --desktop-file="$desktop_file" \ --custom-apprun="${out_real}/KeePassXC-AppRun" --icon-file="$icon" ${executables} else docker run --name "$DOCKER_CONTAINER_NAME" --rm \ --cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse -it \ -v "${out_real}:${out_real}:rw" \ -v "${appdir}:${appdir}:rw" \ -w "$out_real" \ --user $(id -u):$(id -g) \ "$DOCKER_IMAGE" \ bash -c "${linuxdeploy} --verbosity=${verbosity} --plugin=qt \ --appdir='${appdir}' --custom-apprun='${out_real}/KeePassXC-AppRun' \ --desktop-file='${desktop_file}' --icon-file='${icon}' ${executables}" fi if [ $? -ne 0 ]; then exitError "AppDir deployment failed." fi logInfo "Creating AppImage..." local appsign_flag="" local appsign_key_flag="" if ${build_appsign}; then appsign_flag="--sign" appsign_key_flag="--sign-key ${build_key}" fi local appimage_name="KeePassXC-x86_64.AppImage" if [ "" != "$RELEASE_NAME" ]; then appimage_name="KeePassXC-${RELEASE_NAME}-x86_64.AppImage" echo "X-AppImage-Version=${RELEASE_NAME}" >> "$desktop_file" fi # Run appimagetool to package (and possibly sign) AppImage # --no-appstream is required, since it may crash on newer systems # see: https://github.com/AppImage/AppImageKit/issues/856 if ! "$appimagetool" --updateinformation "gh-releases-zsync|keepassxreboot|keepassxc|latest|KeePassXC-*-x86_64.AppImage.zsync" \ ${appsign_flag} ${appsign_key_flag} --no-appstream "$appdir" "${out_real}/${appimage_name}"; then exitError "AppImage creation failed." fi logInfo "Cleaning up temporary files..." ${linuxdeploy_cleanup} ${linuxdeploy_plugin_qt_cleanup} ${appimagetool_cleanup} rm -f "${out_real}/KeePassXC-AppRun" } # ----------------------------------------------------------------------- # build command # ----------------------------------------------------------------------- build() { local build_source_tarball=true local build_snapshot=false local build_snapcraft=false local build_appimage=false local build_generators="" local build_appsign=false local build_key="" local build_vcpkg="" while [ $# -ge 1 ]; do local arg="$1" case "$arg" in -v|--version) RELEASE_NAME="$2" shift ;; -a|--app-name) APP_NAME="$2" shift ;; -s|--source-dir) SRC_DIR="$2" shift ;; -o|--output-dir) OUTPUT_DIR="$2" shift ;; -t|--tag-name) TAG_NAME="$2" shift ;; -d|--docker-image) DOCKER_IMAGE="$2" shift ;; --container-name) DOCKER_CONTAINER_NAME="$2" shift ;; --appsign) build_appsign=true ;; --timestamp) TIMESTAMP_SERVER="$2" shift ;; -k|--key) build_key="$2" shift ;; --snapcraft) build_snapcraft=true ;; --appimage) build_appimage=true ;; --cmake-generator) CMAKE_GENERATOR="$2" shift ;; -c|--cmake-options) CMAKE_OPTIONS="$2" shift ;; --compiler) COMPILER="$2" shift ;; --vcpkg) build_vcpkg="$2" shift ;; -m|--make-options) MAKE_OPTIONS="$2" shift ;; -g|--generators) build_generators="$2" shift ;; -i|--install-prefix) INSTALL_PREFIX="$2" shift ;; -p|--plugins) BUILD_PLUGINS="$2" shift ;; -n|--no-source-tarball) build_source_tarball=false ;; --snapshot) build_snapshot=true ;; -h|--help) printUsage "build" exit ;; *) logError "Unknown option '$arg'\n" printUsage "build" exit 1 ;; esac shift done init # Resolve appsign key to absolute path if under Windows if [[ "${build_key}" && -n "$OS_WINDOWS" ]]; then build_key="$(realpath "${build_key}")" fi if [[ -f ${build_vcpkg} ]]; then CMAKE_OPTIONS="${CMAKE_OPTIONS} -DCMAKE_TOOLCHAIN_FILE=${build_vcpkg} -DX_VCPKG_APPLOCAL_DEPS_INSTALL=ON" fi if ${build_snapshot}; then TAG_NAME="HEAD" local branch=`git rev-parse --abbrev-ref HEAD` logInfo "Using current branch ${branch} to build..." RELEASE_NAME="${RELEASE_NAME}-snapshot" CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_BUILD_TYPE=Snapshot -DOVERRIDE_VERSION=${RELEASE_NAME}" else checkWorkingTreeClean if echo "$TAG_NAME" | grep -qE '\-(alpha|beta)[0-9]+$'; then CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_BUILD_TYPE=PreRelease" logInfo "Checking out pre-release tag '${TAG_NAME}'..." else CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_BUILD_TYPE=Release" logInfo "Checking out release tag '${TAG_NAME}'..." fi if ! git checkout "$TAG_NAME" > /dev/null 2>&1; then exitError "Failed to check out target branch." fi fi if ! ${build_snapshot} && [ -d "$OUTPUT_DIR" ]; then exitError "Output dir '${OUTPUT_DIR}' already exists." fi logInfo "Creating output directory..." if ! mkdir -p "$OUTPUT_DIR"; then exitError "Failed to create output directory!" fi OUTPUT_DIR="$(realpath "$OUTPUT_DIR")" if ${build_source_tarball}; then logInfo "Creating source tarball..." local app_name_lower="$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]')" local prefix="${app_name_lower}-${RELEASE_NAME}" local tarball_name="${prefix}-src.tar" git archive --format=tar "$TAG_NAME" --prefix="${prefix}/" --output="${OUTPUT_DIR}/${tarball_name}" # add .version and .gitrev files to tarball mkdir "${prefix}" echo -n ${RELEASE_NAME} > "${prefix}/.version" echo -n `git rev-parse --short=7 HEAD` > "${prefix}/.gitrev" tar --append --file="${OUTPUT_DIR}/${tarball_name}" "${prefix}/.version" "${prefix}/.gitrev" rm "${prefix}/.version" "${prefix}/.gitrev" rmdir "${prefix}" 2> /dev/null local xz="xz" if ! cmdExists xz; then logWarn "xz not installed. Falling back to bz2..." xz="bzip2" fi $xz -6 -f "${OUTPUT_DIR}/${tarball_name}" fi logInfo "Creating build directory..." mkdir -p "${OUTPUT_DIR}/build-release" cd "${OUTPUT_DIR}/build-release" logInfo "Configuring sources..." for p in ${BUILD_PLUGINS}; do CMAKE_OPTIONS="${CMAKE_OPTIONS} -DWITH_XC_$(echo $p | tr '[:lower:]' '[:upper:]')=On" done if [ -n "$OS_LINUX" ] && ${build_appimage}; then CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_DIST_TYPE=AppImage" # linuxdeploy requires /usr as install prefix INSTALL_PREFIX="/usr" fi if [ -n "$OS_MACOS" ]; then type brew &> /dev/null 2>&1 if [ $? -eq 0 ]; then INSTALL_PREFIX=$(brew --prefix) fi fi # Do not build tests cases CMAKE_OPTIONS="${CMAKE_OPTIONS} -DWITH_TESTS=OFF" if [ "$COMPILER" == "g++" ]; then export CC=gcc elif [ "$COMPILER" == "clang++" ]; then export CC=clang else export CC="$COMPILER" fi export CXX="$COMPILER" if [ -z "$DOCKER_IMAGE" ]; then if [ -n "$OS_MACOS" ]; then # Building on macOS export MACOSX_DEPLOYMENT_TARGET logInfo "Configuring build..." cmake -G "${CMAKE_GENERATOR}" -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES="$(uname -m)" \ -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" ${CMAKE_OPTIONS} "$SRC_DIR" logInfo "Compiling and packaging sources..." cmake --build . -- ${MAKE_OPTIONS} cpack -G "DragNDrop" # Appsign the executables if desired if ${build_appsign}; then logInfo "Signing executable files" appsign "-f" "./${APP_NAME}-${RELEASE_NAME}.dmg" "-k" "${build_key}" fi mv "./${APP_NAME}-${RELEASE_NAME}.dmg" "../${APP_NAME}-${RELEASE_NAME}-$(uname -m).dmg" elif [ -n "$OS_WINDOWS" ]; then # Building on Windows with Msys2 logInfo "Configuring build..." cmake -DCMAKE_BUILD_TYPE=Release -G "${CMAKE_GENERATOR}" -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" \ ${CMAKE_OPTIONS} "$SRC_DIR" logInfo "Compiling and packaging sources..." cmake --build . --config "Release" -- ${MAKE_OPTIONS} # Appsign the executables if desired if ${build_appsign} && [ -f "${build_key}" ]; then logInfo "Signing executable files" appsign "-f" $(find src | grep -Ei 'keepassxc.*(\.exe|\.dll)$') "-k" "${build_key}" fi # Call cpack directly instead of calling make package. # This is important because we want to build the MSI when making a # release. cpack -G "${CPACK_GENERATORS};${build_generators}" mv "${APP_NAME}-"*.* ../ else mkdir -p "${OUTPUT_DIR}/KeePassXC.AppDir" # Building on Linux without Docker container logInfo "Configuring build..." cmake -DCMAKE_BUILD_TYPE=Release ${CMAKE_OPTIONS} \ -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" "$SRC_DIR" logInfo "Compiling sources..." make ${MAKE_OPTIONS} logInfo "Installing to bin dir..." make DESTDIR="${OUTPUT_DIR}/KeePassXC.AppDir" install/strip fi else if ${build_snapcraft}; then logInfo "Building snapcraft docker image..." sudo docker image build -t "$DOCKER_IMAGE" "$(realpath "$SRC_DIR")/ci/snapcraft" logInfo "Launching Docker contain to compile snapcraft..." sudo docker run --name "$DOCKER_CONTAINER_NAME" --rm -it --user $(id -u):$(id -g) \ -v "$(realpath "$SRC_DIR"):/keepassxc" -w "/keepassxc" \ "$DOCKER_IMAGE" snapcraft else mkdir -p "${OUTPUT_DIR}/KeePassXC.AppDir" logInfo "Launching Docker container to compile sources..." docker run --name "$DOCKER_CONTAINER_NAME" --rm \ --cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse \ --user $(id -u):$(id -g) \ -e "CC=${CC}" -e "CXX=${CXX}" -it \ -v "$(realpath "$SRC_DIR"):/keepassxc/src:ro" \ -v "$(realpath "$OUTPUT_DIR"):/keepassxc/out:rw" \ "$DOCKER_IMAGE" \ bash -c "cd /keepassxc/out/build-release && \ cmake -DCMAKE_BUILD_TYPE=Release ${CMAKE_OPTIONS} \ -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} /keepassxc/src && \ make ${MAKE_OPTIONS} && make DESTDIR=/keepassxc/out/KeePassXC.AppDir install/strip" fi if [ 0 -ne $? ]; then exitError "Docker build failed!" fi logInfo "Build finished, Docker container terminated." fi if [ -n "$OS_LINUX" ] && ${build_appimage}; then local appsign_flag="" local appsign_key_flag="" local docker_image_flag="" local docker_container_name_flag="" if ${build_appsign}; then appsign_flag="--appsign" appsign_key_flag="-k ${build_key}" fi if [ "" != "${DOCKER_IMAGE}" ]; then docker_image_flag="-d ${DOCKER_IMAGE}" docker_container_name_flag="--container-name ${DOCKER_CONTAINER_NAME}" fi appimage -a "${OUTPUT_DIR}/KeePassXC.AppDir" -o "${OUTPUT_DIR}" \ ${appsign_flag} ${appsign_key_flag} ${docker_image_flag} ${docker_container_name_flag} fi cleanup logInfo "All done!" } # ----------------------------------------------------------------------- # gpgsign command # ----------------------------------------------------------------------- gpgsign() { local sign_files=() while [ $# -ge 1 ]; do local arg="$1" case "$arg" in -f|--files) while [ "${2:0:1}" != "-" ] && [ $# -ge 2 ]; do sign_files+=("$2") shift done ;; -k|--key|-g|--gpg-key) GPG_KEY="$2" shift ;; -h|--help) printUsage "gpgsign" exit ;; *) logError "Unknown option '$arg'\n" printUsage "gpgsign" exit 1 ;; esac shift done if [ -z "${sign_files}" ]; then logError "Missing arguments, --files is required!\n" printUsage "gpgsign" exit 1 fi for f in "${sign_files[@]}"; do if [ ! -f "$f" ]; then exitError "File '${f}' does not exist or is not a file!" fi logInfo "Signing file '${f}' using release key..." gpg --output "${f}.sig" --armor --local-user "$GPG_KEY" --detach-sig "$f" if [ 0 -ne $? ]; then exitError "Signing failed!" fi logInfo "Creating digest for file '${f}'..." local rp="$(realpath "$f")" local bname="$(basename "$f")" (cd "$(dirname "$rp")"; sha256sum "$bname" > "${bname}.DIGEST") done logInfo "All done!" } # ----------------------------------------------------------------------- # appsign command # ----------------------------------------------------------------------- appsign() { local sign_files=() local key while [ $# -ge 1 ]; do local arg="$1" case "$arg" in -f|--files) while [ "${2:0:1}" != "-" ] && [ $# -ge 2 ]; do sign_files+=("$2") shift done ;; -k|--key|-i|--identity) key="$2" shift ;; -h|--help) printUsage "appsign" exit ;; *) logError "Unknown option '$arg'\n" printUsage "appsign" exit 1 ;; esac shift done if [ -z "${key}" ]; then logError "Missing arguments, --key is required!\n" printUsage "appsign" exit 1 fi if [ -z "${sign_files}" ]; then logError "Missing arguments, --files is required!\n" printUsage "appsign" exit 1 fi for f in "${sign_files[@]}"; do if [ ! -e "${f}" ]; then exitError "File '${f}' does not exist!" fi done if [ -n "$OS_MACOS" ]; then checkXcodeSetup local orig_dir="$(pwd)" local real_src_dir="$(realpath "${SRC_DIR}")" for f in "${sign_files[@]}"; do if [[ ${f: -4} == '.dmg' ]]; then logInfo "Unpacking disk image '${f}'..." local tmp_dir="/tmp/KeePassXC_${RANDOM}" mkdir -p ${tmp_dir}/mnt if ! hdiutil attach -quiet -noautoopen -mountpoint ${tmp_dir}/mnt "${f}"; then exitError "DMG mount failed!" fi cd ${tmp_dir} cp -a ./mnt ./app hdiutil detach -quiet ${tmp_dir}/mnt local app_dir_tmp="./app/KeePassXC.app" if [ ! -d "$app_dir_tmp" ]; then cd "${orig_dir}" exitError "Unpacking failed!" fi elif [[ ${f: -4} == '.app' ]]; then local app_dir_tmp="$f" else logWarn "Skipping non-app file '${f}'..." continue fi logInfo "Signing libraries and frameworks..." if ! find "$app_dir_tmp" \( -name '*.dylib' -o -name '*.so' -o -name '*.framework' \) -print0 | xargs -0 \ xcrun codesign --sign "${key}" --verbose --force --options runtime; then cd "${orig_dir}" exitError "Signing failed!" fi logInfo "Signing executables..." if ! find "${app_dir_tmp}/Contents/MacOS" \( -type f -not -name KeePassXC \) -print0 | xargs -0 \ xcrun codesign --sign "${key}" --verbose --force --options runtime; then cd "${orig_dir}" exitError "Signing failed!" fi # Sign main executable with additional entitlements if ! xcrun codesign --sign "${key}" --verbose --force --options runtime --entitlements \ "${real_src_dir}/share/macosx/keepassxc.entitlements" "${app_dir_tmp}/Contents/MacOS/KeePassXC"; then cd "${orig_dir}" exitError "Signing failed!" fi if [[ ${f: -4} == '.dmg' ]]; then logInfo "Repacking disk image..." hdiutil create \ -volname "KeePassXC" \ -size $((1000 * ($(du -sk ./app | cut -f1) + 5000))) \ -srcfolder ./app \ -fs HFS+ \ -fsargs "-c c=64,a=16,e=16" \ -format UDBZ \ "${tmp_dir}/$(basename "${f}")" cd "${orig_dir}" cp -f "${tmp_dir}/$(basename "${f}")" "${f}" rm -Rf ${tmp_dir} fi logInfo "File '${f}' successfully signed." done elif [ -n "$OS_WINDOWS" ]; then if [[ ! -f "${key}" ]]; then exitError "Appsign key file was not found! (${key})" fi logInfo "Using appsign key ${key}." IFS=$'\n' read -s -r -p "Key password: " password echo for f in "${sign_files[@]}"; do ext=${f: -4} if [[ $ext == ".msi" || $ext == ".exe" || $ext == ".dll" ]]; then # Make sure we can find the signtool checkSigntoolCommandExists # osslsigncode does not succeed at signing MSI files at this time... logInfo "Signing file '${f}' using Microsoft signtool..." signtool sign -f "${key}" -p "${password}" -d "KeePassXC" -td sha256 \ -fd sha256 -tr "${TIMESTAMP_SERVER}" "${f}" if [ 0 -ne $? ]; then exitError "Signing failed!" fi else logInfo "Skipping non-executable file '${f}'..." fi done else exitError "Unsupported platform for code signing!\n" fi logInfo "All done!" } # ----------------------------------------------------------------------- # notarize command # ----------------------------------------------------------------------- notarize() { local notarize_files=() local ac_username local ac_keychain="AC_PASSWORD" while [ $# -ge 1 ]; do local arg="$1" case "$arg" in -f|--files) while [ "${2:0:1}" != "-" ] && [ $# -ge 2 ]; do notarize_files+=("$2") shift done ;; -u|--username) ac_username="$2" shift ;; -c|--keychain) ac_keychain="$2" shift ;; -h|--help) printUsage "notarize" exit ;; *) logError "Unknown option '$arg'\n" printUsage "notarize" exit 1 ;; esac shift done if [ -z "$OS_MACOS" ]; then exitError "Notarization is only supported on macOS!" fi if [ -z "${notarize_files}" ]; then logError "Missing arguments, --files is required!\n" printUsage "notarize" exit 1 fi if [ -z "$ac_username" ]; then logError "Missing arguments, --username is required!" printUsage "notarize" exit 1 fi for f in "${notarize_files[@]}"; do if [[ ${f: -4} != '.dmg' ]]; then logWarn "Skipping non-DMG file '${f}'..." continue fi logInfo "Submitting disk image '${f}' for notarization..." local status status="$(xcrun altool --notarize-app \ --primary-bundle-id "org.keepassxc.keepassxc" \ --username "${ac_username}" \ --password "@keychain:${ac_keychain}" \ --file "${f}")" if [ 0 -ne $? ]; then logError "Submission failed!" exitError "Error message:\n${status}" fi local ticket="$(echo "${status}" | grep -oE '[a-f0-9-]+$')" logInfo "Submission successful. Ticket ID: ${ticket}." logInfo "Waiting for notarization to finish (this may take a while)..." while true; do echo -n "." status="$(xcrun altool --notarization-info "${ticket}" \ --username "${ac_username}" \ --password "@keychain:${ac_keychain}" 2> /dev/null)" if echo "$status" | grep -q "Status Code: 0"; then logInfo "\nNotarization successful." break elif echo "$status" | grep -q "Status Code"; then logError "\nNotarization failed!" exitError "Error message:\n${status}" fi sleep 5 done logInfo "Stapling ticket to disk image..." xcrun stapler staple "${f}" if [ 0 -ne $? ]; then exitError "Stapling failed!" fi logInfo "Disk image successfully notarized." done } # ----------------------------------------------------------------------- # i18n command # ----------------------------------------------------------------------- i18n() { local cmd="$1" if [ -z "$cmd" ]; then logError "No subcommand specified.\n" printUsage i18n exit 1 elif [ "$cmd" != "tx-push" ] && [ "$cmd" != "tx-pull" ] && [ "$cmd" != "lupdate" ]; then logError "Unknown subcommand: '${cmd}'\n" printUsage i18n exit 1 fi shift checkGitRepository if [ "$cmd" == "lupdate" ]; then if [ ! -d share/translations ]; then logError "Command must be called from repository root directory." exit 1 fi checkQt5LUpdateExists logInfo "Updating source translation file..." LUPDATE=lupdate-qt5 if ! command -v $LUPDATE > /dev/null; then LUPDATE=lupdate fi $LUPDATE -no-ui-lines -disable-heuristic similartext -locations none -no-obsolete src \ -ts share/translations/keepassxc_en.ts $@ return 0 fi checkTransifexCommandExists local branch="$(git branch --show-current 2>&1)" local real_branch="$branch" if [[ "$branch" =~ ^release/ ]]; then logInfo "Release branch, setting language resource to master branch." branch="master" elif [ "$branch" != "develop" ] && [ "$branch" != "master" ]; then logError "Must be on master or develop branch!" exit 1 fi local resource="keepassxc.share-translations-keepassxc-en-ts--${branch}" if [ "$cmd" == "tx-push" ]; then echo -e "This will push the \e[1m'en'\e[0m source file from the current branch to Transifex:\n" >&2 echo -e " \e[1m${real_branch}\e[0m -> \e[1m${resource}\e[0m\n" >&2 echo -n "Continue? [y/N] " >&2 read -r yesno if [ "$yesno" != "y" ] && [ "$yesno" != "Y" ]; then logError "Push aborted." exit 1 fi logInfo "Pushing source translation file to Transifex..." tx push -s --use-git-timestamps -r "$resource" $@ elif [ "$cmd" == "tx-pull" ]; then logInfo "Pulling updated translations from Transifex..." tx pull -af --minimum-perc=45 --parallel -r "$resource" $@ fi } # ----------------------------------------------------------------------- # parse global command line # ----------------------------------------------------------------------- MODE="$1" shift || true if [ -z "$MODE" ]; then logError "Missing arguments!\n" printUsage exit 1 elif [ "help" == "$MODE" ]; then printUsage "$1" exit elif [ "check" == "$MODE" ] || [ "merge" == "$MODE" ] || [ "build" == "$MODE" ] \ || [ "gpgsign" == "$MODE" ] || [ "appsign" == "$MODE" ]|| [ "notarize" == "$MODE" ] \ || [ "appimage" == "$MODE" ]|| [ "i18n" == "$MODE" ]; then ${MODE} "$@" else printUsage "$MODE" fi