Merge branch 'meta/release-preparation' into release/2.1.1

This commit is contained in:
Janek Bevendorff 2017-02-06 23:27:05 +01:00
commit 26ff528a85
No known key found for this signature in database
GPG Key ID: CFEC2F6850BFFA53
31 changed files with 1068 additions and 436 deletions

1
.gitattributes vendored
View File

@ -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

View File

@ -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, 20151118](https://medium.com/@jmaynard/a-contribution-policy-for-open-source-that-works-bfc4600c9d83#.i9ntbhmad)
**Source**: [Version 0.3, 20151118](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, 20111119: 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: `<widget class="QCheckBox" name="rememberCheckBox">`
@ -175,3 +176,5 @@ Example: `<widget class="QCheckBox" name="rememberCheckBox">`
[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

View File

@ -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

View File

@ -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)

View File

@ -14,21 +14,41 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -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)

View File

@ -1,350 +0,0 @@
#!/usr/bin/env bash
#
# KeePassXC Release Preparation Helper
# Copyright (C) 2017 KeePassXC team <https://keepassxc.org/>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 or (at your option)
# version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
echo -e "\e[1m\e[32mKeePassXC\e[0m Release Preparation Helper"
echo -e "Copyright (C) 2017 KeePassXC Team <https://keepassxc.org/>\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."

682
release-tool Executable file
View File

@ -0,0 +1,682 @@
#!/usr/bin/env bash
#
# KeePassXC Release Preparation Helper
# Copyright (C) 2017 KeePassXC team <https://keepassxc.org/>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 or (at your option)
# version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
echo -e "\e[1m\e[32mKeePassXC\e[0m Release Preparation Helper"
echo -e "Copyright (C) 2017 KeePassXC Team <https://keepassxc.org/>\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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

View File

@ -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$<$<CONFIG:Debug>:d>.dll
$<TARGET_FILE_DIR:${PROGNAME}>)
install(FILES $<TARGET_FILE_DIR:${PROGNAME}>/qwindows$<$<CONFIG:Debug>:d>.dll DESTINATION "platforms")
endif()

View File

@ -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;
}

View File

@ -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)

View File

@ -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)

View File

@ -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:

View File

@ -42,7 +42,11 @@ QList<Entry*> EntrySearcher::searchEntries(const QString& searchTerm, const Grou
const QList<Group*> 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);
}

View File

@ -33,6 +33,8 @@ private:
QList<Entry*> searchEntries(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity);
QList<Entry*> 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

View File

@ -17,12 +17,20 @@
*/
#include "Application.h"
#include "MainWindow.h"
#include <QAbstractNativeEventFilter>
#include <QFileOpenEvent>
#include <QSocketNotifier>
#include "autotype/AutoType.h"
#if defined(Q_OS_UNIX)
#include <signal.h>
#include <unistd.h>
#include <sys/socket.h>
#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<int> 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<MainWindow*>(m_mainWindow)->appExit();
}
#endif

View File

@ -21,6 +21,8 @@
#include <QApplication>
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

View File

@ -19,6 +19,7 @@
#include <QAction>
#include <QDesktopServices>
#include <QCheckBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QFile>
@ -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?<br><br>%1<br>")
.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<Qt::CheckState>(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<Group*> 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<Group*> 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<Entry*> 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;

View File

@ -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;

View File

@ -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());

View File

@ -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();

View File

@ -155,15 +155,15 @@
<addaction name="actionEntryCopyNotes"/>
<addaction name="separator"/>
</widget>
<addaction name="actionEntryNew"/>
<addaction name="actionEntryClone"/>
<addaction name="actionEntryEdit"/>
<addaction name="actionEntryDelete"/>
<addaction name="actionEntryCopyUsername"/>
<addaction name="actionEntryCopyPassword"/>
<addaction name="menuEntryCopyAttribute"/>
<addaction name="actionEntryAutoType"/>
<addaction name="actionEntryOpenUrl"/>
<addaction name="actionEntryEdit"/>
<addaction name="actionEntryClone"/>
<addaction name="actionEntryDelete"/>
<addaction name="actionEntryNew"/>
</widget>
<widget class="QMenu" name="menuGroups">
<property name="title">

View File

@ -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);

View File

@ -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) {

View File

@ -77,9 +77,6 @@
</item>
<item>
<widget class="QToolButton" name="togglePasswordGeneratorButton">
<property name="text">
<string>Generate</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>

View File

@ -28,6 +28,16 @@
#include "gui/MainWindow.h"
#include "gui/MessageBox.h"
#ifdef QT_STATIC
#include <QtPlugin>
#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

View File

@ -92,6 +92,7 @@ set(TEST_LIBRARIES
Qt5::Widgets
Qt5::Test
${GCRYPT_LIBRARIES}
${GPGERROR_LIBRARIES}
${ZLIB_LIBRARIES}
)

View File

@ -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()

View File

@ -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})