keepassxc
/
release-tool
1471 строка · 47.2 Кб
1#!/usr/bin/env bash
2#
3# KeePassXC Release Preparation Helper
4# Copyright (C) 2021 KeePassXC team <https://keepassxc.org/>
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 2 or (at your option)
9# version 3 of the License.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19printf "\e[1m\e[32mKeePassXC\e[0m Release Preparation Helper\n"
20printf "Copyright (C) 2021 KeePassXC Team <https://keepassxc.org/>\n\n"
21
22set -eE -o pipefail
23
24if [ "$(uname -s)" == "Linux" ]; then
25OS_LINUX="1"
26elif [ "$(uname -s)" == "Darwin" ]; then
27OS_MACOS="1"
28elif [ "$(uname -o)" == "Msys" ]; then
29OS_WINDOWS="1"
30fi
31
32# -----------------------------------------------------------------------
33# global default values
34# -----------------------------------------------------------------------
35RELEASE_NAME=""
36APP_NAME="KeePassXC"
37SRC_DIR="."
38GPG_KEY="CFB4C2166397D0D2"
39GPG_GIT_KEY=""
40OUTPUT_DIR="release"
41SOURCE_BRANCH=""
42TAG_NAME=""
43DOCKER_IMAGE=""
44DOCKER_CONTAINER_NAME="keepassxc-build-container"
45CMAKE_GENERATOR="Unix Makefiles"
46CMAKE_OPTIONS=""
47CPACK_GENERATORS="WIX;ZIP"
48COMPILER="g++"
49MAKE_OPTIONS="-j$(getconf _NPROCESSORS_ONLN)"
50BUILD_PLUGINS="all"
51INSTALL_PREFIX="/usr/local"
52ORIG_BRANCH=""
53ORIG_CWD="$(pwd)"
54MACOSX_DEPLOYMENT_TARGET=10.15
55TIMESTAMP_SERVER="http://timestamp.sectigo.com"
56
57# -----------------------------------------------------------------------
58# helper functions
59# -----------------------------------------------------------------------
60printUsage() {
61local cmd
62if [ -z "$1" ] || [ "help" == "$1" ]; then
63cmd="COMMAND"
64elif [ "check" == "$1" ] || [ "merge" == "$1" ] || [ "build" == "$1" ] || [ "gpgsign" == "$1" ] || \
65[ "appsign" == "$1" ] || [ "notarize" == "$1" ] || [ "appimage" == "$1" ] || [ "i18n" == "$1" ]; then
66cmd="$1"
67else
68logError "Unknown command: '$1'\n"
69cmd="COMMAND"
70fi
71
72printf "\e[1mUsage:\e[0m $(basename "$0") $cmd [OPTIONS, ...]\n"
73
74if [ "COMMAND" == "$cmd" ]; then
75cat << EOF
76
77Commands:
78check Perform a dry-run check, nothing is changed
79merge Merge release branch into main branch and create release tags
80build Build and package binary release from sources
81gpgsign Sign previously compiled release packages with GPG
82appsign Sign binaries with code signing certificates on Windows and macOS
83notarize Submit macOS application DMG for notarization
84help Show help for the given command
85i18n Update translation files and pull from or push to Transifex
86EOF
87elif [ "merge" == "$cmd" ]; then
88cat << EOF
89
90Merge release branch into main branch and create release tags
91
92Options:
93-v, --version Release version number or name (required)
94-a, --app-name Application name (default: '${APP_NAME}')
95-s, --source-dir Source directory (default: '${SRC_DIR}')
96-k, --key GPG key used to sign the merge commit and release tag,
97leave empty to let Git choose your default key
98(default: '${GPG_GIT_KEY}')
99-r, --release-branch Source release branch to merge from (default: 'release/VERSION')
100-t, --tag-name Override release tag name (defaults to version number)
101-h, --help Show this help
102EOF
103elif [ "build" == "$cmd" ]; then
104cat << EOF
105
106Build and package binary release from sources
107
108Options:
109-v, --version Release version number or name (required)
110-a, --app-name Application name (default: '${APP_NAME}')
111-s, --source-dir Source directory (default: '${SRC_DIR}')
112-o, --output-dir Output directory where to build the release
113(default: '${OUTPUT_DIR}')
114-t, --tag-name Release tag to check out (defaults to version number)
115-b, --build Build sources after exporting release
116-d, --docker-image Use the specified Docker image to compile the application.
117The image must have all required build dependencies installed.
118This option has no effect if --build is not set.
119--container-name Docker container name (default: '${DOCKER_CONTAINER_NAME}')
120The container must not exist already
121--snapcraft Create and use docker image to build snapcraft distribution.
122This option has no effect if --docker-image is not set.
123--appimage Build a Linux AppImage after compilation.
124If this option is set, --install-prefix has no effect
125--appsign Perform platform specific App Signing before packaging
126--timestamp Explicitly set the timestamp server to use for appsign (default: '${TIMESTAMP_SERVER}')
127--vcpkg Specify VCPKG toolchain file (example: ~/vcpkg/scripts/buildsystems/vcpkg.cmake)
128-k, --key Specify the App Signing Key/Identity
129--cmake-generator Override the default CMake generator (Default: Ninja)
130-c, --cmake-options Additional CMake options for compiling the sources
131--compiler Compiler to use (default: '${COMPILER}')
132-m, --make-options Make options for compiling sources (default: '${MAKE_OPTIONS}')
133-g, --generators Additional CPack generators (default: '${CPACK_GENERATORS}')
134-i, --install-prefix Install prefix (default: '${INSTALL_PREFIX}')
135-p, --plugins Space-separated list of plugins to build
136(default: ${BUILD_PLUGINS})
137--snapshot Don't checkout the release tag
138-n, --no-source-tarball Don't build source tarball
139-h, --help Show this help
140EOF
141elif [ "gpgsign" == "$cmd" ]; then
142cat << EOF
143
144Sign previously compiled release packages with GPG
145
146Options:
147-f, --files Files to sign (required)
148-k, --key GPG key used to sign the files (default: '${GPG_KEY}')
149-h, --help Show this help
150EOF
151elif [ "appsign" == "$cmd" ]; then
152cat << EOF
153
154Sign binaries with code signing certificates on Windows and macOS
155
156Options:
157-f, --files Files to sign (required)
158-k, --key, -i, --identity
159Signing Key or Apple Developer ID (required)
160--timestamp Explicitly set the timestamp server to use for appsign (default: '${TIMESTAMP_SERVER}')
161-u, --username Apple username for notarization (required on macOS)
162-h, --help Show this help
163EOF
164elif [ "notarize" == "$cmd" ]; then
165cat << EOF
166
167Submit macOS application DMG for notarization
168
169Options:
170-f, --files Files to notarize (required)
171-u, --username Apple username for notarization (required)
172-c, --keychain Apple keychain entry name storing the notarization
173app password (default: 'AC_PASSWORD')
174-h, --help Show this help
175EOF
176elif [ "appimage" == "$cmd" ]; then
177cat << EOF
178
179Generate Linux AppImage from 'make install' AppDir
180
181Options:
182-a, --appdir Input AppDir (required)
183-v, --version KeePassXC version
184-o, --output-dir Output directory where to build the AppImage
185(default: '${OUTPUT_DIR}')
186-d, --docker-image Use the specified Docker image to build the AppImage.
187The image must have all required build dependencies installed.
188--container-name Docker container name (default: '${DOCKER_CONTAINER_NAME}')
189The container must not exist already
190--appsign Embed a PGP signature into the AppImage
191-k, --key The PGP Signing Key
192--verbosity linuxdeploy verbosity (default: 3)
193-h, --help Show this help
194EOF
195elif [ "i18n" == "$cmd" ]; then
196cat << EOF
197
198Update translation files and pull from or push to Transifex
199
200Subcommands:
201tx-push Push source translation file to Transifex
202tx-pull Pull updated translations from Transifex
203lupdate Update source translation file from C++ sources
204EOF
205fi
206}
207
208logInfo() {
209printf "\e[1m[ \e[34mINFO\e[39m ]\e[0m $1\n"
210}
211
212logWarn() {
213printf "\e[1m[ \e[33mWARNING\e[39m ]\e[0m $1\n"
214}
215
216logError() {
217printf "\e[1m[ \e[31mERROR\e[39m ]\e[0m $1\n" >&2
218}
219
220init() {
221if [ -z "$RELEASE_NAME" ]; then
222logError "Missing arguments, --version is required!\n"
223printUsage "check"
224exit 1
225fi
226
227if [ -z "$TAG_NAME" ]; then
228TAG_NAME="$RELEASE_NAME"
229fi
230
231if [ -z "$SOURCE_BRANCH" ]; then
232SOURCE_BRANCH="release/${RELEASE_NAME}"
233fi
234
235ORIG_CWD="$(pwd)"
236SRC_DIR="$(realpath "$SRC_DIR")"
237cd "$SRC_DIR" > /dev/null 2>&1
238ORIG_BRANCH="$(git rev-parse --abbrev-ref HEAD 2> /dev/null)"
239cd "$ORIG_CWD"
240}
241
242cleanup() {
243logInfo "Checking out original branch..."
244if [ "" != "$ORIG_BRANCH" ]; then
245git checkout "$ORIG_BRANCH" > /dev/null 2>&1
246fi
247logInfo "Leaving source directory..."
248cd "$ORIG_CWD"
249}
250
251exitError() {
252cleanup
253logError "$1"
254exit 1
255}
256
257cmdExists() {
258command -v "$1" &> /dev/null
259}
260
261checkSourceDirExists() {
262if [ ! -d "$SRC_DIR" ]; then
263exitError "Source directory '${SRC_DIR}' does not exist!"
264fi
265}
266
267checkOutputDirDoesNotExist() {
268if [ -e "$OUTPUT_DIR" ]; then
269exitError "Output directory '$OUTPUT_DIR' already exists. Please choose a different location!"
270fi
271}
272
273checkGitRepository() {
274if [ ! -d .git ] || [ ! -f CHANGELOG.md ]; then
275exitError "Source directory is not a valid Git repository!"
276fi
277}
278
279checkReleaseDoesNotExist() {
280if [ $(git tag -l $TAG_NAME) ]; then
281exitError "Release '$RELEASE_NAME' (tag: '$TAG_NAME') already exists!"
282fi
283}
284
285checkWorkingTreeClean() {
286if ! git diff-index --quiet HEAD --; then
287exitError "Current working tree is not clean! Please commit or unstage any changes."
288fi
289}
290
291checkSourceBranchExists() {
292if ! git rev-parse "$SOURCE_BRANCH" > /dev/null 2>&1; then
293exitError "Source branch '$SOURCE_BRANCH' does not exist!"
294fi
295}
296
297checkVersionInCMake() {
298local app_name_upper="$(echo "$APP_NAME" | tr '[:lower:]' '[:upper:]')"
299local major_num="$(echo ${RELEASE_NAME} | cut -f1 -d.)"
300local minor_num="$(echo ${RELEASE_NAME} | cut -f2 -d.)"
301local patch_num="$(echo ${RELEASE_NAME} | cut -f3 -d. | cut -f1 -d-)"
302
303if ! grep -q "${app_name_upper}_VERSION_MAJOR \"${major_num}\"" CMakeLists.txt; then
304exitError "${app_name_upper}_VERSION_MAJOR not updated to '${major_num}' in CMakeLists.txt!"
305fi
306
307if ! grep -q "${app_name_upper}_VERSION_MINOR \"${minor_num}\"" CMakeLists.txt; then
308exitError "${app_name_upper}_VERSION_MINOR not updated to '${minor_num}' in CMakeLists.txt!"
309fi
310
311if ! grep -q "${app_name_upper}_VERSION_PATCH \"${patch_num}\"" CMakeLists.txt; then
312exitError "${app_name_upper}_VERSION_PATCH not updated to '${patch_num}' in CMakeLists.txt!"
313fi
314}
315
316checkChangeLog() {
317if [ ! -f CHANGELOG.md ]; then
318exitError "No CHANGELOG file found!"
319fi
320
321if ! grep -qEzo "## ${RELEASE_NAME} \([0-9]{4}-[0-9]{2}-[0-9]{2}\)" CHANGELOG.md; then
322exitError "'CHANGELOG.md' has not been updated to the '${RELEASE_NAME}' release!"
323fi
324}
325
326checkAppStreamInfo() {
327if [ ! -f share/linux/org.keepassxc.KeePassXC.appdata.xml ]; then
328exitError "No AppStream info file found!"
329fi
330
331if ! grep -qEzo "<release version=\"${RELEASE_NAME}\" date=\"[0-9]{4}-[0-9]{2}-[0-9]{2}\">" share/linux/org.keepassxc.KeePassXC.appdata.xml; then
332exitError "'share/linux/org.keepassxc.KeePassXC.appdata.xml' has not been updated to the '${RELEASE_NAME}' release!"
333fi
334}
335
336checkTransifexCommandExists() {
337if ! cmdExists tx; then
338exitError "Transifex tool 'tx' not installed! Please install it using 'pip install transifex-client'."
339fi
340}
341
342checkSigntoolCommandExists() {
343if ! cmdExists signtool; then
344exitError "signtool command not found on the PATH! Add the Windows SDK binary folder to your PATH."
345fi
346}
347
348checkXcodeSetup() {
349if ! cmdExists xcrun; then
350exitError "xcrun command not found on the PATH! Please check that you have correctly installed Xcode."
351fi
352if ! xcrun -f codesign > /dev/null 2>&1; then
353exitError "codesign command not found. You may need to run 'sudo xcode-select -r' to set up Xcode."
354fi
355if ! xcrun -f altool > /dev/null 2>&1; then
356exitError "altool command not found. You may need to run 'sudo xcode-select -r' to set up Xcode."
357fi
358if ! xcrun -f stapler > /dev/null 2>&1; then
359exitError "stapler command not found. You may need to run 'sudo xcode-select -r' to set up Xcode."
360fi
361}
362
363checkQt5LUpdateExists() {
364if cmdExists lupdate && ! $(lupdate -version | grep -q "lupdate version 5\."); then
365if ! cmdExists lupdate-qt5; then
366exitError "Qt Linguist tool (lupdate-qt5) is not installed! Please install using 'apt install qttools5-dev-tools'"
367fi
368fi
369}
370
371performChecks() {
372logInfo "Performing basic checks..."
373
374checkSourceDirExists
375
376logInfo "Changing to source directory..."
377cd "${SRC_DIR}"
378
379logInfo "Validating toolset and repository..."
380
381checkTransifexCommandExists
382checkQt5LUpdateExists
383checkGitRepository
384checkReleaseDoesNotExist
385checkWorkingTreeClean
386checkSourceBranchExists
387
388logInfo "Checking out '${SOURCE_BRANCH}'..."
389git checkout "$SOURCE_BRANCH" > /dev/null 2>&1
390
391logInfo "Attempting to find '${RELEASE_NAME}' in various files..."
392
393checkVersionInCMake
394checkChangeLog
395checkAppStreamInfo
396
397logInfo "\e[1m\e[32mAll checks passed!\e[0m"
398}
399
400# re-implement realpath for OS X (thanks mschrag)
401# https://superuser.com/questions/205127/
402if ! cmdExists realpath; then
403realpath() {
404pushd . > /dev/null
405if [ -d "$1" ]; then
406cd "$1"
407dirs -l +0
408else
409cd "$(dirname "$1")"
410cur_dir=$(dirs -l +0)
411
412if [ "$cur_dir" == "/" ]; then
413echo "$cur_dir$(basename "$1")"
414else
415echo "$cur_dir/$(basename "$1")"
416fi
417fi
418popd > /dev/null
419}
420fi
421
422
423trap 'exitError "Exited upon user request."' SIGINT SIGTERM
424trap 'exitError "Error occurred!"' ERR
425
426
427# -----------------------------------------------------------------------
428# check command
429# -----------------------------------------------------------------------
430check() {
431while [ $# -ge 1 ]; do
432local arg="$1"
433case "$arg" in
434-v|--version)
435RELEASE_NAME="$2"
436shift ;;
437esac
438shift
439done
440
441init
442
443performChecks
444
445cleanup
446
447logInfo "Congrats! You can successfully merge, build, and sign KeepassXC."
448}
449
450# -----------------------------------------------------------------------
451# merge command
452# -----------------------------------------------------------------------
453merge() {
454while [ $# -ge 1 ]; do
455local arg="$1"
456case "$arg" in
457-v|--version)
458RELEASE_NAME="$2"
459shift ;;
460
461-a|--app-name)
462APP_NAME="$2"
463shift ;;
464
465-s|--source-dir)
466SRC_DIR="$2"
467shift ;;
468
469-k|--key|-g|--gpg-key)
470GPG_GIT_KEY="$2"
471shift ;;
472
473--timestamp)
474TIMESTAMP_SERVER="$2"
475shift ;;
476
477-r|--release-branch)
478SOURCE_BRANCH="$2"
479shift ;;
480
481-t|--tag-name)
482TAG_NAME="$2"
483shift ;;
484
485-h|--help)
486printUsage "merge"
487exit ;;
488
489*)
490logError "Unknown option '$arg'\n"
491printUsage "merge"
492exit 1 ;;
493esac
494shift
495done
496
497init
498
499performChecks
500
501# Update translations
502i18n lupdate
503i18n tx-pull
504
505if [ 0 -ne $? ]; then
506exitError "Updating translations failed!"
507fi
508if ! git diff-index --quiet HEAD --; then
509git add -A ./share/translations/
510logInfo "Committing changes..."
511if [ -z "$GPG_GIT_KEY" ]; then
512git commit -m "Update translations"
513else
514git commit -m "Update translations" -S"$GPG_GIT_KEY"
515fi
516fi
517
518local flags="-Pzo"
519if [ -n "$OS_MACOS" ]; then
520flags="-Ezo"
521fi
522CHANGELOG=$(grep ${flags} "## ${RELEASE_NAME} \([0-9]{4}-[0-9]{2}-[0-9]{2}\)\n\n(.|\n)+?\n\n## " CHANGELOG.md \
523| tail -n+3 | sed '$d' | sed 's/^### //')
524COMMIT_MSG="Release ${RELEASE_NAME}"
525
526logInfo "Creating tag '${TAG_NAME}'..."
527if [ -z "$GPG_GIT_KEY" ]; then
528git tag -a "$TAG_NAME" -m "$COMMIT_MSG" -m "${CHANGELOG}" -s
529else
530git tag -a "$TAG_NAME" -m "$COMMIT_MSG" -m "${CHANGELOG}" -s -u "$GPG_GIT_KEY"
531fi
532
533logInfo "Advancing 'latest' tag..."
534if [ -z "$GPG_GIT_KEY" ]; then
535git tag -sf -a "latest" -m "Latest stable release"
536else
537git tag -sf -u "$GPG_GIT_KEY" -a "latest" -m "Latest stable release"
538fi
539
540cleanup
541
542logInfo "All done!"
543logInfo "Don't forget to push the tags using \e[1mgit push --tags\e[0m."
544}
545
546# -----------------------------------------------------------------------
547# appimage command
548# -----------------------------------------------------------------------
549appimage() {
550local appdir
551local build_appsign=false
552local build_key
553local verbosity="1"
554
555while [ $# -ge 1 ]; do
556local arg="$1"
557case "$arg" in
558-v|--version)
559RELEASE_NAME="$2"
560shift ;;
561
562-a|--appdir)
563appdir="$2"
564shift ;;
565
566-o|--output-dir)
567OUTPUT_DIR="$2"
568shift ;;
569
570-d|--docker-image)
571DOCKER_IMAGE="$2"
572shift ;;
573
574--container-name)
575DOCKER_CONTAINER_NAME="$2"
576shift ;;
577
578--appsign)
579build_appsign=true ;;
580
581--verbosity)
582verbosity=$2
583shift ;;
584
585-k|--key)
586build_key="$2"
587shift ;;
588
589-h|--help)
590printUsage "appimage"
591exit ;;
592
593*)
594logError "Unknown option '$arg'\n"
595printUsage "appimage"
596exit 1 ;;
597esac
598shift
599done
600
601if [ -z "${appdir}" ]; then
602logError "Missing arguments, --appdir is required!\n"
603printUsage "appimage"
604exit 1
605fi
606
607if [ ! -d "${appdir}" ]; then
608exitError "AppDir does not exist, please create one with 'make install'!"
609elif [ -e "${appdir}/AppRun" ]; then
610exitError "AppDir has already been run through linuxdeploy, please create a fresh AppDir with 'make install'."
611fi
612
613appdir="$(realpath "$appdir")"
614
615local out="${OUTPUT_DIR}"
616if [ -z "$out" ]; then
617out="."
618fi
619mkdir -p "$out"
620local out_real="$(realpath "$out")"
621cd "$out"
622
623local linuxdeploy="linuxdeploy"
624local linuxdeploy_cleanup
625local linuxdeploy_plugin_qt="linuxdeploy-plugin-qt"
626local linuxdeploy_plugin_qt_cleanup
627local appimagetool="appimagetool"
628local appimagetool_cleanup
629
630logInfo "Testing for AppImage tools..."
631local docker_test_cmd
632if [ "" != "$DOCKER_IMAGE" ]; then
633docker_test_cmd="docker run -it --user $(id -u):$(id -g) --rm ${DOCKER_IMAGE}"
634fi
635
636# Test if linuxdeploy and linuxdeploy-plugin-qt are installed
637# on the system or inside the Docker container
638if ! ${docker_test_cmd} which ${linuxdeploy} > /dev/null; then
639logInfo "Downloading linuxdeploy..."
640linuxdeploy="./linuxdeploy"
641linuxdeploy_cleanup="rm -f ${linuxdeploy}"
642if ! curl -Lf "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" > "$linuxdeploy"; then
643exitError "linuxdeploy download failed."
644fi
645chmod +x "$linuxdeploy"
646fi
647if ! ${docker_test_cmd} which ${linuxdeploy_plugin_qt} > /dev/null; then
648logInfo "Downloading linuxdeploy-plugin-qt..."
649linuxdeploy_plugin_qt="./linuxdeploy-plugin-qt"
650linuxdeploy_plugin_qt_cleanup="rm -f ${linuxdeploy_plugin_qt}"
651if ! curl -Lf "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" > "$linuxdeploy_plugin_qt"; then
652exitError "linuxdeploy-plugin-qt download failed."
653fi
654chmod +x "$linuxdeploy_plugin_qt"
655fi
656
657# appimagetool is always run outside a Docker container, so we can access our GPG keys
658if ! cmdExists ${appimagetool}; then
659logInfo "Downloading appimagetool..."
660appimagetool="./appimagetool"
661appimagetool_cleanup="rm -f ${appimagetool}"
662if ! curl -Lf "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" > "$appimagetool"; then
663exitError "appimagetool download failed."
664fi
665chmod +x "$appimagetool"
666fi
667
668# Create custom AppRun wrapper
669cat << 'EOF' > "${out_real}/KeePassXC-AppRun"
670#!/usr/bin/env bash
671
672export PATH="$(dirname $0)/usr/bin:${PATH}"
673export LD_LIBRARY_PATH="$(dirname $0)/usr/lib:${LD_LIBRARY_PATH}"
674
675if [ "$1" == "cli" ]; then
676shift
677exec keepassxc-cli "$@"
678elif [ "$1" == "proxy" ]; then
679shift
680exec keepassxc-proxy "$@"
681elif [ -v CHROME_WRAPPER ] || [ -v MOZ_LAUNCHED_CHILD ]; then
682exec keepassxc-proxy "$@"
683else
684exec keepassxc "$@"
685fi
686EOF
687chmod +x "${out_real}/KeePassXC-AppRun"
688
689# Find .desktop files, icons, and binaries to deploy
690local desktop_file="$(find "$appdir" -name "org.keepassxc.KeePassXC.desktop" | head -n1)"
691local icon="$(find "$appdir" -path '*/application/256x256/apps/keepassxc.png' | head -n1)"
692local executables="$(find "$appdir" -type f -executable -path '*/bin/keepassxc*' -print0 | xargs -0 -i printf " --executable={}")"
693
694logInfo "Collecting libs and patching binaries..."
695if [ -z "$DOCKER_IMAGE" ]; then
696"$linuxdeploy" --verbosity=${verbosity} --plugin=qt --appdir="$appdir" --desktop-file="$desktop_file" \
697--custom-apprun="${out_real}/KeePassXC-AppRun" --icon-file="$icon" ${executables}
698else
699docker run --name "$DOCKER_CONTAINER_NAME" --rm \
700--cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse -it \
701-v "${out_real}:${out_real}:rw" \
702-v "${appdir}:${appdir}:rw" \
703-w "$out_real" \
704--user $(id -u):$(id -g) \
705"$DOCKER_IMAGE" \
706bash -c "${linuxdeploy} --verbosity=${verbosity} --plugin=qt \
707--appdir='${appdir}' --custom-apprun='${out_real}/KeePassXC-AppRun' \
708--desktop-file='${desktop_file}' --icon-file='${icon}' ${executables}"
709fi
710
711if [ $? -ne 0 ]; then
712exitError "AppDir deployment failed."
713fi
714
715logInfo "Creating AppImage..."
716local appsign_flag=""
717local appsign_key_flag=""
718if ${build_appsign}; then
719appsign_flag="--sign"
720appsign_key_flag="--sign-key ${build_key}"
721fi
722local appimage_name="KeePassXC-x86_64.AppImage"
723if [ "" != "$RELEASE_NAME" ]; then
724appimage_name="KeePassXC-${RELEASE_NAME}-x86_64.AppImage"
725echo "X-AppImage-Version=${RELEASE_NAME}" >> "$desktop_file"
726fi
727
728# Run appimagetool to package (and possibly sign) AppImage
729# --no-appstream is required, since it may crash on newer systems
730# see: https://github.com/AppImage/AppImageKit/issues/856
731if ! "$appimagetool" --updateinformation "gh-releases-zsync|keepassxreboot|keepassxc|latest|KeePassXC-*-x86_64.AppImage.zsync" \
732${appsign_flag} ${appsign_key_flag} --no-appstream "$appdir" "${out_real}/${appimage_name}"; then
733exitError "AppImage creation failed."
734fi
735
736logInfo "Cleaning up temporary files..."
737${linuxdeploy_cleanup}
738${linuxdeploy_plugin_qt_cleanup}
739${appimagetool_cleanup}
740rm -f "${out_real}/KeePassXC-AppRun"
741}
742
743# -----------------------------------------------------------------------
744# build command
745# -----------------------------------------------------------------------
746build() {
747local build_source_tarball=true
748local build_snapshot=false
749local build_snapcraft=false
750local build_appimage=false
751local build_generators=""
752local build_appsign=false
753local build_key=""
754local build_vcpkg=""
755
756while [ $# -ge 1 ]; do
757local arg="$1"
758case "$arg" in
759-v|--version)
760RELEASE_NAME="$2"
761shift ;;
762
763-a|--app-name)
764APP_NAME="$2"
765shift ;;
766
767-s|--source-dir)
768SRC_DIR="$2"
769shift ;;
770
771-o|--output-dir)
772OUTPUT_DIR="$2"
773shift ;;
774
775-t|--tag-name)
776TAG_NAME="$2"
777shift ;;
778
779-d|--docker-image)
780DOCKER_IMAGE="$2"
781shift ;;
782
783--container-name)
784DOCKER_CONTAINER_NAME="$2"
785shift ;;
786
787--appsign)
788build_appsign=true ;;
789
790--timestamp)
791TIMESTAMP_SERVER="$2"
792shift ;;
793
794-k|--key)
795build_key="$2"
796shift ;;
797
798--snapcraft)
799build_snapcraft=true ;;
800
801--appimage)
802build_appimage=true ;;
803
804--cmake-generator)
805CMAKE_GENERATOR="$2"
806shift ;;
807
808-c|--cmake-options)
809CMAKE_OPTIONS="$2"
810shift ;;
811
812--compiler)
813COMPILER="$2"
814shift ;;
815
816--vcpkg)
817build_vcpkg="$2"
818shift ;;
819
820-m|--make-options)
821MAKE_OPTIONS="$2"
822shift ;;
823
824-g|--generators)
825build_generators="$2"
826shift ;;
827
828-i|--install-prefix)
829INSTALL_PREFIX="$2"
830shift ;;
831
832-p|--plugins)
833BUILD_PLUGINS="$2"
834shift ;;
835
836-n|--no-source-tarball)
837build_source_tarball=false ;;
838
839--snapshot)
840build_snapshot=true ;;
841
842-h|--help)
843printUsage "build"
844exit ;;
845
846*)
847logError "Unknown option '$arg'\n"
848printUsage "build"
849exit 1 ;;
850esac
851shift
852done
853
854init
855
856# Resolve appsign key to absolute path if under Windows
857if [[ "${build_key}" && -n "$OS_WINDOWS" ]]; then
858build_key="$(realpath "${build_key}")"
859fi
860
861if [[ -f ${build_vcpkg} ]]; then
862CMAKE_OPTIONS="${CMAKE_OPTIONS} -DCMAKE_TOOLCHAIN_FILE=${build_vcpkg} -DX_VCPKG_APPLOCAL_DEPS_INSTALL=ON"
863fi
864
865if ${build_snapshot}; then
866TAG_NAME="HEAD"
867local branch=`git rev-parse --abbrev-ref HEAD`
868logInfo "Using current branch ${branch} to build..."
869RELEASE_NAME="${RELEASE_NAME}-snapshot"
870CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_BUILD_TYPE=Snapshot -DOVERRIDE_VERSION=${RELEASE_NAME}"
871else
872checkWorkingTreeClean
873if echo "$TAG_NAME" | grep -qE '\-(alpha|beta)[0-9]+$'; then
874CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_BUILD_TYPE=PreRelease"
875logInfo "Checking out pre-release tag '${TAG_NAME}'..."
876else
877CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_BUILD_TYPE=Release"
878logInfo "Checking out release tag '${TAG_NAME}'..."
879fi
880
881if ! git checkout "$TAG_NAME" > /dev/null 2>&1; then
882exitError "Failed to check out target branch."
883fi
884fi
885
886if ! ${build_snapshot} && [ -d "$OUTPUT_DIR" ]; then
887exitError "Output dir '${OUTPUT_DIR}' already exists."
888fi
889
890logInfo "Creating output directory..."
891if ! mkdir -p "$OUTPUT_DIR"; then
892exitError "Failed to create output directory!"
893fi
894OUTPUT_DIR="$(realpath "$OUTPUT_DIR")"
895
896if ${build_source_tarball}; then
897logInfo "Creating source tarball..."
898local app_name_lower="$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]')"
899local prefix="${app_name_lower}-${RELEASE_NAME}"
900local tarball_name="${prefix}-src.tar"
901
902git archive --format=tar "$TAG_NAME" --prefix="${prefix}/" --output="${OUTPUT_DIR}/${tarball_name}"
903
904# add .version and .gitrev files to tarball
905mkdir "${prefix}"
906echo -n ${RELEASE_NAME} > "${prefix}/.version"
907echo -n `git rev-parse --short=7 HEAD` > "${prefix}/.gitrev"
908tar --append --file="${OUTPUT_DIR}/${tarball_name}" "${prefix}/.version" "${prefix}/.gitrev"
909rm "${prefix}/.version" "${prefix}/.gitrev"
910rmdir "${prefix}" 2> /dev/null
911
912local xz="xz"
913if ! cmdExists xz; then
914logWarn "xz not installed. Falling back to bz2..."
915xz="bzip2"
916fi
917$xz -6 -f "${OUTPUT_DIR}/${tarball_name}"
918fi
919
920logInfo "Creating build directory..."
921mkdir -p "${OUTPUT_DIR}/build-release"
922cd "${OUTPUT_DIR}/build-release"
923
924logInfo "Configuring sources..."
925for p in ${BUILD_PLUGINS}; do
926CMAKE_OPTIONS="${CMAKE_OPTIONS} -DWITH_XC_$(echo $p | tr '[:lower:]' '[:upper:]')=On"
927done
928if [ -n "$OS_LINUX" ] && ${build_appimage}; then
929CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_DIST_TYPE=AppImage"
930# linuxdeploy requires /usr as install prefix
931INSTALL_PREFIX="/usr"
932fi
933if [ -n "$OS_MACOS" ]; then
934type brew &> /dev/null 2>&1
935if [ $? -eq 0 ]; then
936INSTALL_PREFIX=$(brew --prefix)
937fi
938fi
939
940# Do not build tests cases
941CMAKE_OPTIONS="${CMAKE_OPTIONS} -DWITH_TESTS=OFF"
942
943if [ "$COMPILER" == "g++" ]; then
944export CC=gcc
945elif [ "$COMPILER" == "clang++" ]; then
946export CC=clang
947else
948export CC="$COMPILER"
949fi
950export CXX="$COMPILER"
951
952if [ -z "$DOCKER_IMAGE" ]; then
953if [ -n "$OS_MACOS" ]; then
954# Building on macOS
955export MACOSX_DEPLOYMENT_TARGET
956
957logInfo "Configuring build..."
958cmake -G "${CMAKE_GENERATOR}" -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES="$(uname -m)" \
959-DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" ${CMAKE_OPTIONS} "$SRC_DIR"
960
961logInfo "Compiling and packaging sources..."
962cmake --build . -- ${MAKE_OPTIONS}
963cpack -G "DragNDrop"
964
965# Appsign the executables if desired
966if ${build_appsign}; then
967logInfo "Signing executable files"
968appsign "-f" "./${APP_NAME}-${RELEASE_NAME}.dmg" "-k" "${build_key}"
969fi
970
971mv "./${APP_NAME}-${RELEASE_NAME}.dmg" "../${APP_NAME}-${RELEASE_NAME}-$(uname -m).dmg"
972elif [ -n "$OS_WINDOWS" ]; then
973# Building on Windows with Msys2
974logInfo "Configuring build..."
975cmake -DCMAKE_BUILD_TYPE=Release -G "${CMAKE_GENERATOR}" -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" \
976${CMAKE_OPTIONS} "$SRC_DIR"
977
978logInfo "Compiling and packaging sources..."
979cmake --build . --config "Release" -- ${MAKE_OPTIONS}
980
981# Appsign the executables if desired
982if ${build_appsign} && [ -f "${build_key}" ]; then
983logInfo "Signing executable files"
984appsign "-f" $(find src | grep -Ei 'keepassxc.*(\.exe|\.dll)$') "-k" "${build_key}"
985fi
986
987# Call cpack directly instead of calling make package.
988# This is important because we want to build the MSI when making a
989# release.
990cpack -G "${CPACK_GENERATORS};${build_generators}"
991
992mv "${APP_NAME}-"*.* ../
993else
994mkdir -p "${OUTPUT_DIR}/KeePassXC.AppDir"
995
996# Building on Linux without Docker container
997logInfo "Configuring build..."
998cmake -DCMAKE_BUILD_TYPE=Release ${CMAKE_OPTIONS} \
999-DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" "$SRC_DIR"
1000
1001logInfo "Compiling sources..."
1002make ${MAKE_OPTIONS}
1003
1004logInfo "Installing to bin dir..."
1005make DESTDIR="${OUTPUT_DIR}/KeePassXC.AppDir" install/strip
1006fi
1007else
1008if ${build_snapcraft}; then
1009logInfo "Building snapcraft docker image..."
1010
1011sudo docker image build -t "$DOCKER_IMAGE" "$(realpath "$SRC_DIR")/ci/snapcraft"
1012
1013logInfo "Launching Docker contain to compile snapcraft..."
1014
1015sudo docker run --name "$DOCKER_CONTAINER_NAME" --rm -it --user $(id -u):$(id -g) \
1016-v "$(realpath "$SRC_DIR"):/keepassxc" -w "/keepassxc" \
1017"$DOCKER_IMAGE" snapcraft
1018else
1019mkdir -p "${OUTPUT_DIR}/KeePassXC.AppDir"
1020
1021logInfo "Launching Docker container to compile sources..."
1022
1023docker run --name "$DOCKER_CONTAINER_NAME" --rm \
1024--cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse \
1025--user $(id -u):$(id -g) \
1026-e "CC=${CC}" -e "CXX=${CXX}" -it \
1027-v "$(realpath "$SRC_DIR"):/keepassxc/src:ro" \
1028-v "$(realpath "$OUTPUT_DIR"):/keepassxc/out:rw" \
1029"$DOCKER_IMAGE" \
1030bash -c "cd /keepassxc/out/build-release && \
1031cmake -DCMAKE_BUILD_TYPE=Release ${CMAKE_OPTIONS} \
1032-DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} /keepassxc/src && \
1033make ${MAKE_OPTIONS} && make DESTDIR=/keepassxc/out/KeePassXC.AppDir install/strip"
1034fi
1035
1036if [ 0 -ne $? ]; then
1037exitError "Docker build failed!"
1038fi
1039
1040logInfo "Build finished, Docker container terminated."
1041fi
1042
1043if [ -n "$OS_LINUX" ] && ${build_appimage}; then
1044local appsign_flag=""
1045local appsign_key_flag=""
1046local docker_image_flag=""
1047local docker_container_name_flag=""
1048if ${build_appsign}; then
1049appsign_flag="--appsign"
1050appsign_key_flag="-k ${build_key}"
1051fi
1052if [ "" != "${DOCKER_IMAGE}" ]; then
1053docker_image_flag="-d ${DOCKER_IMAGE}"
1054docker_container_name_flag="--container-name ${DOCKER_CONTAINER_NAME}"
1055fi
1056appimage -a "${OUTPUT_DIR}/KeePassXC.AppDir" -o "${OUTPUT_DIR}" \
1057${appsign_flag} ${appsign_key_flag} ${docker_image_flag} ${docker_container_name_flag}
1058fi
1059
1060cleanup
1061
1062logInfo "All done!"
1063}
1064
1065# -----------------------------------------------------------------------
1066# gpgsign command
1067# -----------------------------------------------------------------------
1068gpgsign() {
1069local sign_files=()
1070
1071while [ $# -ge 1 ]; do
1072local arg="$1"
1073case "$arg" in
1074-f|--files)
1075while [ "${2:0:1}" != "-" ] && [ $# -ge 2 ]; do
1076sign_files+=("$2")
1077shift
1078done ;;
1079
1080-k|--key|-g|--gpg-key)
1081GPG_KEY="$2"
1082shift ;;
1083
1084-h|--help)
1085printUsage "gpgsign"
1086exit ;;
1087
1088*)
1089logError "Unknown option '$arg'\n"
1090printUsage "gpgsign"
1091exit 1 ;;
1092esac
1093shift
1094done
1095
1096if [ -z "${sign_files}" ]; then
1097logError "Missing arguments, --files is required!\n"
1098printUsage "gpgsign"
1099exit 1
1100fi
1101
1102for f in "${sign_files[@]}"; do
1103if [ ! -f "$f" ]; then
1104exitError "File '${f}' does not exist or is not a file!"
1105fi
1106
1107logInfo "Signing file '${f}' using release key..."
1108gpg --output "${f}.sig" --armor --local-user "$GPG_KEY" --detach-sig "$f"
1109
1110if [ 0 -ne $? ]; then
1111exitError "Signing failed!"
1112fi
1113
1114logInfo "Creating digest for file '${f}'..."
1115local rp="$(realpath "$f")"
1116local bname="$(basename "$f")"
1117(cd "$(dirname "$rp")"; sha256sum "$bname" > "${bname}.DIGEST")
1118done
1119
1120logInfo "All done!"
1121}
1122
1123# -----------------------------------------------------------------------
1124# appsign command
1125# -----------------------------------------------------------------------
1126appsign() {
1127local sign_files=()
1128local key
1129
1130while [ $# -ge 1 ]; do
1131local arg="$1"
1132case "$arg" in
1133-f|--files)
1134while [ "${2:0:1}" != "-" ] && [ $# -ge 2 ]; do
1135sign_files+=("$2")
1136shift
1137done ;;
1138
1139-k|--key|-i|--identity)
1140key="$2"
1141shift ;;
1142
1143-h|--help)
1144printUsage "appsign"
1145exit ;;
1146
1147*)
1148logError "Unknown option '$arg'\n"
1149printUsage "appsign"
1150exit 1 ;;
1151esac
1152shift
1153done
1154
1155if [ -z "${key}" ]; then
1156logError "Missing arguments, --key is required!\n"
1157printUsage "appsign"
1158exit 1
1159fi
1160
1161if [ -z "${sign_files}" ]; then
1162logError "Missing arguments, --files is required!\n"
1163printUsage "appsign"
1164exit 1
1165fi
1166
1167for f in "${sign_files[@]}"; do
1168if [ ! -e "${f}" ]; then
1169exitError "File '${f}' does not exist!"
1170fi
1171done
1172
1173if [ -n "$OS_MACOS" ]; then
1174checkXcodeSetup
1175
1176local orig_dir="$(pwd)"
1177local real_src_dir="$(realpath "${SRC_DIR}")"
1178for f in "${sign_files[@]}"; do
1179if [[ ${f: -4} == '.dmg' ]]; then
1180logInfo "Unpacking disk image '${f}'..."
1181local tmp_dir="/tmp/KeePassXC_${RANDOM}"
1182mkdir -p ${tmp_dir}/mnt
1183if ! hdiutil attach -quiet -noautoopen -mountpoint ${tmp_dir}/mnt "${f}"; then
1184exitError "DMG mount failed!"
1185fi
1186cd ${tmp_dir}
1187cp -a ./mnt ./app
1188hdiutil detach -quiet ${tmp_dir}/mnt
1189local app_dir_tmp="./app/KeePassXC.app"
1190
1191if [ ! -d "$app_dir_tmp" ]; then
1192cd "${orig_dir}"
1193exitError "Unpacking failed!"
1194fi
1195elif [[ ${f: -4} == '.app' ]]; then
1196local app_dir_tmp="$f"
1197else
1198logWarn "Skipping non-app file '${f}'..."
1199continue
1200fi
1201
1202logInfo "Signing libraries and frameworks..."
1203if ! find "$app_dir_tmp" \( -name '*.dylib' -o -name '*.so' -o -name '*.framework' \) -print0 | xargs -0 \
1204xcrun codesign --sign "${key}" --verbose --force --options runtime; then
1205cd "${orig_dir}"
1206exitError "Signing failed!"
1207fi
1208logInfo "Signing executables..."
1209if ! find "${app_dir_tmp}/Contents/MacOS" \( -type f -not -name KeePassXC \) -print0 | xargs -0 \
1210xcrun codesign --sign "${key}" --verbose --force --options runtime; then
1211cd "${orig_dir}"
1212exitError "Signing failed!"
1213fi
1214# Sign main executable with additional entitlements
1215if ! xcrun codesign --sign "${key}" --verbose --force --options runtime --entitlements \
1216"${real_src_dir}/share/macosx/keepassxc.entitlements" "${app_dir_tmp}/Contents/MacOS/KeePassXC"; then
1217cd "${orig_dir}"
1218exitError "Signing failed!"
1219fi
1220
1221if [[ ${f: -4} == '.dmg' ]]; then
1222logInfo "Repacking disk image..."
1223hdiutil create \
1224-volname "KeePassXC" \
1225-size $((1000 * ($(du -sk ./app | cut -f1) + 5000))) \
1226-srcfolder ./app \
1227-fs HFS+ \
1228-fsargs "-c c=64,a=16,e=16" \
1229-format UDBZ \
1230"${tmp_dir}/$(basename "${f}")"
1231
1232cd "${orig_dir}"
1233cp -f "${tmp_dir}/$(basename "${f}")" "${f}"
1234rm -Rf ${tmp_dir}
1235fi
1236
1237logInfo "File '${f}' successfully signed."
1238done
1239
1240elif [ -n "$OS_WINDOWS" ]; then
1241if [[ ! -f "${key}" ]]; then
1242exitError "Appsign key file was not found! (${key})"
1243fi
1244
1245logInfo "Using appsign key ${key}."
1246IFS=$'\n' read -s -r -p "Key password: " password
1247echo
1248
1249for f in "${sign_files[@]}"; do
1250ext=${f: -4}
1251if [[ $ext == ".msi" || $ext == ".exe" || $ext == ".dll" ]]; then
1252# Make sure we can find the signtool
1253checkSigntoolCommandExists
1254
1255# osslsigncode does not succeed at signing MSI files at this time...
1256logInfo "Signing file '${f}' using Microsoft signtool..."
1257signtool sign -f "${key}" -p "${password}" -d "KeePassXC" -td sha256 \
1258-fd sha256 -tr "${TIMESTAMP_SERVER}" "${f}"
1259
1260if [ 0 -ne $? ]; then
1261exitError "Signing failed!"
1262fi
1263else
1264logInfo "Skipping non-executable file '${f}'..."
1265fi
1266done
1267
1268else
1269exitError "Unsupported platform for code signing!\n"
1270fi
1271
1272logInfo "All done!"
1273}
1274
1275
1276# -----------------------------------------------------------------------
1277# notarize command
1278# -----------------------------------------------------------------------
1279notarize() {
1280local notarize_files=()
1281local ac_username
1282local ac_keychain="AC_PASSWORD"
1283
1284while [ $# -ge 1 ]; do
1285local arg="$1"
1286case "$arg" in
1287-f|--files)
1288while [ "${2:0:1}" != "-" ] && [ $# -ge 2 ]; do
1289notarize_files+=("$2")
1290shift
1291done ;;
1292
1293-u|--username)
1294ac_username="$2"
1295shift ;;
1296
1297-c|--keychain)
1298ac_keychain="$2"
1299shift ;;
1300
1301-h|--help)
1302printUsage "notarize"
1303exit ;;
1304
1305*)
1306logError "Unknown option '$arg'\n"
1307printUsage "notarize"
1308exit 1 ;;
1309esac
1310shift
1311done
1312
1313if [ -z "$OS_MACOS" ]; then
1314exitError "Notarization is only supported on macOS!"
1315fi
1316
1317if [ -z "${notarize_files}" ]; then
1318logError "Missing arguments, --files is required!\n"
1319printUsage "notarize"
1320exit 1
1321fi
1322
1323if [ -z "$ac_username" ]; then
1324logError "Missing arguments, --username is required!"
1325printUsage "notarize"
1326exit 1
1327fi
1328
1329for f in "${notarize_files[@]}"; do
1330if [[ ${f: -4} != '.dmg' ]]; then
1331logWarn "Skipping non-DMG file '${f}'..."
1332continue
1333fi
1334
1335logInfo "Submitting disk image '${f}' for notarization..."
1336local status
1337status="$(xcrun altool --notarize-app \
1338--primary-bundle-id "org.keepassxc.keepassxc" \
1339--username "${ac_username}" \
1340--password "@keychain:${ac_keychain}" \
1341--file "${f}")"
1342
1343if [ 0 -ne $? ]; then
1344logError "Submission failed!"
1345exitError "Error message:\n${status}"
1346fi
1347
1348local ticket="$(echo "${status}" | grep -oE '[a-f0-9-]+$')"
1349logInfo "Submission successful. Ticket ID: ${ticket}."
1350
1351logInfo "Waiting for notarization to finish (this may take a while)..."
1352while true; do
1353echo -n "."
1354
1355status="$(xcrun altool --notarization-info "${ticket}" \
1356--username "${ac_username}" \
1357--password "@keychain:${ac_keychain}" 2> /dev/null)"
1358
1359if echo "$status" | grep -q "Status Code: 0"; then
1360logInfo "\nNotarization successful."
1361break
1362elif echo "$status" | grep -q "Status Code"; then
1363logError "\nNotarization failed!"
1364exitError "Error message:\n${status}"
1365fi
1366
1367sleep 5
1368done
1369
1370logInfo "Stapling ticket to disk image..."
1371xcrun stapler staple "${f}"
1372
1373if [ 0 -ne $? ]; then
1374exitError "Stapling failed!"
1375fi
1376
1377logInfo "Disk image successfully notarized."
1378done
1379}
1380
1381
1382# -----------------------------------------------------------------------
1383# i18n command
1384# -----------------------------------------------------------------------
1385
1386i18n() {
1387local cmd="$1"
1388if [ -z "$cmd" ]; then
1389logError "No subcommand specified.\n"
1390printUsage i18n
1391exit 1
1392elif [ "$cmd" != "tx-push" ] && [ "$cmd" != "tx-pull" ] && [ "$cmd" != "lupdate" ]; then
1393logError "Unknown subcommand: '${cmd}'\n"
1394printUsage i18n
1395exit 1
1396fi
1397shift
1398
1399checkGitRepository
1400
1401if [ "$cmd" == "lupdate" ]; then
1402if [ ! -d share/translations ]; then
1403logError "Command must be called from repository root directory."
1404exit 1
1405fi
1406
1407checkQt5LUpdateExists
1408
1409logInfo "Updating source translation file..."
1410LUPDATE=lupdate-qt5
1411if ! command -v $LUPDATE > /dev/null; then
1412LUPDATE=lupdate
1413fi
1414$LUPDATE -no-ui-lines -disable-heuristic similartext -locations none -extensions c,cpp,h,js,mm,qrc,ui \
1415-no-obsolete src -ts share/translations/keepassxc_en.ts $@
1416
1417return 0
1418fi
1419
1420checkTransifexCommandExists
1421
1422local branch="$(git branch --show-current 2>&1)"
1423local real_branch="$branch"
1424if [[ "$branch" =~ ^release/ ]]; then
1425logInfo "Release branch, setting language resource to master branch."
1426branch="master"
1427elif [ "$branch" != "develop" ] && [ "$branch" != "master" ]; then
1428logError "Must be on master or develop branch!"
1429exit 1
1430fi
1431local resource="keepassxc.share-translations-keepassxc-en-ts--${branch}"
1432
1433if [ "$cmd" == "tx-push" ]; then
1434echo -e "This will push the \e[1m'en'\e[0m source file from the current branch to Transifex:\n" >&2
1435echo -e " \e[1m${real_branch}\e[0m -> \e[1m${resource}\e[0m\n" >&2
1436echo -n "Continue? [y/N] " >&2
1437read -r yesno
1438if [ "$yesno" != "y" ] && [ "$yesno" != "Y" ]; then
1439logError "Push aborted."
1440exit 1
1441fi
1442
1443logInfo "Pushing source translation file to Transifex..."
1444tx push -s --use-git-timestamps -r "$resource" $@
1445
1446elif [ "$cmd" == "tx-pull" ]; then
1447logInfo "Pulling updated translations from Transifex..."
1448tx pull -af --minimum-perc=60 -r "$resource" $@
1449fi
1450}
1451
1452
1453# -----------------------------------------------------------------------
1454# parse global command line
1455# -----------------------------------------------------------------------
1456MODE="$1"
1457shift || true
1458if [ -z "$MODE" ]; then
1459logError "Missing arguments!\n"
1460printUsage
1461exit 1
1462elif [ "help" == "$MODE" ]; then
1463printUsage "$1"
1464exit
1465elif [ "check" == "$MODE" ] || [ "merge" == "$MODE" ] || [ "build" == "$MODE" ] \
1466|| [ "gpgsign" == "$MODE" ] || [ "appsign" == "$MODE" ]|| [ "notarize" == "$MODE" ] \
1467|| [ "appimage" == "$MODE" ]|| [ "i18n" == "$MODE" ]; then
1468${MODE} "$@"
1469else
1470printUsage "$MODE"
1471fi
1472