mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-12-29 01:06:27 -05:00
1494 lines
48 KiB
Bash
Executable File
1494 lines
48 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# KeePassXC Release Preparation Helper
|
|
# Copyright (C) 2021 KeePassXC team <https://keepassxc.org/>
|
|
#
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
printf "\e[1m\e[32mKeePassXC\e[0m Release Preparation Helper\n"
|
|
printf "Copyright (C) 2021 KeePassXC Team <https://keepassxc.org/>\n\n"
|
|
|
|
set -eE -o pipefail
|
|
|
|
# -----------------------------------------------------------------------
|
|
# global default values
|
|
# -----------------------------------------------------------------------
|
|
RELEASE_NAME=""
|
|
APP_NAME="KeePassXC"
|
|
SRC_DIR="."
|
|
GPG_KEY="CFB4C2166397D0D2"
|
|
GPG_GIT_KEY=""
|
|
OUTPUT_DIR="release"
|
|
SOURCE_BRANCH=""
|
|
TARGET_BRANCH="master"
|
|
TAG_NAME=""
|
|
DOCKER_IMAGE=""
|
|
DOCKER_CONTAINER_NAME="keepassxc-build-container"
|
|
CMAKE_GENERATOR="Ninja"
|
|
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.13
|
|
GREP="grep"
|
|
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')
|
|
--target-branch Target branch to merge to (default: '${TARGET_BRANCH}')
|
|
-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() {
|
|
logError "$1"
|
|
cleanup
|
|
exit 1
|
|
}
|
|
|
|
exitTrap() {
|
|
exitError "Existing upon user request..."
|
|
}
|
|
|
|
cmdExists() {
|
|
command -v "$1" &> /dev/null
|
|
}
|
|
|
|
checkGrepCompat() {
|
|
if ! grep -qPzo test <(echo test) 2> /dev/null; then
|
|
if [ -e /usr/local/opt/grep/libexec/gnubin/grep ]; then
|
|
GREP="/usr/local/opt/grep/libexec/gnubin/grep"
|
|
else
|
|
exitError "Incompatible grep implementation! If on macOS, please run 'brew install grep'."
|
|
fi
|
|
fi
|
|
}
|
|
|
|
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 | $GREP -q "^$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
|
|
}
|
|
|
|
checkTargetBranchExists() {
|
|
if ! git rev-parse "$TARGET_BRANCH" > /dev/null 2>&1; then
|
|
exitError "Target branch '$TARGET_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 -qPzo "## ${RELEASE_NAME} \(\d{4}-\d{2}-\d{2}\)\n" 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 -qPzo "<release version=\"${RELEASE_NAME}\" date=\"\d{4}-\d{2}-\d{2}\">" 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
|
|
}
|
|
|
|
checkSnapcraft() {
|
|
if [ ! -f snap/snapcraft.yaml ]; then
|
|
echo "Could not find snap/snapcraft.yaml!"
|
|
return
|
|
fi
|
|
|
|
if ! $GREP -qPzo "KEEPASSXC_BUILD_TYPE=Release" snap/snapcraft.yaml; then
|
|
exitError "'snapcraft.yaml' is not set for a release build!"
|
|
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..."
|
|
|
|
checkGrepCompat
|
|
|
|
checkSourceDirExists
|
|
|
|
logInfo "Changing to source directory..."
|
|
cd "${SRC_DIR}"
|
|
|
|
logInfo "Validating toolset and repository..."
|
|
|
|
checkTransifexCommandExists
|
|
checkQt5LUpdateExists
|
|
checkGitRepository
|
|
checkReleaseDoesNotExist
|
|
checkWorkingTreeClean
|
|
checkSourceBranchExists
|
|
checkTargetBranchExists
|
|
|
|
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
|
|
checkSnapcraft
|
|
|
|
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 exitTrap SIGINT SIGTERM 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 ;;
|
|
|
|
--target-branch)
|
|
TARGET_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
|
|
|
|
CHANGELOG=$($GREP -Pzo "(?<=${RELEASE_NAME} \(\d{4}-\d{2}-\d{2}\)\n\n)\n?(?:.|\n)+?\n(?=## )" CHANGELOG.md \
|
|
| sed 's/^### //' | tr -d \\0)
|
|
COMMIT_MSG="Release ${RELEASE_NAME}"
|
|
|
|
logInfo "Checking out target branch '${TARGET_BRANCH}'..."
|
|
git checkout "$TARGET_BRANCH" > /dev/null 2>&1
|
|
|
|
logInfo "Merging '${SOURCE_BRANCH}' into '${TARGET_BRANCH}'..."
|
|
|
|
git merge "$SOURCE_BRANCH" --no-ff -m "$COMMIT_MSG" -m "${CHANGELOG}" "$SOURCE_BRANCH" -S"$GPG_GIT_KEY"
|
|
|
|
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
|
|
|
|
cleanup
|
|
|
|
logInfo "All done!"
|
|
logInfo "Please merge the release branch back into the develop branch now and then push your changes."
|
|
logInfo "Don't forget to also 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}" && "$(uname -o)" == "Msys" ]]; 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
|
|
checkGrepCompat
|
|
checkWorkingTreeClean
|
|
|
|
if $(echo "$TAG_NAME" | $GREP -qP "\-(alpha|beta)\\d+\$"); 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
|
|
git checkout "$TAG_NAME" > /dev/null 2>&1
|
|
fi
|
|
|
|
OUTPUT_DIR="$(realpath "$OUTPUT_DIR")"
|
|
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
|
|
|
|
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 "${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 [ "$(uname -o 2> /dev/null)" == "GNU/Linux" ] && ${build_appimage}; then
|
|
CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_DIST_TYPE=AppImage"
|
|
# linuxdeploy requires /usr as install prefix
|
|
INSTALL_PREFIX="/usr"
|
|
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 [ "$(uname -s)" == "Darwin" ]; 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 [ "$(uname -o)" == "Msys" ]; 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 -Pi '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 [ "$(uname -o 2> /dev/null)" == "GNU/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 [ "$(uname -s)" == "Darwin" ]; then
|
|
checkXcodeSetup
|
|
checkGrepCompat
|
|
|
|
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 [ "$(uname -o)" == "Msys" ]; 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 [ "$(uname -s)" != "Darwin" ]; 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}" 2> /dev/null)"
|
|
|
|
if [ 0 -ne $? ]; then
|
|
logError "Submission failed!"
|
|
exitError "Error message:\n${status}"
|
|
fi
|
|
|
|
local ticket="$(echo "${status}" | $GREP -oP "[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=60 --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
|