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