#!/usr/bin/env bash
#
# KeePassXC Release Preparation Helper
# Copyright (C) 2017 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) 2017 KeePassXC Team \n\n"
# -----------------------------------------------------------------------
# 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_OPTIONS=""
CPACK_GENERATORS="WIX;ZIP"
COMPILER="g++"
MAKE_OPTIONS="-j8"
BUILD_PLUGINS="all"
INSTALL_PREFIX="/usr/local"
ORIG_BRANCH=""
ORIG_CWD="$(pwd)"
# -----------------------------------------------------------------------
# helper functions
# -----------------------------------------------------------------------
printUsage() {
local cmd
if [ "" == "$1" ] || [ "help" == "$1" ]; then
cmd="COMMAND"
elif [ "check" == "$1" ] || [ "merge" == "$1" ] || [ "build" == "$1" ] \
|| [ "gpgsign" == "$1" ] || [ "appsign" == "$1" ] || [ "appimage" == "$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
help Show help for the given command
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
-k, --key Specify the App Signing Key/Identity
-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 Signing Key or Apple Developer ID
-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
fi
}
logInfo() {
printf "\e[1m[ \e[34mINFO\e[39m ]\e[0m $1\n"
}
logError() {
printf "\e[1m[ \e[31mERROR\e[39m ]\e[0m $1\n" >&2
}
init() {
if [ "" == "$RELEASE_NAME" ]; then
logError "Missing arguments, --version is required!\n"
printUsage "check"
exit 1
fi
if [ "" == "$TAG_NAME" ]; then
TAG_NAME="$RELEASE_NAME"
fi
if [ "" == "$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
}
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 ]; then
exitError "Source directory is not a valid Git repository!"
fi
}
checkReleaseDoesNotExist() {
git tag | grep -q "^$TAG_NAME$"
if [ $? -eq 0 ]; then
exitError "Release '$RELEASE_NAME' (tag: '$TAG_NAME') already exists!"
fi
}
checkWorkingTreeClean() {
git diff-index --quiet HEAD --
if [ $? -ne 0 ]; then
exitError "Current working tree is not clean! Please commit or unstage any changes."
fi
}
checkSourceBranchExists() {
git rev-parse "$SOURCE_BRANCH" > /dev/null 2>&1
if [ $? -ne 0 ]; then
exitError "Source branch '$SOURCE_BRANCH' does not exist!"
fi
}
checkTargetBranchExists() {
git rev-parse "$TARGET_BRANCH" > /dev/null 2>&1
if [ $? -ne 0 ]; 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-)"
grep -q "${app_name_upper}_VERSION_MAJOR \"${major_num}\"" CMakeLists.txt
if [ $? -ne 0 ]; then
exitError "${app_name_upper}_VERSION_MAJOR not updated to '${major_num}' in CMakeLists.txt!"
fi
grep -q "${app_name_upper}_VERSION_MINOR \"${minor_num}\"" CMakeLists.txt
if [ $? -ne 0 ]; then
exitError "${app_name_upper}_VERSION_MINOR not updated to '${minor_num}' in CMakeLists.txt!"
fi
grep -q "${app_name_upper}_VERSION_PATCH \"${patch_num}\"" CMakeLists.txt
if [ $? -ne 0 ]; then
exitError "${app_name_upper}_VERSION_PATCH not updated to '${patch_num}' in CMakeLists.txt!"
fi
}
checkChangeLog() {
if [ ! -f CHANGELOG ]; then
exitError "No CHANGELOG file found!"
fi
grep -qPzo "${RELEASE_NAME} \(\d{4}-\d{2}-\d{2}\)\n=+\n" CHANGELOG
if [ $? -ne 0 ]; then
exitError "'CHANGELOG' 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
grep -qPzo "" share/linux/org.keepassxc.KeePassXC.appdata.xml
if [ $? -ne 0 ]; then
exitError "'share/linux/org.keepassxc.KeePassXC.appdata.xml' has not been updated to the '${RELEASE_NAME}' release!"
fi
}
checkSnapcraft() {
if [ ! -f snapcraft.yaml ]; then
echo "No snapcraft file found!"
return
fi
grep -qPzo "version: ${RELEASE_NAME}" snapcraft.yaml
if [ $? -ne 0 ]; then
exitError "'snapcraft.yaml' has not been updated to the '${RELEASE_NAME}' release!"
fi
grep -qPzo "KEEPASSXC_BUILD_TYPE=Release" snapcraft.yaml
if [ $? -ne 0 ]; 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
}
checkCodesignCommandExists() {
if ! cmdExists codesign; then
exitError "codesign command not found on the PATH! Please check that you have correctly installed 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
checkTargetBranchExists
logInfo "Checking out '${SOURCE_BRANCH}'..."
git checkout "$SOURCE_BRANCH"
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
# -----------------------------------------------------------------------
# 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 ;;
-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
logInfo "Updating language files..."
./share/translations/update.sh update
./share/translations/update.sh pull
if [ 0 -ne $? ]; then
exitError "Updating translations failed!"
fi
git diff-index --quiet HEAD --
if [ $? -ne 0 ]; then
git add ./share/translations/*
logInfo "Committing changes..."
if [ "" == "$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(?=\n)" \
CHANGELOG | grep -Pzo '(?<=\n\n)(.|\n)+' | tr -d \\0)
COMMIT_MSG="Release ${RELEASE_NAME}"
logInfo "Checking out target branch '${TARGET_BRANCH}'..."
git checkout "$TARGET_BRANCH"
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 [ "" == "$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
logError "AppDir does not exist, please create one with 'make install'!\n"
exit 1
elif [ -e "${appdir}/AppRun" ]; then
logError "AppDir has already been run through linuxdeploy, please create a fresh AppDir with 'make install'.\n"
exit 1
fi
appdir="$(realpath "$appdir")"
local out="${OUTPUT_DIR}"
if [ "" == "$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 --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}"
curl -L "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" > "$linuxdeploy"
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}"
curl -L "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" > "$linuxdeploy_plugin_qt"
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}"
curl -L "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" > "$appimagetool"
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" -name 'keepassxc.png' | grep -P 'application/256x256/apps/keepassxc.png$' | head -n1)"
local executables="$(IFS=$'\n' find "$appdir" | grep -P '/usr/bin/keepassxc[^/]*$' | xargs -i printf " --executable={}")"
logInfo "Collecting libs and patching binaries..."
if [ "" == "$DOCKER_IMAGE" ]; then
"$linuxdeploy" --verbosity=${verbosity} --plugin=qt --appdir="$appdir" --desktop-file="$desktop_file" \
--custom-apprun="${out_real}/KeePassXC-AppRun" --icon-file="$icon" ${executables} \
--library=$(ldconfig -p | grep x86-64 | grep -oP '/[^\s]+/libgpg-error\.so\.\d+$' | head -n1)
else
desktop_file="${desktop_file//${appdir}/\/keepassxc\/AppDir}"
icon="${icon//${appdir}/\/keepassxc\/AppDir}"
executables="${executables//${appdir}/\/keepassxc\/AppDir}"
docker run --name "$DOCKER_CONTAINER_NAME" --rm \
--cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse \
-v "${appdir}:/keepassxc/AppDir:rw" \
-v "${out_real}:/keepassxc/out:rw" \
"$DOCKER_IMAGE" \
bash -c "cd /keepassxc/out && ${linuxdeploy} --verbosity=${verbosity} --plugin=qt --appdir=/keepassxc/AppDir \
--custom-apprun="/keepassxc/out/KeePassXC-AppRun" --desktop-file=${desktop_file} --icon-file=${icon} ${executables} \
--library=\$(ldconfig -p | grep x86-64 | grep -oP '/[^\s]+/libgpg-error\.so\.\d+$' | head -n1)"
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"
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
"$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}"
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=""
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 ;;
-k|--key)
build_key="$2"
shift ;;
--snapcraft)
build_snapcraft=true ;;
--appimage)
build_appimage=true ;;
-c|--cmake-options)
CMAKE_OPTIONS="$2"
shift ;;
--compiler)
COMPILER="$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
if [[ ${build_appsign} && ! -f ${build_key} ]]; then
exitError "--appsign specified with invalid key file\n"
fi
init
OUTPUT_DIR="$(realpath "$OUTPUT_DIR")"
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"
else
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"
fi
logInfo "Creating output directory..."
mkdir -p "$OUTPUT_DIR"
if [ $? -ne 0 ]; 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}"
if ! ${build_snapshot}; then
# add .version file to tar
mkdir "${prefix}"
echo -n ${RELEASE_NAME} > "${prefix}/.version"
tar --append --file="${OUTPUT_DIR}/${tarball_name}" "${prefix}/.version"
rm "${prefix}/.version"
rmdir "${prefix}" 2> /dev/null
fi
xz -6 "${OUTPUT_DIR}/${tarball_name}"
fi
if ! ${build_snapshot} && [ -e "${OUTPUT_DIR}/build-release" ]; then
logInfo "Cleaning existing build directory..."
rm -r "${OUTPUT_DIR}/build-release" 2> /dev/null
if [ $? -ne 0 ]; then
exitError "Failed to clean existing build directory, please do it manually."
fi
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)" == "GNU/Linux" ] && ${build_appimage}; then
CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_DIST_TYPE=AppImage"
# linuxdeploy requires /usr as install prefix
INSTALL_PREFIX="/usr"
fi
if [ "$COMPILER" == "g++" ]; then
export CC=gcc
elif [ "$COMPILER" == "clang++" ]; then
export CC=clang
fi
export CXX="$COMPILER"
if [ "" == "$DOCKER_IMAGE" ]; then
if [ "$(uname -s)" == "Darwin" ]; then
# Building on macOS
export MACOSX_DEPLOYMENT_TARGET=10.10
logInfo "Configuring build..."
cmake -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" \
-DCMAKE_PREFIX_PATH="/usr/local/opt/qt/lib/cmake" \
${CMAKE_OPTIONS} "$SRC_DIR"
logInfo "Compiling and packaging sources..."
make ${MAKE_OPTIONS} package
# 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" ../
elif [ "$(uname -o)" == "Msys" ]; then
# Building on Windows with Msys2
logInfo "Configuring build..."
cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off -G"MSYS Makefiles" \
-DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" ${CMAKE_OPTIONS} "$SRC_DIR"
logInfo "Compiling and packaging sources..."
mingw32-make ${MAKE_OPTIONS} preinstall
# Appsign the executables if desired
if [[ ${build_appsign} ]]; then
logInfo "Signing executable files"
appsign "-f" $(find src | grep -P '\.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}"
# Inject the portable config into the zip build and rename
for filename in ${APP_NAME}-*.zip; do
logInfo "Creating portable zip file"
local folder=$(echo ${filename} | sed -r 's/(.*)\.zip/\1/')
python -c 'import zipfile,sys ; zipfile.ZipFile(sys.argv[1],"a").write(sys.argv[2],sys.argv[3])' \
${filename} ${SRC_DIR}/share/keepassxc.ini ${folder}/keepassxc.ini
mv ${filename} ${folder}-portable.zip
done
mv "${APP_NAME}-"*.* ../
else
mkdir -p "${OUTPUT_DIR}/KeePassXC.AppDir"
# Building on Linux without Docker container
logInfo "Configuring build..."
cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off ${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 \
-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 \
-e "CC=${CC}" -e "CXX=${CXX}" \
-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 -DWITH_TESTS=Off ${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)" == "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 [ ! -f "${f}" ]; then
exitError "File '${f}' does not exist or is not a file!"
fi
done
if [ "$(uname -s)" == "Darwin" ]; then
checkCodesignCommandExists
local orig_dir="$(pwd)"
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
hdiutil attach -quiet -noautoopen -mountpoint ${tmp_dir}/mnt "${f}"
cd ${tmp_dir}
cp -a ./mnt ./app
hdiutil detach -quiet ${tmp_dir}/mnt
if [ ! -d ./app/KeePassXC.app ]; then
cd "${orig_dir}"
exitError "Unpacking failed!"
fi
logInfo "Signing app using codesign..."
codesign --sign "${key}" --verbose --deep --entitlements ${orig_dir}/share/macosx/keepassxc.entitlements ./app/KeePassXC.app
if [ 0 -ne $? ]; then
cd "${orig_dir}"
exitError "Signing failed!"
fi
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}
else
logInfo "Skipping non-DMG file '${f}'..."
fi
done
elif [ "$(uname -o)" == "Msys" ]; then
if [[ ! -f "${key}" ]]; then
exitError "Key file was not found!"
fi
read -s -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" \
-t "http://timestamp.comodoca.com/authenticode" "${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!"
}
# -----------------------------------------------------------------------
# parse global command line
# -----------------------------------------------------------------------
MODE="$1"
shift
if [ "" == "$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" ] || [ "appimage" == "$MODE" ]; then
${MODE} "$@"
else
printUsage "$MODE"
fi