diff --git a/.gitattributes b/.gitattributes index 9f713b466..9d1ecabf4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ src/version.h.cmake export-subst .gitattributes export-ignore .gitignore export-ignore +.github export-ignore .travis.yml export-ignore .tx export-ignore snapcraft.yaml export-ignore diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 01b8d6137..67b0e1746 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,31 +1,32 @@ -# Contributing to KeePassX Reboot +# Contributing to KeePassXC :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: -The following is a set of guidelines for contributing to KeePassX Reboot on GitHub. +The following is a set of guidelines for contributing to KeePassXC on GitHub. These are just guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. -#### Table Of Contents +#### Table of contents [What should I know before I get started?](#what-should-i-know-before-i-get-started) * [Open Source Contribution Policy](#open-source-contribution-policy) -[How Can I Contribute?](#how-can-i-contribute) - * [Feature Requests](#feature-requests) - * [Bug Reports](#bug-reports) - * [Your First Code Contribution](#your-first-code-contribution) - * [Pull Requests](#pull-requests) +[How can I contribute?](#how-can-i-contribute) + * [Feature requests](#feature-requests) + * [Bug reports](#bug-reports) + * [Discuss with the team](#discuss-with-the-team) + * [Your first code contribution](#your-first-code-contribution) + * [Pull requests](#pull-requests) * [Translations](#translations) [Styleguides](#styleguides) - * [Git Branch Strategy](#git_branch_strategy) - * [Git Commit Messages](#git-commit-messages) - * [Coding Styleguide](#coding-styleguide) + * [Git branch strategy](#git-branch-strategy) + * [Git commit messages](#git-commit-messages) + * [Coding styleguide](#coding-styleguide) ## What should I know before I get started? ### Open Source Contribution Policy -[Version 0.3, 2015–11–18](https://medium.com/@jmaynard/a-contribution-policy-for-open-source-that-works-bfc4600c9d83#.i9ntbhmad) +**Source**: [Version 0.3, 2015–11–18](https://medium.com/@jmaynard/a-contribution-policy-for-open-source-that-works-bfc4600c9d83#.i9ntbhmad) #### Policy @@ -49,35 +50,35 @@ If we reject your contribution, it means only that we do not consider it suitabl * 0.3, 2011–11–19: Added “irrevocably” to “we can use” and changed “it” to “your contribution” in the “if rejected” section. Thanks to Patrick Maupin. -## How Can I Contribute? -### Feature Requests +## How can I contribute? +### Feature requests -We're always looking for suggestions to improve our application. If you have a suggestion for improving an existing feature, or would like to suggest a completely new feature for KeePassX Reboot, please use the Issues section or our [Google Groups](https://groups.google.com/forum/#!forum/keepassx-reboot) forum. +We're always looking for suggestions to improve our application. If you have a suggestion to improve an existing feature, or would like to suggest a completely new feature for KeePassXC, please use the [issue tracker on GitHub][issues-section]. For more general discussion, try using our [Google Groups][google-groups] forum. -### Bug Reports +### Bug reports -Our software isn't always perfect, but we strive to always improve our work. You may file bug reports in the Issues section. +Our software isn't always perfect, but we strive to always improve our work. You may file bug reports in the issue tracker. -Before submitting a Bug Report, check if the problem has already been reported. Please refrain from opening a duplicate issue. If you want to highlight a deficiency on an existing issue, simply add a comment. +Before submitting a bug report, check if the problem has already been reported. Please refrain from opening a duplicate issue. If you want to add further information to an existing issue, simply add a comment on that issue. -### Discuss with the Team +### Discuss with the team -You can talk to the KeePassX Reboot Team about Bugs, new feature, Issue and PullRequests at our [Google Groups](https://groups.google.com/forum/#!forum/keepassx-reboot) forum +As with feature requests, you can talk to the KeePassXC team about bugs, new features, other issues and pull requests on the dedicated issue tracker, using the [Google Groups][google-groups] forum, or in the IRC channel on Freenode (`#keepassxc-dev` on `irc.freenode.net`, or use a [webchat link](https://webchat.freenode.net/?channels=%23keepassxc-dev)). -### Your First Code Contribution +### Your first code contribution -Unsure where to begin contributing to KeePassX Reboot? You can start by looking through these `beginner` and `help-wanted` issues: +Unsure where to begin contributing to KeePassXC? You can start by looking through these `beginner` and `help-wanted` issues: -* [Beginner issues][beginner] - issues which should only require a few lines of code, and a test or two. -* [Help wanted issues][help-wanted] - issues which should be a bit more involved than `beginner` issues. +* [Beginner issues][beginner] – issues which should only require a few lines of code, and a test or two. +* ['Help wanted' issues][help-wanted] – issues which should be a bit more involved than `beginner` issues. -Both issue lists are sorted by total number of comments. While not perfect, number of comments is a reasonable proxy for impact a given change will have. +Both issue lists are sorted by total number of comments. While not perfect, looking at the number of comments on an issue can give a general idea of how much an impact a given change will have. -### Pull Requests +### Pull requests Along with our desire to hear your feedback and suggestions, we're also interested in accepting direct assistance in the form of code. -All pull requests must comply with the above requirements and with the [Styleguides](#styleguides). +All pull requests must comply with the above requirements and with the [styleguides](#styleguides). ### Translations @@ -86,19 +87,20 @@ Please join an existing language team or request a new one if there is none. ## Styleguides -### Git Branch Strategy +### Git branch strategy The Branch Strategy is based on [git-flow-lite](http://nvie.com/posts/a-successful-git-branching-model/). -* **master** -> always points to the last release published -* **develop** -> points to the next planned release, tested and reviewed code -* **feature/**[name] -> points to brand new feature in codebase, candidate for merge into develop (subject to rebase) +* **master** – points to the latest public release +* **develop** – points to the development of the next release, contains tested and reviewed code +* **feature/**[name] – points to a branch with a new feature, one which is candidate for merge into develop (subject to rebase) +* **hotfix/**[id]-[description] – points to a branch with a fix for a particular issue ID -### Git Commit Messages +### Git commit messages * Use the present tense ("Add feature" not "Added feature") -* Use the imperative mood ("Move cursor to..." not "Moves cursor to...") +* Use the imperative mood ("Move cursor to…" not "Moves cursor to…") * Limit the first line to 72 characters or less * Reference issues and pull requests liberally * When only changing documentation, include `[ci skip]` in the commit description @@ -114,21 +116,21 @@ The Branch Strategy is based on [git-flow-lite](http://nvie.com/posts/a-successf * :lock: `:lock:` when dealing with security -### Coding Styleguide +### Coding styleguide This project follows the [Qt Coding Style](https://wiki.qt.io/Qt_Coding_Style). All submissions are expected to follow this style. -In particular Code must follow the following specific rules: +In particular, code must stick to the following rules: -#### Naming Convention +#### Naming convention `lowerCamelCase` -For names made of only one word, the fist letter is lowercase. -For names made of multiple concatenated words, the first letter is lowercase and each subsequent concatenated word is capitalized. +For names made of only one word, the first letter should be lowercase. +For names made of multiple concatenated words, the first letter of the whole is lowercase, and the first letter of each subsequent word is capitalized. #### Indention -For C++ files (.cpp .h): 4 spaces -For Qt-UI files (.ui): 2 spaces +For **C++ files** (*.cpp .h*): 4 spaces +For **Qt-UI files** (*.ui*): 2 spaces #### Pointers ```c @@ -165,9 +167,8 @@ Use prefix: `m_*` Example: `m_variable` -#### GUI Widget names -Widget names must be related to the desired program behaviour. -Preferably end the name with the Widget Classname +#### GUI widget names +Widget names must be related to the desired program behavior, and preferably end with the widget's classname. Example: `` @@ -175,3 +176,5 @@ Example: `` [beginner]:https://github.com/keepassxreboot/keepassx/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner+label%3A%22help+wanted%22+sort%3Acomments-desc [help-wanted]:https://github.com/keepassxreboot/keepassx/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22+sort%3Acomments-desc +[issues-section]:https://github.com/keepassxreboot/keepassxc/issues +[google-groups]:https://groups.google.com/forum/#!forum/keepassx-reboot diff --git a/AppImage-Recipe.sh b/AppImage-Recipe.sh index 9575f077b..dc30cb696 100755 --- a/AppImage-Recipe.sh +++ b/AppImage-Recipe.sh @@ -39,26 +39,54 @@ mkdir -p $APP.AppDir wget -q https://github.com/probonopd/AppImages/raw/master/functions.sh -O ./functions.sh . ./functions.sh +LIB_DIR=./usr/lib +if [ -d ./usr/lib/x86_64-linux-gnu ]; then + LIB_DIR=./usr/lib/x86_64-linux-gnu +fi + cd $APP.AppDir cp -a ../../bin-release/* . -mv ./usr/local/* ./usr -rmdir ./usr/local -patch_strings_in_file /usr/local ./ +cp -a ./usr/local/* ./usr +rm -R ./usr/local +rmdir ./opt 2> /dev/null +patch_strings_in_file /usr/local ././ patch_strings_in_file /usr ./ +# bundle Qt platform plugins and themes +QXCB_PLUGIN="$(find /usr/lib -name 'libqxcb.so' 2> /dev/null)" +if [ "$QXCB_PLUGIN" == "" ]; then + QXCB_PLUGIN="$(find /opt/qt*/plugins -name 'libqxcb.so' 2> /dev/null)" +fi +QT_PLUGIN_PATH="$(dirname $(dirname $QXCB_PLUGIN))" +mkdir -p ".${QT_PLUGIN_PATH}/platforms" +cp "$QXCB_PLUGIN" ".${QT_PLUGIN_PATH}/platforms/" + get_apprun copy_deps delete_blacklisted +# remove dbus and systemd libs as they are not blacklisted +find . -name libdbus-1.so.3 -exec rm {} \; +find . -name libsystemd.so.0 -exec rm {} \; + get_desktop get_icon +cat << EOF > ./usr/bin/keepassxc_env +#!/usr/bin/env bash +#export QT_QPA_PLATFORMTHEME=gtk2 +export LD_LIBRARY_PATH="../opt/qt58/lib:\${LD_LIBRARY_PATH}" +export QT_PLUGIN_PATH="..${QT_PLUGIN_PATH}" +exec keepassxc "\$@" +EOF +chmod +x ./usr/bin/keepassxc_env +sed -i 's/Exec=keepassxc/Exec=keepassxc_env/' keepassxc.desktop get_desktopintegration $LOWERAPP GLIBC_NEEDED=$(glibc_needed) cd .. -generate_appimage +generate_type2_appimage mv ../out/*.AppImage .. rmdir ../out > /dev/null 2>&1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 051aba199..2fd890e01 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,6 +172,8 @@ set(CMAKE_AUTOMOC ON) # Make sure we don't enable asserts there. set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS_NONE QT_NO_DEBUG) +find_package(LibGPGError REQUIRED) + find_package(Gcrypt 1.6.0 REQUIRED) if (WITH_XC_HTTP) diff --git a/Dockerfile b/Dockerfile index 3aee19e3c..9623b60dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,21 +14,41 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -FROM ubuntu:16.04 +FROM ubuntu:14.04 -RUN set -x && apt-get update RUN set -x \ + && apt-get update \ + && apt-get install --yes software-properties-common + +RUN set -x \ + && add-apt-repository --yes ppa:beineri/opt-qt58-trusty + +RUN set -x \ + && apt-get update \ && apt-get install --yes \ + g++ \ cmake \ libgcrypt20-dev \ - qtbase5-dev \ - qttools5-dev-tools \ + qt58base \ + qt58tools \ + qt58x11extras \ libmicrohttpd-dev \ - libqt5x11extras5-dev \ libxi-dev \ libxtst-dev \ - zlib1g-dev + zlib1g-dev \ + wget \ + file \ + fuse \ + python +RUN set -x \ + && apt-get install --yes mesa-common-dev + VOLUME /keepassxc/src VOLUME /keepassxc/out WORKDIR /keepassxc + +ENV CMAKE_PREFIX_PATH=/opt/qt58/lib/cmake +ENV LD_LIBRARY_PATH=/opt/qt58/lib +RUN set -x \ + && echo /opt/qt58/lib > /etc/ld.so.conf.d/qt58.conf diff --git a/cmake/FindLibGPGError.cmake b/cmake/FindLibGPGError.cmake new file mode 100644 index 000000000..fe9ef9123 --- /dev/null +++ b/cmake/FindLibGPGError.cmake @@ -0,0 +1,9 @@ + +find_path(GPGERROR_INCLUDE_DIR gpg-error.h) + +find_library(GPGERROR_LIBRARIES gpg-error) + +mark_as_advanced(GPGERROR_LIBRARIES GPGERROR_INCLUDE_DIR) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(LibGPGError DEFAULT_MSG GPGERROR_LIBRARIES GPGERROR_INCLUDE_DIR) diff --git a/make_release.sh b/make_release.sh deleted file mode 100755 index 19c661b38..000000000 --- a/make_release.sh +++ /dev/null @@ -1,350 +0,0 @@ -#!/usr/bin/env bash -# -# KeePassXC Release Preparation Helper -# Copyright (C) 2017 KeePassXC team -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 or (at your option) -# version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -echo -e "\e[1m\e[32mKeePassXC\e[0m Release Preparation Helper" -echo -e "Copyright (C) 2017 KeePassXC Team \n" - - -# default values -RELEASE_NAME="" -APP_NAME="KeePassXC" -APP_NAME_LOWER="keepassxc" -SRC_DIR="." -GPG_KEY="CFB4C2166397D0D2" -GPG_GIT_KEY="" -OUTPUT_DIR="release" -BRANCH="" -RELEASE_BRANCH="master" -TAG_NAME="" -BUILD_SOURCES=false -DOCKER_IMAGE="" -DOCKER_CONTAINER_NAME="${APP_NAME_LOWER}-build-container" -CMAKE_OPTIONS="" -COMPILER="g++" -MAKE_OPTIONS="-j8" -BUILD_PLUGINS="autotype" -INSTALL_PREFIX="/usr/local" - -ORIG_BRANCH="$(git rev-parse --abbrev-ref HEAD 2> /dev/null)" -ORIG_CWD="$(pwd)" - - -# helper functions -printUsage() { - echo -e "\e[1mUsage:\e[0m $(basename $0) [options]" - cat << EOF - -Options: - -v, --version Release version number or name (required) - -a, --app-name Application name (default: '${APP_NAME}') - -s, --source-dir Source directory (default: '${SRC_DIR}') - -k, --gpg-key GPG key used to sign the release tarball - (default: '${GPG_KEY}') - -g, --gpg-git-key GPG key used to sign the merge commit and release tag, - leave empty to let Git choose your default key - (default: '${GPG_GIT_KEY}') - -o, --output-dir Output directory where to build the release - (default: '${OUTPUT_DIR}') - --develop-branch Development branch to merge from (default: 'release/VERSION') - --release-branch Target release branch to merge to (default: '${RELEASE_BRANCH}') - -t, --tag-name Override release tag name (defaults to version number) - -b, --build Build sources after exporting release - -d, --docker-image Use the specified Docker image to compile the application. - The image must have all required build dependencies installed. - This option has no effect if --build is not set. - --container-name Docker container name (default: '${DOCKER_CONTAINER_NAME}') - The container must not exist already - -c, --cmake-options Additional CMake options for compiling the sources - --compiler Compiler to use (default: '${COMPILER}') - -m, --make-options Make options for compiling sources (default: '${MAKE_OPTIONS}') - -i, --install-prefix Install prefix (default: '${INSTALL_PREFIX}') - -p, --plugins Space-separated list of plugins to build - (default: ${BUILD_PLUGINS}) - -h, --help Show this help - -EOF -} - -logInfo() { - echo -e "\e[1m[ \e[34mINFO\e[39m ]\e[0m $1" -} - -logError() { - echo -e "\e[1m[ \e[31mERROR\e[39m ]\e[0m $1" >&2 -} - -exitError() { - logError "$1" - if [ "" != "$ORIG_BRANCH" ]; then - git checkout "$ORIG_BRANCH" > /dev/null 2>&1 - fi - cd "$ORIG_CWD" - exit 1 -} - - -# parse command line options -while [ $# -ge 1 ]; do - arg="$1" - - case "$arg" in - -a|--app-name) - APP_NAME="$2" - shift ;; - - -s|--source-dir) - SRC_DIR"$2" - shift ;; - - -v|--version) - RELEASE_NAME="$2" - shift ;; - - -k|--gpg-key) - GPG_KEY="$2" - shift ;; - - -g|--gpg-git-key) - GPG_GIT_KEY="$2" - shift ;; - - -o|--output-dir) - OUTPUT_DIR="$2" - shift ;; - - --develop-branch) - BRANCH="$2" - shift ;; - - --release-branch) - RELEASE_BRANCH="$2" - shift ;; - - -t|--tag-name) - TAG_NAME="$2" - shift ;; - - -b|--build) - BUILD_SOURCES=true ;; - - -d|--docker-image) - DOCKER_IMAGE="$2" - shift ;; - - --container-name) - DOCKER_CONTAINER_NAME="$2" - shift ;; - - -c|--cmake-options) - CMAKE_OPTIONS="$2" - shift ;; - - -m|--make-options) - MAKE_OPTIONS="$2" - shift ;; - - --compiler) - COMPILER="$2" - shift ;; - - -p|--plugins) - BUILD_PLUGINS="$2" - shift ;; - - -h|--help) - printUsage - exit ;; - - *) - logError "Unknown option '$arg'\n" - printUsage - exit 1 ;; - esac - shift -done - - -if [ "" == "$RELEASE_NAME" ]; then - logError "Missing arguments, --version is required!\n" - printUsage - exit 1 -fi - -if [ "" == "$TAG_NAME" ]; then - TAG_NAME="$RELEASE_NAME" -fi -if [ "" == "$BRANCH" ]; then - BRANCH="release/${RELEASE_NAME}" -fi -APP_NAME_LOWER="$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]')" -APP_NAME_UPPER="$(echo "$APP_NAME" | tr '[:lower:]' '[:upper:]')" - -SRC_DIR="$(realpath "$SRC_DIR")" -OUTPUT_DIR="$(realpath "$OUTPUT_DIR")" -if [ ! -d "$SRC_DIR" ]; then - exitError "Source directory '${SRC_DIR}' does not exist!" -fi - -logInfo "Changing to source directory..." -cd "${SRC_DIR}" - -logInfo "Performing basic checks..." - -if [ -e "$OUTPUT_DIR" ]; then - exitError "Output directory '$OUTPUT_DIR' already exists. Please choose a different location!" -fi - -if [ ! -d .git ] || [ ! -f CHANGELOG ]; then - exitError "Source directory is not a valid Git repository!" -fi - -git tag | grep -q "$RELEASE_NAME" -if [ $? -eq 0 ]; then - exitError "Release '$RELEASE_NAME' already exists!" -fi - -git diff-index --quiet HEAD -- -if [ $? -ne 0 ]; then - exitError "Current working tree is not clean! Please commit or unstage any changes." -fi - -git checkout "$BRANCH" > /dev/null 2>&1 -if [ $? -ne 0 ]; then - exitError "Source branch '$BRANCH' does not exist!" -fi - -grep -q "${APP_NAME_UPPER}_VERSION \"${RELEASE_NAME}\"" CMakeLists.txt -if [ $? -ne 0 ]; then - exitError "${APP_NAME_UPPER}_VERSION version not updated to '${RELEASE_NAME}' in CMakeLists.txt!" -fi - -grep -q "${APP_NAME_UPPER}_VERSION_NUM \"${RELEASE_NAME}\"" CMakeLists.txt -if [ $? -ne 0 ]; then - exitError "${APP_NAME_UPPER}_VERSION_NUM version not updated to '${RELEASE_NAME}' in CMakeLists.txt!" -fi - -if [ ! -f CHANGELOG ]; then - exitError "No CHANGELOG file found!" -fi - -grep -qPzo "${RELEASE_NAME} \(\d{4}-\d{2}-\d{2}\)\n=+\n" CHANGELOG -if [ $? -ne 0 ]; then - exitError "CHANGELOG does not contain any information about the '${RELEASE_NAME}' release!" -fi - -git checkout "$RELEASE_BRANCH" > /dev/null 2>&1 -if [ $? -ne 0 ]; then - exitError "Release branch '$RELEASE_BRANCH' does not exist!" -fi - -logInfo "All checks pass, getting our hands dirty now!" - -logInfo "Merging '${BRANCH}' into '${RELEASE_BRANCH}'..." - -CHANGELOG=$(grep -Pzo "(?<=${RELEASE_NAME} \(\d{4}-\d{2}-\d{2}\)\n)=+\n\n(?:.|\n)+?\n(?=\n)" \ - CHANGELOG | grep -Pzo '(?<=\n\n)(.|\n)+' | tr -d \\0) -COMMIT_MSG="Release ${RELEASE_NAME}" - -git merge "$BRANCH" --no-ff -m "$COMMIT_MSG" -m "${CHANGELOG}" "$BRANCH" -S"$GPG_GIT_KEY" - -logInfo "Creating tag '${RELEASE_NAME}'..." -if [ "" == "$GPG_GIT_KEY" ]; then - git tag -a "$RELEASE_NAME" -m "$COMMIT_MSG" -m "${CHANGELOG}" -s -else - git tag -a "$RELEASE_NAME" -m "$COMMIT_MSG" -m "${CHANGELOG}" -s -u "$GPG_GIT_KEY" -fi - -logInfo "Merge done, creating target directory..." -mkdir -p "$OUTPUT_DIR" - -if [ $? -ne 0 ]; then - exitError "Failed to create output directory!" -fi - -logInfo "Creating source tarball..." -TARBALL_NAME="${APP_NAME_LOWER}-${RELEASE_NAME}-src.tar.bz2" -git archive --format=tar "$RELEASE_BRANCH" --prefix="${APP_NAME_LOWER}-${RELEASE_NAME}/" \ - | bzip2 -9 > "${OUTPUT_DIR}/${TARBALL_NAME}" - - -if $BUILD_SOURCES; then - logInfo "Creating build directory..." - mkdir -p "${OUTPUT_DIR}/build-release" - mkdir -p "${OUTPUT_DIR}/bin-release" - cd "${OUTPUT_DIR}/build-release" - - logInfo "Configuring sources..." - for p in $BUILD_PLUGINS; do - CMAKE_OPTIONS="${CMAKE_OPTIONS} -DWITH_XC_$(echo $p | tr '[:lower:]' '[:upper:]')=On" - done - - if [ "$COMPILER" == "g++" ]; then - export CC=gcc - elif [ "$COMPILER" == "clang++" ]; then - export CC=clang - fi - export CXX="$COMPILER" - - if [ "" == "$DOCKER_IMAGE" ]; then - cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off $CMAKE_OPTIONS \ - -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" "$SRC_DIR" - - logInfo "Compiling sources..." - make $MAKE_OPTIONS - - logInfo "Installing to bin dir..." - make DESTDIR="${OUTPUT_DIR}/bin-release" install/strip - else - logInfo "Launching Docker container to compile sources..." - - docker run --name "$DOCKER_CONTAINER_NAME" --rm \ - -e "CC=${CC}" -e "CXX=${CXX}" \ - -v "$(realpath "$SRC_DIR"):/keepassxc/src:ro" \ - -v "$(realpath "$OUTPUT_DIR"):/keepassxc/out:rw" \ - "$DOCKER_IMAGE" \ - bash -c "cd /keepassxc/out/build-release && \ - cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off $CMAKE_OPTIONS \ - -DCMAKE_INSTALL_PREFIX=\"${INSTALL_PREFIX}\" /keepassxc/src && \ - make $MAKE_OPTIONS && make DESTDIR=/keepassxc/out/bin-release install/strip" - - logInfo "Build finished, Docker container terminated." - fi - - logInfo "Creating AppImage..." - ${SRC_DIR}/AppImage-Recipe.sh "$APP_NAME" "$RELEASE_NAME" - - cd .. - logInfo "Signing source tarball..." - gpg --output "${TARBALL_NAME}.sig" --armor --local-user "$GPG_KEY" --detach-sig "$TARBALL_NAME" - - logInfo "Signing AppImage..." - APPIMAGE_NAME="${APP_NAME}-${RELEASE_NAME}-x86_64.AppImage" - gpg --output "${APPIMAGE_NAME}.sig" --armor --local-user "$GPG_KEY" --detach-sig "$APPIMAGE_NAME" - - logInfo "Creating digests..." - sha256sum "$TARBALL_NAME" > "${TARBALL_NAME}.DIGEST" - sha256sum "$APPIMAGE_NAME" > "${APPIMAGE_NAME}.DIGEST" -fi - -logInfo "Leaving source directory..." -cd "$ORIG_CWD" -git checkout "$ORIG_BRANCH" > /dev/null 2>&1 - -logInfo "All done!" -logInfo "Please merge the release branch back into the develop branch now and then push your changes." -logInfo "Don't forget to also push the tags using \e[1mgit push --tags\e[0m." diff --git a/release-tool b/release-tool new file mode 100755 index 000000000..7bc54cda0 --- /dev/null +++ b/release-tool @@ -0,0 +1,682 @@ +#!/usr/bin/env bash +# +# KeePassXC Release Preparation Helper +# Copyright (C) 2017 KeePassXC team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 or (at your option) +# version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +echo -e "\e[1m\e[32mKeePassXC\e[0m Release Preparation Helper" +echo -e "Copyright (C) 2017 KeePassXC Team \n" + + +# ----------------------------------------------------------------------- +# global default values +# ----------------------------------------------------------------------- +RELEASE_NAME="" +APP_NAME="KeePassXC" +SRC_DIR="." +GPG_KEY="CFB4C2166397D0D2" +GPG_GIT_KEY="" +OUTPUT_DIR="release" +SOURCE_BRANCH="" +TARGET_BRANCH="master" +TAG_NAME="" +DOCKER_IMAGE="" +DOCKER_CONTAINER_NAME="keepassxc-build-container" +CMAKE_OPTIONS="" +COMPILER="g++" +MAKE_OPTIONS="-j8" +BUILD_PLUGINS="autotype" +INSTALL_PREFIX="/usr/local" +BUILD_SOURCE_TARBALL=true +ORIG_BRANCH="" +ORIG_CWD="$(pwd)" + +# ----------------------------------------------------------------------- +# helper functions +# ----------------------------------------------------------------------- +printUsage() { + local cmd + if [ "" == "$1" ] || [ "help" == "$1" ]; then + cmd="COMMAND" + elif [ "merge" == "$1" ] || [ "build" == "$1" ] || [ "sign" == "$1" ]; then + cmd="$1" + else + logError "Unknown command: '$1'\n" + cmd="COMMAND" + fi + + echo -e "\e[1mUsage:\e[0m $(basename $0) $cmd [options]" + + if [ "COMMAND" == "$cmd" ]; then + cat << EOF + +Commands: + merge Merge release branch into main branch and create release tags + build Build and package binary release from sources + sign Sign previously compiled release packages + help Show help for the given command +EOF + elif [ "merge" == "$cmd" ]; then + cat << EOF + +Merge release branch into main branch and create release tags + +Options: + -v, --version Release version number or name (required) + -a, --app-name Application name (default: '${APP_NAME}') + -s, --source-dir Source directory (default: '${SRC_DIR}') + -g, --gpg-key GPG key used to sign the merge commit and release tag, + leave empty to let Git choose your default key + (default: '${GPG_GIT_KEY}') + -r, --release-branch Source release branch to merge from (default: 'release/VERSION') + --target-branch Target branch to merge to (default: '${TARGET_BRANCH}') + -t, --tag-name Override release tag name (defaults to version number) + -h, --help Show this help +EOF + elif [ "build" == "$cmd" ]; then + cat << EOF + +Build and package binary release from sources + +Options: + -v, --version Release version number or name (required) + -a, --app-name Application name (default: '${APP_NAME}') + -s, --source-dir Source directory (default: '${SRC_DIR}') + -o, --output-dir Output directory where to build the release + (default: '${OUTPUT_DIR}') + -t, --tag-name Release tag to check out (defaults to version number) + -b, --build Build sources after exporting release + -d, --docker-image Use the specified Docker image to compile the application. + The image must have all required build dependencies installed. + This option has no effect if --build is not set. + --container-name Docker container name (default: '${DOCKER_CONTAINER_NAME}') + The container must not exist already + -c, --cmake-options Additional CMake options for compiling the sources + --compiler Compiler to use (default: '${COMPILER}') + -m, --make-options Make options for compiling sources (default: '${MAKE_OPTIONS}') + -i, --install-prefix Install prefix (default: '${INSTALL_PREFIX}') + -p, --plugins Space-separated list of plugins to build + (default: ${BUILD_PLUGINS}) + -n, --no-source-tarball Don't build source tarball + -h, --help Show this help +EOF + elif [ "sign" == "$cmd" ]; then + cat << EOF + +Sign previously compiled release packages + +Options: + -f, --files Files to sign (required) + -g, --gpg-key GPG key used to sign the files (default: '${GPG_KEY}') + -h, --help Show this help +EOF + fi +} + +logInfo() { + echo -e "\e[1m[ \e[34mINFO\e[39m ]\e[0m $1" +} + +logError() { + echo -e "\e[1m[ \e[31mERROR\e[39m ]\e[0m $1" >&2 +} + +init() { + ORIG_CWD="$(pwd)" + cd "$SRC_DIR" > /dev/null 2>&1 + ORIG_BRANCH="$(git rev-parse --abbrev-ref HEAD 2> /dev/null)" + cd "$ORIG_CWD" +} + +cleanup() { + logInfo "Checking out original branch..." + if [ "" != "$ORIG_BRANCH" ]; then + git checkout "$ORIG_BRANCH" > /dev/null 2>&1 + fi + logInfo "Leaving source directory..." + cd "$ORIG_CWD" +} + +exitError() { + logError "$1" + cleanup + exit 1 +} + +exitTrap() { + exitError "Existing upon user request..." +} + +checkSourceDirExists() { + if [ ! -d "$SRC_DIR" ]; then + exitError "Source directory '${SRC_DIR}' does not exist!" + fi +} + +checkOutputDirDoesNotExist() { + if [ -e "$OUTPUT_DIR" ]; then + exitError "Output directory '$OUTPUT_DIR' already exists. Please choose a different location!" + fi +} + +checkGitRepository() { + if [ ! -d .git ] || [ ! -f CHANGELOG ]; then + exitError "Source directory is not a valid Git repository!" + fi +} + +checkTagExists() { + git tag | grep -q "$TAG_NAME" + if [ $? -ne 0 ]; then + exitError "Tag '${TAG_NAME}' does not exist!" + fi +} + +checkReleaseDoesNotExist() { + git tag | grep -q "$TAG_NAME" + if [ $? -eq 0 ]; then + exitError "Release '$RELEASE_NAME' (tag: '$TAG_NAME') already exists!" + fi +} + +checkWorkingTreeClean() { + git diff-index --quiet HEAD -- + if [ $? -ne 0 ]; then + exitError "Current working tree is not clean! Please commit or unstage any changes." + fi +} + +checkSourceBranchExists() { + git rev-parse "$SOURCE_BRANCH" > /dev/null 2>&1 + if [ $? -ne 0 ]; then + exitError "Source branch '$SOURCE_BRANCH' does not exist!" + fi +} + +checkTargetBranchExists() { + git rev-parse "$TARGET_BRANCH" > /dev/null 2>&1 + if [ $? -ne 0 ]; then + exitError "Target branch '$TARGET_BRANCH' does not exist!" + fi +} + +checkVersionInCMake() { + local app_name_upper="$(echo "$APP_NAME" | tr '[:lower:]' '[:upper:]')" + + grep -q "${app_name_upper}_VERSION \"${RELEASE_NAME}\"" CMakeLists.txt + if [ $? -ne 0 ]; then + exitError "${app_name_upper}_VERSION version not updated to '${RELEASE_NAME}' in CMakeLists.txt!" + fi + + grep -q "${app_name_upper}_VERSION_NUM \"${RELEASE_NAME}\"" CMakeLists.txt + if [ $? -ne 0 ]; then + exitError "${app_name_upper}_VERSION_NUM version not updated to '${RELEASE_NAME}' in CMakeLists.txt!" + fi +} + +checkChangeLog() { + if [ ! -f CHANGELOG ]; then + exitError "No CHANGELOG file found!" + fi + + grep -qPzo "${RELEASE_NAME} \(\d{4}-\d{2}-\d{2}\)\n=+\n" CHANGELOG + if [ $? -ne 0 ]; then + exitError "CHANGELOG does not contain any information about the '${RELEASE_NAME}' release!" + fi +} + +checkTransifexCommandExists() { + command -v tx > /dev/null + if [ 0 -ne $? ]; then + exitError "Transifex tool 'tx' not installed! Please install it using 'pip install transifex-client'" + fi +} + +# re-implement realpath for OS X (thanks mschrag) +# https://superuser.com/questions/205127/ +if $(command -v realpath > /dev/null); then + realpath() { + pushd . > /dev/null + if [ -d "$1" ]; then + cd "$1" + dirs -l +0 + else + cd "$(dirname "$1")" + cur_dir=$(dirs -l +0) + + if [ "$cur_dir" == "/" ]; then + echo "$cur_dir$(basename "$1")" + else + echo "$cur_dir/$(basename "$1")" + fi + fi + popd > /dev/null + } +fi + + +trap exitTrap SIGINT SIGTERM + + +# ----------------------------------------------------------------------- +# merge command +# ----------------------------------------------------------------------- +merge() { + while [ $# -ge 1 ]; do + local arg="$1" + case "$arg" in + -v|--version) + RELEASE_NAME="$2" + shift ;; + + -a|--app-name) + APP_NAME="$2" + shift ;; + + -s|--source-dir) + SRC_DIR="$2" + shift ;; + + -g|--gpg-key) + GPG_GIT_KEY="$2" + shift ;; + + -r|--release-branch) + SOURCE_BRANCH="$2" + shift ;; + + --target-branch) + TARGET_BRANCH="$2" + shift ;; + + -t|--tag-name) + TAG_NAME="$2" + shift ;; + + -h|--help) + printUsage "merge" + exit ;; + + *) + logError "Unknown option '$arg'\n" + printUsage "merge" + exit 1 ;; + esac + shift + done + + if [ "" == "$RELEASE_NAME" ]; then + logError "Missing arguments, --version is required!\n" + printUsage "merge" + exit 1 + fi + + if [ "" == "$TAG_NAME" ]; then + TAG_NAME="$RELEASE_NAME" + fi + + if [ "" == "$SOURCE_BRANCH" ]; then + SOURCE_BRANCH="release/${RELEASE_NAME}" + fi + + init + + SRC_DIR="$(realpath "$SRC_DIR")" + + logInfo "Performing basic checks..." + + checkSourceDirExists + + logInfo "Changing to source directory..." + cd "${SRC_DIR}" + + checkTransifexCommandExists + checkGitRepository + checkReleaseDoesNotExist + checkWorkingTreeClean + checkSourceBranchExists + checkTargetBranchExists + checkVersionInCMake + checkChangeLog + + logInfo "All checks pass, getting our hands dirty now!" + + logInfo "Checking out source branch..." + git checkout "$SOURCE_BRANCH" + + logInfo "Updating language files..." + ./share/translations/update.sh + if [ 0 -ne $? ]; then + exitError "Updating translations failed!" + fi + git diff-index --quiet HEAD -- + if [ $? -ne 0 ]; then + git add ./share/translations/* + logInfo "Committing changes..." + if [ "" == "$GPG_GIT_KEY" ]; then + git commit -m "Update translations" + else + git commit -m "Update translations" -S"$GPG_GIT_KEY" + fi + fi + + logInfo "Checking out target branch '${TARGET_BRANCH}'..." + git checkout "$TARGET_BRANCH" + + logInfo "Merging '${SOURCE_BRANCH}' into '${TARGET_BRANCH}'..." + + CHANGELOG=$(grep -Pzo "(?<=${RELEASE_NAME} \(\d{4}-\d{2}-\d{2}\)\n)=+\n\n?(?:.|\n)+?\n(?=\n)" \ + CHANGELOG | grep -Pzo '(?<=\n\n)(.|\n)+' | tr -d \\0) + COMMIT_MSG="Release ${RELEASE_NAME}" + + git merge "$SOURCE_BRANCH" --no-ff -m "$COMMIT_MSG" -m "${CHANGELOG}" "$SOURCE_BRANCH" -S"$GPG_GIT_KEY" + + logInfo "Creating tag '${TAG_NAME}'..." + if [ "" == "$GPG_GIT_KEY" ]; then + git tag -a "$TAG_NAME" -m "$COMMIT_MSG" -m "${CHANGELOG}" -s + else + git tag -a "$TAG_NAME" -m "$COMMIT_MSG" -m "${CHANGELOG}" -s -u "$GPG_GIT_KEY" + fi + + cleanup + + logInfo "All done!" + logInfo "Please merge the release branch back into the develop branch now and then push your changes." + logInfo "Don't forget to also push the tags using \e[1mgit push --tags\e[0m." +} + +# ----------------------------------------------------------------------- +# build command +# ----------------------------------------------------------------------- +build() { + while [ $# -ge 1 ]; do + local arg="$1" + case "$arg" in + -v|--version) + RELEASE_NAME="$2" + shift ;; + + -a|--app-name) + APP_NAME="$2" + shift ;; + + -s|--source-dir) + SRC_DIR="$2" + shift ;; + + -o|--output-dir) + OUTPUT_DIR="$2" + shift ;; + + -t|--tag-name) + TAG_NAME="$2" + shift ;; + + -d|--docker-image) + DOCKER_IMAGE="$2" + shift ;; + + --container-name) + DOCKER_CONTAINER_NAME="$2" + shift ;; + + -c|--cmake-options) + CMAKE_OPTIONS="$2" + shift ;; + + --compiler) + COMPILER="$2" + shift ;; + + -m|--make-options) + MAKE_OPTIONS="$2" + shift ;; + + -i|--install-prefix) + INSTALL_PREFIX="$2" + shift ;; + + -p|--plugins) + BUILD_PLUGINS="$2" + shift ;; + + -n|--no-source-tarball) + BUILD_SOURCE_TARBALL=false ;; + + -h|--help) + printUsage "build" + exit ;; + + *) + logError "Unknown option '$arg'\n" + printUsage "build" + exit 1 ;; + esac + shift + done + + if [ "" == "$RELEASE_NAME" ]; then + logError "Missing arguments, --version is required!\n" + printUsage "build" + exit 1 + fi + + if [ "" == "$TAG_NAME" ]; then + TAG_NAME="$RELEASE_NAME" + fi + + init + + SRC_DIR="$(realpath "$SRC_DIR")" + OUTPUT_DIR="$(realpath "$OUTPUT_DIR")" + + logInfo "Performing basic checks..." + + checkSourceDirExists + + logInfo "Changing to source directory..." + cd "${SRC_DIR}" + + checkTagExists + checkGitRepository + checkWorkingTreeClean + checkOutputDirDoesNotExist + + logInfo "All checks pass, getting our hands dirty now!" + + logInfo "Checking out release tag '${TAG_NAME}'..." + git checkout "$TAG_NAME" + + logInfo "Creating output directory..." + mkdir -p "$OUTPUT_DIR" + + if [ $? -ne 0 ]; then + exitError "Failed to create output directory!" + fi + + if $BUILD_SOURCE_TARBALL; then + logInfo "Creating source tarball..." + local app_name_lower="$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]')" + TARBALL_NAME="${app_name_lower}-${RELEASE_NAME}-src.tar.xz" + git archive --format=tar "$TAG_NAME" --prefix="${app_name_lower}-${RELEASE_NAME}/" \ + | xz -6 > "${OUTPUT_DIR}/${TARBALL_NAME}" + fi + + logInfo "Creating build directory..." + mkdir -p "${OUTPUT_DIR}/build-release" + cd "${OUTPUT_DIR}/build-release" + + logInfo "Configuring sources..." + for p in $BUILD_PLUGINS; do + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DWITH_XC_$(echo $p | tr '[:lower:]' '[:upper:]')=On" + done + + if [ "$COMPILER" == "g++" ]; then + export CC=gcc + elif [ "$COMPILER" == "clang++" ]; then + export CC=clang + fi + export CXX="$COMPILER" + + if [ "" == "$DOCKER_IMAGE" ]; then + if [ "$(uname -s)" == "Darwin" ]; then + # Building on OS X + local qt_vers="$(ls /usr/local/Cellar/qt5 2> /dev/null | sort -r | head -n1)" + if [ "" == "$qt_vers" ]; then + exitError "Couldn't find Qt5! Please make sure it is available in '/usr/local/Cellar/qt5'." + fi + export MACOSX_DEPLOYMENT_TARGET=10.7 + + logInfo "Configuring build..." + cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" \ + -DCMAKE_OSX_ARCHITECTURES=x86_64 -DWITH_CXX11=OFF \ + -DCMAKE_PREFIX_PATH="/usr/local/Cellar/qt5/${qt_vers}/lib/cmake" \ + -DQT_BINARY_DIR="/usr/local/Cellar/qt5/${qt_vers}/bin" $CMAKE_OPTIONS "$SRC_DIR" + + logInfo "Compiling and packaging sources..." + make $MAKE_OPTIONS package + + mv "./${APP_NAME}-${RELEASE_NAME}.dmg" ../ + elif [ "$(uname -o)" == "Msys" ]; then + # Building on Windows with Msys + logInfo "Configuring build..." + cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off -G"MSYS Makefiles" \ + -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" $CMAKE_OPTIONS "$SRC_DIR" + + logInfo "Compiling and packaging sources..." + make $MAKE_OPTIONS package + + mv "./${APP_NAME}-${RELEASE_NAME}-"*.{exe,zip} ../ + else + mkdir -p "${OUTPUT_DIR}/bin-release" + + # Building on Linux without Docker container + logInfo "Configuring build..." + cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off $CMAKE_OPTIONS \ + -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" "$SRC_DIR" + + logInfo "Compiling sources..." + make $MAKE_OPTIONS + + logInfo "Installing to bin dir..." + make DESTDIR="${OUTPUT_DIR}/bin-release" install/strip + + logInfo "Creating AppImage..." + ${SRC_DIR}/AppImage-Recipe.sh "$APP_NAME" "$RELEASE_NAME" + fi + else + mkdir -p "${OUTPUT_DIR}/bin-release" + + logInfo "Launching Docker container to compile sources..." + + docker run --name "$DOCKER_CONTAINER_NAME" --rm \ + --cap-add SYS_ADMIN --device /dev/fuse \ + -e "CC=${CC}" -e "CXX=${CXX}" \ + -v "$(realpath "$SRC_DIR"):/keepassxc/src:ro" \ + -v "$(realpath "$OUTPUT_DIR"):/keepassxc/out:rw" \ + "$DOCKER_IMAGE" \ + bash -c "cd /keepassxc/out/build-release && \ + cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off $CMAKE_OPTIONS \ + -DCMAKE_INSTALL_PREFIX=\"${INSTALL_PREFIX}\" /keepassxc/src && \ + make $MAKE_OPTIONS && make DESTDIR=/keepassxc/out/bin-release install/strip && \ + /keepassxc/src/AppImage-Recipe.sh "$APP_NAME" "$RELEASE_NAME"" + + if [ 0 -ne $? ]; then + exitError "Docker build failed!" + fi + + logInfo "Build finished, Docker container terminated." + fi + + cleanup + + logInfo "All done!" +} + + +# ----------------------------------------------------------------------- +# sign command +# ----------------------------------------------------------------------- +sign() { + SIGN_FILES=() + + while [ $# -ge 1 ]; do + local arg="$1" + case "$arg" in + -f|--files) + while [ "${2:0:1}" != "-" ] && [ $# -ge 2 ]; do + SIGN_FILES+=("$2") + shift + done ;; + + -g|--gpg-key) + GPG_KEY="$2" + shift ;; + + -h|--help) + printUsage "sign" + exit ;; + + *) + logError "Unknown option '$arg'\n" + printUsage "sign" + exit 1 ;; + esac + shift + done + + if [ -z "$SIGN_FILES" ]; then + logError "Missing arguments, --files is required!\n" + printUsage "sign" + exit 1 + fi + + for f in "${SIGN_FILES[@]}"; do + if [ ! -f "$f" ]; then + exitError "File '${f}' does not exist!" + fi + + logInfo "Signing file '${f}'..." + gpg --output "${f}.sig" --armor --local-user "$GPG_KEY" --detach-sig "$f" + + if [ 0 -ne $? ]; then + exitError "Signing failed!" + fi + + logInfo "Creating digest for file '${f}'..." + sha256sum "$f" > "${f}.DIGEST" + done + + logInfo "All done!" +} + + +# ----------------------------------------------------------------------- +# parse global command line +# ----------------------------------------------------------------------- +MODE="$1" +shift +if [ "" == "$MODE" ]; then + logError "Missing arguments!\n" + printUsage + exit 1 +elif [ "help" == "$MODE" ]; then + printUsage "$1" + exit +elif [ "merge" == "$MODE" ] || [ "build" == "$MODE" ] || [ "sign" == "$MODE" ]; then + $MODE "$@" +else + printUsage "$MODE" +fi diff --git a/share/windows/installer-header.bmp b/share/windows/installer-header.bmp new file mode 100644 index 000000000..f9e17cae7 Binary files /dev/null and b/share/windows/installer-header.bmp differ diff --git a/share/windows/installer-wizard.bmp b/share/windows/installer-wizard.bmp new file mode 100644 index 000000000..76d04518f Binary files /dev/null and b/share/windows/installer-wizard.bmp differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 30332c71e..8c3948842 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -218,6 +218,7 @@ target_link_libraries(${PROGNAME} Qt5::Widgets Qt5::Network ${GCRYPT_LIBRARIES} + ${GPGERROR_LIBRARIES} ${ZLIB_LIBRARIES}) set_target_properties(${PROGNAME} PROPERTIES ENABLE_EXPORTS ON) @@ -257,9 +258,25 @@ if(APPLE) endif() if(MINGW) - set(CPACK_GENERATOR "ZIP") + string(REPLACE "AMD" "Win" OUTPUT_FILE_POSTFIX "${CMAKE_HOST_SYSTEM_PROCESSOR}") + set(CPACK_GENERATOR "ZIP;NSIS") set(CPACK_STRIP_FILES ON) - set(CPACK_PACKAGE_FILE_NAME "${PROGNAME}-${KEEPASSXC_VERSION_NUM}") + set(CPACK_PACKAGE_FILE_NAME "${PROGNAME}-${KEEPASSXC_VERSION}-${OUTPUT_FILE_POSTFIX}") + set(CPACK_PACKAGE_INSTALL_DIRECTORY ${PROGNAME}) + set(CPACK_PACKAGE_VERSION ${KEEPASSXC_VERSION}) + set(CPACK_PACKAGE_VENDOR "${PROGNAME} Team") + string(REGEX REPLACE "/" "\\\\\\\\" CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/share/windows/installer-header.bmp") + set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE.GPL-2") + set(CPACK_NSIS_MUI_ICON "${CMAKE_SOURCE_DIR}/share/windows/keepassxc.ico") + set(CPACK_NSIS_MUI_UNIICON "${CPACK_NSIS_MUI_ICON}") + set(CPACK_NSIS_INSTALLED_ICON_NAME "\\\\${PROGNAME}.exe") + string(REGEX REPLACE "/" "\\\\\\\\" CPACK_NSIS_MUI_WELCOMEFINISHPAGE_BITMAP "${CMAKE_SOURCE_DIR}/share/windows/installer-wizard.bmp") + set(CPACK_NSIS_MUI_UNWELCOMEFINISHPAGE_BITMAP "${CPACK_NSIS_MUI_WELCOMEFINISHPAGE_BITMAP}") + set(CPACK_NSIS_CREATE_ICONS_EXTRA "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${PROGNAME}.lnk' '$INSTDIR\\\\${PROGNAME}.exe'") + set(CPACK_NSIS_DELETE_ICONS_EXTRA "Delete '$SMPROGRAMS\\\\$START_MENU\\\\${PROGNAME}.lnk'") + set(CPACK_NSIS_URL_INFO_ABOUT "https://keepassxc.org") + set(CPACK_NSIS_PACKAGE_NAME "${PROGNAME} v${KEEPASSXC_VERSION}") + set(CPACK_NSIS_MUI_FINISHPAGE_RUN "../${PROGNAME}.exe") include(CPack) install(CODE " @@ -267,5 +284,9 @@ if(MINGW) " COMPONENT Runtime) include(DeployQt4) - install_qt4_executable(${PROGNAME}.exe "qjpeg;qgif;qico;qtaccessiblewidgets") + install_qt4_executable(${PROGNAME}.exe) + add_custom_command(TARGET ${PROGNAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${Qt5Core_DIR}/../../../share/qt5/plugins/platforms/qwindows$<$:d>.dll + $) + install(FILES $/qwindows$<$:d>.dll DESTINATION "platforms") endif() diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index b2b06e7c8..46e2670ac 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -353,6 +353,12 @@ void Entry::setTitle(const QString& title) void Entry::setUrl(const QString& url) { + bool remove = url != m_attributes->value(EntryAttributes::URLKey) && + (m_attributes->value(EntryAttributes::RememberCmdExecAttr) == "1" || + m_attributes->value(EntryAttributes::RememberCmdExecAttr) == "0"); + if (remove) { + m_attributes->remove(EntryAttributes::RememberCmdExecAttr); + } m_attributes->set(EntryAttributes::URLKey, url, m_attributes->isProtected(EntryAttributes::URLKey)); } @@ -508,7 +514,8 @@ Entry* Entry::clone(CloneFlags flags) const entry->m_data.timeInfo.setLocationChanged(now); } - + if (flags & CloneRenameTitle) + entry->setTitle(entry->title() + tr(" - Clone")); return entry; } diff --git a/src/core/Entry.h b/src/core/Entry.h index 66b9362a6..ae60b596c 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -115,7 +115,8 @@ public: CloneNoFlags = 0, CloneNewUuid = 1, // generate a random uuid for the clone CloneResetTimeInfo = 2, // set all TimeInfo attributes to the current time - CloneIncludeHistory = 4 // clone the history items + CloneIncludeHistory = 4, // clone the history items + CloneRenameTitle = 8 // add "-Clone" after the original title }; Q_DECLARE_FLAGS(CloneFlags, CloneFlag) diff --git a/src/core/EntryAttributes.cpp b/src/core/EntryAttributes.cpp index 195a8f14a..b633cae32 100644 --- a/src/core/EntryAttributes.cpp +++ b/src/core/EntryAttributes.cpp @@ -24,6 +24,7 @@ const QString EntryAttributes::URLKey = "URL"; const QString EntryAttributes::NotesKey = "Notes"; const QStringList EntryAttributes::DefaultAttributes(QStringList() << TitleKey << UserNameKey << PasswordKey << URLKey << NotesKey); +const QString EntryAttributes::RememberCmdExecAttr = "_EXEC_CMD"; EntryAttributes::EntryAttributes(QObject* parent) : QObject(parent) diff --git a/src/core/EntryAttributes.h b/src/core/EntryAttributes.h index 1c0ddaaeb..211b6d483 100644 --- a/src/core/EntryAttributes.h +++ b/src/core/EntryAttributes.h @@ -52,6 +52,7 @@ public: static const QString URLKey; static const QString NotesKey; static const QStringList DefaultAttributes; + static const QString RememberCmdExecAttr; static bool isDefaultAttribute(const QString& key); Q_SIGNALS: diff --git a/src/core/EntrySearcher.cpp b/src/core/EntrySearcher.cpp index c0360a36c..01e152e2a 100644 --- a/src/core/EntrySearcher.cpp +++ b/src/core/EntrySearcher.cpp @@ -42,7 +42,11 @@ QList EntrySearcher::searchEntries(const QString& searchTerm, const Grou const QList children = group->children(); for (Group* childGroup : children) { if (childGroup->searchingEnabled() != Group::Disable) { - searchResult.append(searchEntries(searchTerm, childGroup, caseSensitivity)); + if (matchGroup(searchTerm, childGroup, caseSensitivity)) { + searchResult.append(childGroup->entriesRecursive()); + } else { + searchResult.append(searchEntries(searchTerm, childGroup, caseSensitivity)); + } } } @@ -69,3 +73,21 @@ bool EntrySearcher::wordMatch(const QString& word, Entry* entry, Qt::CaseSensiti entry->url().contains(word, caseSensitivity) || entry->notes().contains(word, caseSensitivity); } + +bool EntrySearcher::matchGroup(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity) +{ + const QStringList wordList = searchTerm.split(QRegExp("\\s"), QString::SkipEmptyParts); + for (const QString& word : wordList) { + if (!wordMatch(word, group, caseSensitivity)) { + return false; + } + } + + return true; +} + +bool EntrySearcher::wordMatch(const QString& word, const Group* group, Qt::CaseSensitivity caseSensitivity) +{ + return group->name().contains(word, caseSensitivity) || + group->notes().contains(word, caseSensitivity); +} diff --git a/src/core/EntrySearcher.h b/src/core/EntrySearcher.h index c7075dc9b..4e8d4eabe 100644 --- a/src/core/EntrySearcher.h +++ b/src/core/EntrySearcher.h @@ -33,6 +33,8 @@ private: QList searchEntries(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity); QList matchEntry(const QString& searchTerm, Entry* entry, Qt::CaseSensitivity caseSensitivity); bool wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity); + bool matchGroup(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity); + bool wordMatch(const QString& word, const Group* group, Qt::CaseSensitivity caseSensitivity); }; #endif // KEEPASSX_ENTRYSEARCHER_H diff --git a/src/gui/Application.cpp b/src/gui/Application.cpp index d982f22ca..26d9d2283 100644 --- a/src/gui/Application.cpp +++ b/src/gui/Application.cpp @@ -17,12 +17,20 @@ */ #include "Application.h" +#include "MainWindow.h" #include #include +#include #include "autotype/AutoType.h" +#if defined(Q_OS_UNIX) +#include +#include +#include +#endif + #if defined(Q_OS_UNIX) && !defined(Q_OS_OSX) class XcbEventFilter : public QAbstractNativeEventFilter { @@ -65,12 +73,18 @@ public: Application::Application(int& argc, char** argv) : QApplication(argc, argv) , m_mainWindow(nullptr) +#ifdef Q_OS_UNIX + , m_unixSignalNotifier(nullptr) +#endif { #if defined(Q_OS_UNIX) && !defined(Q_OS_OSX) installNativeEventFilter(new XcbEventFilter()); #elif defined(Q_OS_WIN) installNativeEventFilter(new WinEventFilter()); #endif +#if defined(Q_OS_UNIX) + registerUnixSignals(); +#endif } void Application::setMainWindow(QWidget* mainWindow) @@ -98,3 +112,57 @@ bool Application::event(QEvent* event) return QApplication::event(event); } + +#if defined(Q_OS_UNIX) +int Application::unixSignalSocket[2]; + +void Application::registerUnixSignals() +{ + int result = ::socketpair(AF_UNIX, SOCK_STREAM, 0, unixSignalSocket); + Q_ASSERT(0 == result); + if (0 != result) { + // do not register handles when socket creation failed, otherwise + // application will be unresponsive to signals such as SIGINT or SIGTERM + return; + } + + QVector const handledSignals = { SIGQUIT, SIGINT, SIGTERM, SIGHUP }; + for (auto s: handledSignals) { + struct sigaction sigAction; + + sigAction.sa_handler = handleUnixSignal; + sigemptyset(&sigAction.sa_mask); + sigAction.sa_flags = 0 | SA_RESTART; + sigaction(s, &sigAction, nullptr); + } + + m_unixSignalNotifier = new QSocketNotifier(unixSignalSocket[1], QSocketNotifier::Read, this); + connect(m_unixSignalNotifier, SIGNAL(activated(int)), this, SLOT(quitBySignal())); +} + +void Application::handleUnixSignal(int sig) +{ + switch (sig) { + case SIGQUIT: + case SIGINT: + case SIGTERM: + { + char buf = 0; + ::write(unixSignalSocket[0], &buf, sizeof(buf)); + return; + } + case SIGHUP: + return; + } +} + +void Application::quitBySignal() +{ + m_unixSignalNotifier->setEnabled(false); + char buf; + ::read(unixSignalSocket[1], &buf, sizeof(buf)); + + if (nullptr != m_mainWindow) + static_cast(m_mainWindow)->appExit(); +} +#endif diff --git a/src/gui/Application.h b/src/gui/Application.h index 149b61ddf..9bfe4d549 100644 --- a/src/gui/Application.h +++ b/src/gui/Application.h @@ -21,6 +21,8 @@ #include +class QSocketNotifier; + class Application : public QApplication { Q_OBJECT @@ -34,8 +36,23 @@ public: Q_SIGNALS: void openFile(const QString& filename); +private Q_SLOTS: +#if defined(Q_OS_UNIX) + void quitBySignal(); +#endif + private: QWidget* m_mainWindow; + +#if defined(Q_OS_UNIX) + /** + * Register Unix signals such as SIGINT and SIGTERM for clean shutdown. + */ + void registerUnixSignals(); + QSocketNotifier* m_unixSignalNotifier; + static void handleUnixSignal(int sig); + static int unixSignalSocket[2]; +#endif }; #endif // KEEPASSX_APPLICATION_H diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 00bf1168a..4a1298deb 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -312,8 +313,10 @@ void DatabaseWidget::cloneEntry() return; } - Entry* entry = currentEntry->clone(Entry::CloneNewUuid | Entry::CloneResetTimeInfo); + Entry* entry = currentEntry->clone(Entry::CloneNewUuid | Entry::CloneResetTimeInfo | Entry::CloneRenameTitle); entry->setGroup(currentEntry->group()); + if (isInSearchMode()) + search(m_lastSearchText); m_entryView->setFocus(); m_entryView->setCurrentEntry(entry); } @@ -494,8 +497,46 @@ void DatabaseWidget::openUrlForEntry(Entry* entry) } if (urlString.startsWith("cmd://")) { + // check if decision to execute command was stored + if (entry->attributes()->hasKey(EntryAttributes::RememberCmdExecAttr)) { + if (entry->attributes()->value(EntryAttributes::RememberCmdExecAttr) == "1") { + QProcess::startDetached(urlString.mid(6)); + } + return; + } + + // otherwise ask user if (urlString.length() > 6) { - QProcess::startDetached(urlString.mid(6)); + QString cmdTruncated = urlString.mid(6); + if (cmdTruncated.length() > 400) + cmdTruncated = cmdTruncated.left(400) + " […]"; + QMessageBox msgbox(QMessageBox::Icon::Question, + tr("Execute command?"), + tr("Do you really want to execute the following command?

%1
") + .arg(cmdTruncated.toHtmlEscaped()), + QMessageBox::Yes | QMessageBox::No, + this + ); + msgbox.setDefaultButton(QMessageBox::No); + + QCheckBox* checkbox = new QCheckBox(tr("Remember my choice"), &msgbox); + msgbox.setCheckBox(checkbox); + bool remember = false; + QObject::connect(checkbox, &QCheckBox::stateChanged, [&](int state) { + if (static_cast(state) == Qt::CheckState::Checked) { + remember = true; + } + }); + + int result = msgbox.exec(); + if (result == QMessageBox::Yes) { + QProcess::startDetached(urlString.mid(6)); + } + + if (remember) { + entry->attributes()->set(EntryAttributes::RememberCmdExecAttr, + result == QMessageBox::Yes ? "1" : "0"); + } } } else { @@ -722,15 +763,10 @@ void DatabaseWidget::unlockDatabase(bool accepted) replaceDatabase(db); - const QList groups = m_db->rootGroup()->groupsRecursive(true); - for (Group* group : groups) { - if (group->uuid() == m_groupBeforeLock) { - m_groupView->setCurrentGroup(group); - break; - } - } - + restoreGroupEntryFocus(m_groupBeforeLock, m_entryBeforeLock); m_groupBeforeLock = Uuid(); + m_entryBeforeLock = Uuid(); + setCurrentWidget(m_mainWidget); m_unlockDatabaseWidget->clearForms(); Q_EMIT unlockedDatabase(); @@ -755,7 +791,7 @@ void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::Mod void DatabaseWidget::switchToEntryEdit() { Entry* entry = m_entryView->currentEntry(); - Q_ASSERT(entry); + if (!entry) { return; } @@ -766,7 +802,7 @@ void DatabaseWidget::switchToEntryEdit() void DatabaseWidget::switchToGroupEdit() { Group* group = m_groupView->currentGroup(); - Q_ASSERT(group); + if (!group) { return; } @@ -943,6 +979,10 @@ void DatabaseWidget::lock() m_groupBeforeLock = m_db->rootGroup()->uuid(); } + if (m_entryView->currentEntry()) { + m_entryBeforeLock = m_entryView->currentEntry()->uuid(); + } + clearAllWidgets(); m_unlockDatabaseWidget->load(m_filename); setCurrentWidget(m_unlockDatabaseWidget); @@ -1028,7 +1068,22 @@ void DatabaseWidget::reloadDatabaseFile() } } + Uuid groupBeforeReload; + if (m_groupView && m_groupView->currentGroup()) { + groupBeforeReload = m_groupView->currentGroup()->uuid(); + } + else { + groupBeforeReload = m_db->rootGroup()->uuid(); + } + + Uuid entryBeforeReload; + if (m_entryView && m_entryView->currentEntry()) { + entryBeforeReload = m_entryView->currentEntry()->uuid(); + } + replaceDatabase(db); + restoreGroupEntryFocus(groupBeforeReload, entryBeforeReload); + } else { MessageBox::critical(this, tr("Autoreload Failed"), @@ -1061,6 +1116,35 @@ QStringList DatabaseWidget::customEntryAttributes() const return entry->attributes()->customKeys(); } +/* + * Restores the focus on the group and entry that was focused + * before the database was locked or reloaded. + */ +void DatabaseWidget::restoreGroupEntryFocus(Uuid groupUuid, Uuid entryUuid) +{ + Group* restoredGroup = nullptr; + const QList groups = m_db->rootGroup()->groupsRecursive(true); + for (Group* group : groups) { + if (group->uuid() == groupUuid) { + restoredGroup = group; + break; + } + } + + if (restoredGroup != nullptr) { + m_groupView->setCurrentGroup(restoredGroup); + + const QList entries = restoredGroup->entries(); + for (Entry* entry : entries) { + if (entry->uuid() == entryUuid) { + m_entryView->setCurrentEntry(entry); + break; + } + } + } + +} + bool DatabaseWidget::isGroupSelected() const { return m_groupView->currentGroup() != nullptr; diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index f55fa2027..79e58cecf 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -163,6 +163,7 @@ private Q_SLOTS: // Database autoreload slots void onWatchedFileChanged(); void reloadDatabaseFile(); + void restoreGroupEntryFocus(Uuid groupUuid, Uuid EntryUuid); private: void setClipboardTextAndMinimize(const QString& text); @@ -190,6 +191,7 @@ private: Group* m_newParent; QString m_filename; Uuid m_groupBeforeLock; + Uuid m_entryBeforeLock; // Search state QString m_lastSearchText; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index cc94ca9a9..819bda5dc 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -364,7 +364,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) bool groupSelected = dbWidget->isGroupSelected(); m_ui->actionEntryNew->setEnabled(!inSearch); - m_ui->actionEntryClone->setEnabled(singleEntrySelected && !inSearch); + m_ui->actionEntryClone->setEnabled(singleEntrySelected); m_ui->actionEntryEdit->setEnabled(singleEntrySelected); m_ui->actionEntryDelete->setEnabled(entriesSelected); m_ui->actionEntryCopyTitle->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTitle()); diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index cf2c9cd96..ab9924a75 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -42,6 +42,7 @@ public: public Q_SLOTS: void openDatabase(const QString& fileName, const QString& pw = QString(), const QString& keyFile = QString()); + void appExit(); protected: void closeEvent(QCloseEvent* event) override; @@ -68,7 +69,6 @@ private Q_SLOTS: void applySettingsChanges(); void trayIconTriggered(QSystemTrayIcon::ActivationReason reason); void toggleWindow(); - void appExit(); void lockDatabasesAfterInactivity(); void repairDatabase(); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 05b80caa2..188ef1586 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -155,15 +155,15 @@
- - - - + + + +
diff --git a/src/gui/SearchWidget.cpp b/src/gui/SearchWidget.cpp index 4ac01b3fc..933686dfa 100644 --- a/src/gui/SearchWidget.cpp +++ b/src/gui/SearchWidget.cpp @@ -41,6 +41,7 @@ SearchWidget::SearchWidget(QWidget *parent) connect(this, SIGNAL(escapePressed()), m_ui->searchEdit, SLOT(clear())); new QShortcut(Qt::CTRL + Qt::Key_F, this, SLOT(searchFocus()), nullptr, Qt::ApplicationShortcut); + new QShortcut(Qt::Key_Escape, m_ui->searchEdit, SLOT(clear()), nullptr, Qt::ApplicationShortcut); m_ui->searchEdit->installEventFilter(this); diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index f9b104912..f2372a0d5 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -89,6 +89,7 @@ void EditEntryWidget::setupMain() add(tr("Entry"), m_mainWidget); m_mainUi->togglePasswordButton->setIcon(filePath()->onOffIcon("actions", "password-show")); + m_mainUi->togglePasswordGeneratorButton->setIcon(filePath()->icon("actions", "password-generator", false)); connect(m_mainUi->togglePasswordButton, SIGNAL(toggled(bool)), m_mainUi->passwordEdit, SLOT(setShowPassword(bool))); connect(m_mainUi->togglePasswordGeneratorButton, SIGNAL(toggled(bool)), SLOT(togglePasswordGeneratorButton(bool))); connect(m_mainUi->expireCheck, SIGNAL(toggled(bool)), m_mainUi->expireDatePicker, SLOT(setEnabled(bool))); @@ -434,6 +435,9 @@ void EditEntryWidget::saveEntry() void EditEntryWidget::updateEntryData(Entry* entry) const { + entry->attributes()->copyCustomKeysFrom(m_entryAttributes); + entry->attachments()->copyDataFrom(m_entryAttachments); + entry->setTitle(m_mainUi->titleEdit->text()); entry->setUsername(m_mainUi->usernameEdit->text()); entry->setUrl(m_mainUi->urlEdit->text()); @@ -443,9 +447,6 @@ void EditEntryWidget::updateEntryData(Entry* entry) const entry->setNotes(m_mainUi->notesEdit->toPlainText()); - entry->attributes()->copyCustomKeysFrom(m_entryAttributes); - entry->attachments()->copyDataFrom(m_entryAttachments); - IconStruct iconStruct = m_iconsWidget->state(); if (iconStruct.number < 0) { diff --git a/src/gui/entry/EditEntryWidgetMain.ui b/src/gui/entry/EditEntryWidgetMain.ui index 083f1c033..b896963c0 100644 --- a/src/gui/entry/EditEntryWidgetMain.ui +++ b/src/gui/entry/EditEntryWidgetMain.ui @@ -77,9 +77,6 @@ - - Generate - true diff --git a/src/main.cpp b/src/main.cpp index a94d65eaa..224e54d1e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -28,6 +28,16 @@ #include "gui/MainWindow.h" #include "gui/MessageBox.h" +#ifdef QT_STATIC +#include + +#ifdef Q_OS_WIN +Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin) +#elif Q_OS_LINUX +Q_IMPORT_PLUGIN(QXcbIntegrationPlugin) +#endif +#endif + int main(int argc, char** argv) { #ifdef QT_NO_DEBUG diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0ea73b2fe..5840a5b4b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -92,6 +92,7 @@ set(TEST_LIBRARIES Qt5::Widgets Qt5::Test ${GCRYPT_LIBRARIES} + ${GPGERROR_LIBRARIES} ${ZLIB_LIBRARIES} ) diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index c23226a28..0c776e021 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -481,8 +481,7 @@ void TestGui::testSearch() QCOMPARE(entry->title(), origTitle.append("_edited")); // Cancel search, should return to normal view - QTest::mouseClick(searchTextEdit, Qt::LeftButton); - QTest::keyClick(searchTextEdit, Qt::Key_Escape); + QTest::keyClick(m_mainWindow, Qt::Key_Escape); QTRY_COMPARE(m_dbWidget->currentMode(), DatabaseWidget::ViewMode); } @@ -567,7 +566,7 @@ void TestGui::testCloneEntry() QCOMPARE(entryView->model()->rowCount(), 2); Entry* entryClone = entryView->entryFromIndex(entryView->model()->index(1, 1)); QVERIFY(entryOrg->uuid() != entryClone->uuid()); - QCOMPARE(entryClone->title(), entryOrg->title()); + QCOMPARE(entryClone->title(), entryOrg->title() + QString(" - Clone")); } void TestGui::testDragAndDropEntry() diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 846e39230..83f00b4bc 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -20,6 +20,7 @@ target_link_libraries(kdbx-extract keepassx_core Qt5::Core ${GCRYPT_LIBRARIES} + ${GPGERROR_LIBRARIES} ${ZLIB_LIBRARIES}) add_executable(kdbx-merge kdbx-merge.cpp) @@ -27,6 +28,7 @@ target_link_libraries(kdbx-merge keepassx_core Qt5::Core ${GCRYPT_LIBRARIES} + ${GPGERROR_LIBRARIES} ${ZLIB_LIBRARIES})