From 198691182b1cf0fbfa3664045768c3af583bbf9e Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Wed, 18 Jan 2017 00:15:33 +0100 Subject: [PATCH 01/49] Implement clean shutdown after receiving Unix signals, resolves #169 The database wasn't saved properly and lockfiles were not removed when receiving the signals SIGINT, SIGTERM, SIGQUIT or SIGHUP. This patch implements signal handling and performs a clean shutdown after receiving SIGINT SIGTERM or SIGQUIT and ignores SIGHUP. Since this uses POSIX syscalls for signal and socket handling, there is no Windows implementation at the moment. --- src/gui/Application.cpp | 67 ++++++++++++++++++++++++++++++++++++++++- src/gui/Application.h | 17 ++++++++++- src/gui/MainWindow.h | 2 +- 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/gui/Application.cpp b/src/gui/Application.cpp index d982f22c..8c4a21f8 100644 --- a/src/gui/Application.cpp +++ b/src/gui/Application.cpp @@ -17,12 +17,21 @@ */ #include "Application.h" +#include "MainWindow.h" #include #include +#include #include "autotype/AutoType.h" +#if defined(Q_OS_UNIX) +#include +#include +#include +#endif + +class MainWindow; #if defined(Q_OS_UNIX) && !defined(Q_OS_OSX) class XcbEventFilter : public QAbstractNativeEventFilter { @@ -64,13 +73,16 @@ public: Application::Application(int& argc, char** argv) : QApplication(argc, argv) - , m_mainWindow(nullptr) + , m_mainWindow(nullptr), m_unixSignalNotifier(nullptr) { #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 +110,56 @@ 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() +{ + 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 149b61dd..44e03d2f 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,21 @@ public: Q_SIGNALS: void openFile(const QString& filename); -private: +private Q_SLOTS: + void quitBySignal(); + +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/MainWindow.h b/src/gui/MainWindow.h index cf2c9cd9..ab9924a7 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(); From b5cf6c7161090bc8a266bdc11145ecf446d336de Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Wed, 18 Jan 2017 00:33:47 +0100 Subject: [PATCH 02/49] Add missing #ifdef around slot --- src/gui/Application.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/gui/Application.h b/src/gui/Application.h index 44e03d2f..9bfe4d54 100644 --- a/src/gui/Application.h +++ b/src/gui/Application.h @@ -37,9 +37,11 @@ Q_SIGNALS: void openFile(const QString& filename); private Q_SLOTS: +#if defined(Q_OS_UNIX) void quitBySignal(); +#endif -private: +private: QWidget* m_mainWindow; #if defined(Q_OS_UNIX) From 4eb39dc5ff06e247c022cb33d3803331270f432a Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Wed, 18 Jan 2017 00:39:36 +0100 Subject: [PATCH 03/49] Remove obsolete forward-declaration and disable QSocketNotifier after firing --- src/gui/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/Application.cpp b/src/gui/Application.cpp index 8c4a21f8..cfc87c10 100644 --- a/src/gui/Application.cpp +++ b/src/gui/Application.cpp @@ -31,7 +31,6 @@ #include #endif -class MainWindow; #if defined(Q_OS_UNIX) && !defined(Q_OS_OSX) class XcbEventFilter : public QAbstractNativeEventFilter { @@ -156,6 +155,7 @@ void Application::handleUnixSignal(int sig) void Application::quitBySignal() { + m_unixSignalNotifier->setEnabled(false); char buf; ::read(unixSignalSocket[1], &buf, sizeof(buf)); From 62808f834206b76217496c4d164ca43d5d952ef7 Mon Sep 17 00:00:00 2001 From: Ryan Olds Date: Mon, 23 Jan 2017 19:23:21 -0800 Subject: [PATCH 04/49] Adjusted order of entry's context menu --- src/gui/MainWindow.ui | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 05b80caa..81ac4a23 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -155,15 +155,15 @@ - - - - - + + + + + From a5f12db6bada6f72d4e8a8a23fa760dfd979bfe3 Mon Sep 17 00:00:00 2001 From: Ryan Olds Date: Tue, 24 Jan 2017 10:31:49 -0800 Subject: [PATCH 05/49] Moved autotype after copyattribute --- src/gui/MainWindow.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 81ac4a23..188ef158 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -158,12 +158,12 @@ + - - + From cdbf58b2c13186dc487f915a717639ec44b95d35 Mon Sep 17 00:00:00 2001 From: louib Date: Tue, 24 Jan 2017 22:17:16 -0500 Subject: [PATCH 06/49] Preserve group/entry focus when replacing db. (#176) --- src/gui/DatabaseWidget.cpp | 59 ++++++++++++++++++++++++++++++++------ src/gui/DatabaseWidget.h | 2 ++ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 985374c4..f08b432f 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -722,15 +722,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(); @@ -943,6 +938,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 +1027,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 +1075,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 f55fa202..79e58cec 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; From 70727895f7cbbbe7dad17cf9d0fa73969c58eb58 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Tue, 24 Jan 2017 22:24:34 -0500 Subject: [PATCH 07/49] Added ifdef guard --- src/gui/Application.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/gui/Application.cpp b/src/gui/Application.cpp index cfc87c10..26d9d228 100644 --- a/src/gui/Application.cpp +++ b/src/gui/Application.cpp @@ -72,7 +72,10 @@ public: Application::Application(int& argc, char** argv) : QApplication(argc, argv) - , m_mainWindow(nullptr), m_unixSignalNotifier(nullptr) + , m_mainWindow(nullptr) +#ifdef Q_OS_UNIX + , m_unixSignalNotifier(nullptr) +#endif { #if defined(Q_OS_UNIX) && !defined(Q_OS_OSX) installNativeEventFilter(new XcbEventFilter()); From 3c687d29d898a09ba5078ef0c6e5a4cc06bcf3b3 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sun, 22 Jan 2017 20:14:37 +0100 Subject: [PATCH 08/49] Make regexp less strict --- make_release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make_release.sh b/make_release.sh index 19c661b3..2f8fd5e4 100755 --- a/make_release.sh +++ b/make_release.sh @@ -256,7 +256,7 @@ 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 "(?<=${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}" From 1c12cd6b9e3159999098ca3d4e765104bb4e19f1 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sun, 22 Jan 2017 22:39:05 +0100 Subject: [PATCH 09/49] Add wget, file, fuse and python for building AppImages inside the container --- AppImage-Recipe.sh | 4 ++-- Dockerfile | 6 +++++- make_release.sh | 11 +++++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/AppImage-Recipe.sh b/AppImage-Recipe.sh index 9575f077..322b5464 100755 --- a/AppImage-Recipe.sh +++ b/AppImage-Recipe.sh @@ -41,8 +41,8 @@ wget -q https://github.com/probonopd/AppImages/raw/master/functions.sh -O ./func cd $APP.AppDir cp -a ../../bin-release/* . -mv ./usr/local/* ./usr -rmdir ./usr/local +cp -a ./usr/local/* ./usr +rm -R ./usr/local patch_strings_in_file /usr/local ./ patch_strings_in_file /usr ./ diff --git a/Dockerfile b/Dockerfile index 3aee19e3..44400993 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,11 @@ RUN set -x \ libqt5x11extras5-dev \ libxi-dev \ libxtst-dev \ - zlib1g-dev + zlib1g-dev \ + wget \ + file \ + fuse \ + python VOLUME /keepassxc/src VOLUME /keepassxc/out diff --git a/make_release.sh b/make_release.sh index 2f8fd5e4..8a7281c8 100755 --- a/make_release.sh +++ b/make_release.sh @@ -89,6 +89,7 @@ logError() { } exitError() { + return logError "$1" if [ "" != "$ORIG_BRANCH" ]; then git checkout "$ORIG_BRANCH" > /dev/null 2>&1 @@ -309,10 +310,14 @@ if $BUILD_SOURCES; then 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" else 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" \ @@ -320,13 +325,11 @@ if $BUILD_SOURCES; then 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" + make $MAKE_OPTIONS && make DESTDIR=/keepassxc/out/bin-release install/strip && \ + /keepassxc/src/AppImage-Recipe.sh "$APP_NAME" "$RELEASE_NAME"" logInfo "Build finished, Docker container terminated." fi - - logInfo "Creating AppImage..." - ${SRC_DIR}/AppImage-Recipe.sh "$APP_NAME" "$RELEASE_NAME" cd .. logInfo "Signing source tarball..." From 0456815bd5cdcbd5c86375333b97fb81ed4cfee5 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Mon, 23 Jan 2017 01:06:31 +0100 Subject: [PATCH 10/49] Fix AppImage not launching on all platforms --- AppImage-Recipe.sh | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/AppImage-Recipe.sh b/AppImage-Recipe.sh index 322b5464..d9cbd565 100755 --- a/AppImage-Recipe.sh +++ b/AppImage-Recipe.sh @@ -43,13 +43,23 @@ cd $APP.AppDir cp -a ../../bin-release/* . cp -a ./usr/local/* ./usr rm -R ./usr/local -patch_strings_in_file /usr/local ./ +patch_strings_in_file /usr/local ././ patch_strings_in_file /usr ./ +# bundle Qt platform plugins +QXCB_PLUGIN="$(find /usr/lib -name 'libqxcb.so' 2> /dev/null)" +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 get_desktopintegration $LOWERAPP @@ -58,7 +68,7 @@ GLIBC_NEEDED=$(glibc_needed) cd .. -generate_appimage +generate_type2_appimage mv ../out/*.AppImage .. rmdir ../out > /dev/null 2>&1 From 86cdb64b1d270ae5a1f95bb1ebbccc479a95e6e1 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Mon, 23 Jan 2017 01:11:20 +0100 Subject: [PATCH 11/49] Re-enable checks --- make_release.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/make_release.sh b/make_release.sh index 8a7281c8..58f03534 100755 --- a/make_release.sh +++ b/make_release.sh @@ -89,7 +89,6 @@ logError() { } exitError() { - return logError "$1" if [ "" != "$ORIG_BRANCH" ]; then git checkout "$ORIG_BRANCH" > /dev/null 2>&1 From 1310b34e9c9d7b70c8eb00c55680b72dee8f5e13 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Tue, 24 Jan 2017 22:37:02 -0500 Subject: [PATCH 12/49] Added NSIS installer to CPack packager for Windows --- src/CMakeLists.txt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 30332c71..22ca342c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -257,9 +257,19 @@ if(APPLE) endif() if(MINGW) - set(CPACK_GENERATOR "ZIP") + 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}") + set(CPACK_PACKAGE_INSTALL_DIRECTORY ${PROGNAME}) + set(CPACK_PACKAGE_VERSION ${KEEPASSXC_VERSION}) + set(CPACK_PACKAGE_VENDOR "${PROGNAME} Team") + 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_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 " From 292ed892c105de7281654796abb1e6aa4c8779ab Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Thu, 26 Jan 2017 01:15:12 +0100 Subject: [PATCH 13/49] Fix Windows linker and runtime errors when building against static Qt --- cmake/FindLibGPGError.cmake | 9 +++++++++ src/CMakeLists.txt | 1 + src/main.cpp | 10 ++++++++++ tests/CMakeLists.txt | 1 + utils/CMakeLists.txt | 2 ++ 5 files changed, 23 insertions(+) create mode 100644 cmake/FindLibGPGError.cmake diff --git a/cmake/FindLibGPGError.cmake b/cmake/FindLibGPGError.cmake new file mode 100644 index 00000000..fe9ef912 --- /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/src/CMakeLists.txt b/src/CMakeLists.txt index 22ca342c..b843de85 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) diff --git a/src/main.cpp b/src/main.cpp index a94d65ea..224e54d1 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 0ea73b2f..5840a5b4 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/utils/CMakeLists.txt b/utils/CMakeLists.txt index 846e3923..83f00b4b 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}) From 6ccae6cc37dbe5123c266f768c48fbd23136cdc8 Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Thu, 26 Jan 2017 00:30:16 +0900 Subject: [PATCH 14/49] Pressing escape quits search --- src/gui/SearchWidget.cpp | 1 + tests/gui/TestGui.cpp | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/SearchWidget.cpp b/src/gui/SearchWidget.cpp index 4ac01b3f..933686df 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/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index c23226a2..7d667760 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); } From 16ed89c471444317fb513f6b29b34978b477ac7b Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Wed, 25 Jan 2017 20:02:32 -0500 Subject: [PATCH 15/49] Implement ability to clone an entry when in search mode. * Cloned entries have "- Clone" appended to their name --- src/core/Entry.cpp | 3 ++- src/core/Entry.h | 3 ++- src/gui/DatabaseWidget.cpp | 4 +++- src/gui/MainWindow.cpp | 2 +- tests/gui/TestGui.cpp | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index b2b06e7c..ecc275a2 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -508,7 +508,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 66b9362a..ae60b596 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/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index f08b432f..6e3398c2 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -312,8 +312,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); } diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index cc94ca9a..819bda5d 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/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 7d667760..0c776e02 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -566,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() From 1554722a8382afbff47bcb0cc18f69194d923805 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Thu, 26 Jan 2017 02:58:46 +0100 Subject: [PATCH 16/49] Add missing find_package call --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 051aba19..2fd890e0 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) From 66253e142bfcd58f614b57c20d6c91845e5ba5d2 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Thu, 26 Jan 2017 20:08:53 +0100 Subject: [PATCH 17/49] Install qwindows platform abstraction plugin on Windows --- src/CMakeLists.txt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b843de85..69af4359 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -258,9 +258,10 @@ if(APPLE) endif() if(MINGW) + 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}") + 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") @@ -278,5 +279,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() From 11dec27dd169a04920e1e0cb67c1bdacaf80e284 Mon Sep 17 00:00:00 2001 From: rockihack Date: Thu, 26 Jan 2017 21:09:57 +0100 Subject: [PATCH 18/49] MacOS: Fix Global Autotype when frontmost window title is empty. --- src/autotype/mac/AutoTypeMac.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/autotype/mac/AutoTypeMac.cpp b/src/autotype/mac/AutoTypeMac.cpp index 90563a23..e55c336c 100644 --- a/src/autotype/mac/AutoTypeMac.cpp +++ b/src/autotype/mac/AutoTypeMac.cpp @@ -98,7 +98,9 @@ QString AutoTypePlatformMac::activeWindowTitle() if (windowLayer(window) == 0) { // First toplevel window in list (front to back order) title = windowTitle(window); - break; + if (!title.isEmpty()) { + break; + } } } From 4ed03c2db27794b7ae582a19446bd952552d5b0e Mon Sep 17 00:00:00 2001 From: Louis-Bertrand Varin Date: Thu, 26 Jan 2017 21:00:52 -0500 Subject: [PATCH 19/49] Reuse password generator icon. --- src/gui/entry/EditEntryWidget.cpp | 1 + src/gui/entry/EditEntryWidgetMain.ui | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index d9ba5bd8..c823b3de 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))); diff --git a/src/gui/entry/EditEntryWidgetMain.ui b/src/gui/entry/EditEntryWidgetMain.ui index 083f1c03..b896963c 100644 --- a/src/gui/entry/EditEntryWidgetMain.ui +++ b/src/gui/entry/EditEntryWidgetMain.ui @@ -77,9 +77,6 @@ - - Generate - true From b97024c8f622b239c27d2d378257bbde201621b3 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Fri, 27 Jan 2017 20:42:27 +0100 Subject: [PATCH 20/49] Add more KeePassXC branding to the Windows installer --- src/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 69af4359..8c394884 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -265,8 +265,13 @@ if(MINGW) 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") From aa6f6177152ffe314d7ad78ee4083bcb02eae946 Mon Sep 17 00:00:00 2001 From: Edward Jones Date: Fri, 27 Jan 2017 21:08:08 +0000 Subject: [PATCH 21/49] Update CONTRIBUTING.md * Replace instances of 'KeePassX Reboot' with 'KeePassXC' * Lowercase headers to be consistent with README * Add more headers to the table of contents * Make the link to the issue tracker more prominent (preferred over Google Groups, apparently) * Add information about the #keepassxc-dev IRC channel on Freenode * Add 'hotfix' to the branch strategy (seems in the standard and is also used) * Rephrase some paragraphs to make them clearer, fix a few typos --- .github/CONTRIBUTING.md | 89 +++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 01b8d613..67b0e174 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 From 7ea306a61a7769042012ec267db64ca3b1a2c3ac Mon Sep 17 00:00:00 2001 From: Edward Jones Date: Fri, 27 Jan 2017 23:00:36 +0000 Subject: [PATCH 22/49] Prompt the user before executing a command in a cmd:// URL Fixes #51. (Does not have a "don't ask me anymore" option.) --- src/gui/DatabaseWidget.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 6e3398c2..9b19d161 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -497,7 +497,16 @@ void DatabaseWidget::openUrlForEntry(Entry* entry) if (urlString.startsWith("cmd://")) { if (urlString.length() > 6) { - QProcess::startDetached(urlString.mid(6)); + QMessageBox::StandardButton result; + result = MessageBox::question( + this, tr("Execute command?"), + tr("Do you really want to execute the following command?

%1") + .arg(urlString.left(200).toHtmlEscaped()), + QMessageBox::Yes | QMessageBox::No); + + if (result == QMessageBox::Yes) { + QProcess::startDetached(urlString.mid(6)); + } } } else { From 01e9d39b63b500944c59adbe163e6d5a8bfa57b0 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sat, 28 Jan 2017 14:18:43 +0100 Subject: [PATCH 23/49] Add 'Remember my choice' checkbox Allow user to store preference when asked whether to execute command, resolves #51 --- src/core/Entry.cpp | 6 +++++ src/core/EntryAttributes.cpp | 1 + src/core/EntryAttributes.h | 1 + src/gui/DatabaseWidget.cpp | 44 ++++++++++++++++++++++++++----- src/gui/entry/EditEntryWidget.cpp | 6 ++--- 5 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index ecc275a2..46e2670a 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)); } diff --git a/src/core/EntryAttributes.cpp b/src/core/EntryAttributes.cpp index 195a8f14..b633cae3 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 1c0ddaae..211b6d48 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/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 9b19d161..46b15f5d 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -496,17 +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) { - QMessageBox::StandardButton result; - result = MessageBox::question( - this, tr("Execute command?"), - tr("Do you really want to execute the following command?

%1") - .arg(urlString.left(200).toHtmlEscaped()), - QMessageBox::Yes | QMessageBox::No); - + QString cmdTruncated = urlString; + 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 { diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index c823b3de..f3535f9b 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -434,6 +434,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 +446,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) { From 3e6f76826b4207dfac13eeff375b30ca0149374d Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sat, 28 Jan 2017 14:24:33 +0100 Subject: [PATCH 24/49] Don't show cmd:// prefix in confirmation dialog --- src/gui/DatabaseWidget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 46b15f5d..ec65ad90 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -507,7 +507,7 @@ void DatabaseWidget::openUrlForEntry(Entry* entry) // otherwise ask user if (urlString.length() > 6) { - QString cmdTruncated = urlString; + QString cmdTruncated = urlString.mid(6); if (cmdTruncated.length() > 400) cmdTruncated = cmdTruncated.left(400) + " […]"; QMessageBox msgbox(QMessageBox::Icon::Question, From a3fd3205a9961b11b92137b344b5520690e0db00 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sat, 28 Jan 2017 18:27:20 +0200 Subject: [PATCH 25/49] KeePassX PR Migration: #190 Search for Group Names (#168) * Search Group details in addition to entry details; feature parity with KeePass * Remove assertions to prevent crashes in Debug mode when search result is empty --- src/core/EntrySearcher.cpp | 24 +++++++++++++++++++++++- src/core/EntrySearcher.h | 2 ++ src/gui/DatabaseWidget.cpp | 4 ++-- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/core/EntrySearcher.cpp b/src/core/EntrySearcher.cpp index c0360a36..01e152e2 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 c7075dc9..4e8d4eab 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/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index ec65ad90..f1ab0410 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -791,7 +791,7 @@ void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::Mod void DatabaseWidget::switchToEntryEdit() { Entry* entry = m_entryView->currentEntry(); - Q_ASSERT(entry); + if (!entry) { return; } @@ -802,7 +802,7 @@ void DatabaseWidget::switchToEntryEdit() void DatabaseWidget::switchToGroupEdit() { Group* group = m_groupView->currentGroup(); - Q_ASSERT(group); + if (!group) { return; } From c87c8117194594a6c9ca4a5e6dc4db502b58551c Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sat, 28 Jan 2017 22:11:05 +0100 Subject: [PATCH 26/49] Make release script more modular and useful on other platforms --- make_release.sh | 352 ---------------------------- release-tool | 596 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 596 insertions(+), 352 deletions(-) delete mode 100755 make_release.sh create mode 100755 release-tool diff --git a/make_release.sh b/make_release.sh deleted file mode 100755 index 58f03534..00000000 --- a/make_release.sh +++ /dev/null @@ -1,352 +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 - - logInfo "Creating AppImage..." - ${SRC_DIR}/AppImage-Recipe.sh "$APP_NAME" "$RELEASE_NAME" - else - 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"" - - logInfo "Build finished, Docker container terminated." - fi - - 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 00000000..8e3285b1 --- /dev/null +++ b/release-tool @@ -0,0 +1,596 @@ +#!/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'" + echo + cmd="COMMAND" + fi + + echo -e "\e[1mUsage:\e[0m $(basename $0) $cmd [options]" + + if [ "COMMAND" == "$cmd" ]; then + cat << EOF + +Commands: + merge Merge release development branch into main branch + and create release tags + build Build and package binary release from sources + sign Sign compile release packages + help Show help for the given command +EOF + elif [ "merge" == "$cmd" ]; then + 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}') + -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 + +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 + +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 +} + + +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}" + + checkGitRepository + checkReleaseDoesNotExist + checkWorkingTreeClean + checkSourceBranchExists + checkTargetBranchExists + checkVersionInCMake + checkChangeLog + + logInfo "All checks pass, getting our hands dirty now!" + + 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 "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" + 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 + + if [ "$(uname -i)" == "Msys" ]; then + logInfo "Bundling binary packages..." + make package + else + 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 + 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-Recipe2.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 "$@" +fi From 96ca7a8cbcd55e4e9342e267a279ab47828dd584 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sat, 28 Jan 2017 22:19:23 +0100 Subject: [PATCH 27/49] Fix CMake options on Windows --- release-tool | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/release-tool b/release-tool index 8e3285b1..7c2a432c 100755 --- a/release-tool +++ b/release-tool @@ -64,10 +64,9 @@ printUsage() { cat << EOF Commands: - merge Merge release development branch into main branch - and create release tags + merge Merge release branch into main branch and create release tags build Build and package binary release from sources - sign Sign compile release packages + sign Sign previously compiled release packages help Show help for the given command EOF elif [ "merge" == "$cmd" ]; then @@ -477,16 +476,25 @@ build() { 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 if [ "$(uname -i)" == "Msys" ]; then + 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 sources..." + make $MAKE_OPTIONS + logInfo "Bundling binary packages..." make package else + 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 From b7180893c6a1a36be71ff772ed44f49ed5221db0 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sat, 28 Jan 2017 22:23:51 +0100 Subject: [PATCH 28/49] Fix check for Msys --- release-tool | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/release-tool b/release-tool index 7c2a432c..ef842a66 100755 --- a/release-tool +++ b/release-tool @@ -476,8 +476,7 @@ build() { export CXX="$COMPILER" if [ "" == "$DOCKER_IMAGE" ]; then - - if [ "$(uname -i)" == "Msys" ]; then + if [ "$(uname -o)" == "Msys" ]; then logInfo "Configuring build..." cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off -G"MSYS Makefiles" \ -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" $CMAKE_OPTIONS "$SRC_DIR" From c043be3aa44e22126a1c88d19da2ed8e008b4363 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sat, 28 Jan 2017 22:45:07 +0100 Subject: [PATCH 29/49] Copy windows binaries to release-bin directory --- release-tool | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/release-tool b/release-tool index ef842a66..282b4db0 100755 --- a/release-tool +++ b/release-tool @@ -481,12 +481,11 @@ build() { cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off -G"MSYS Makefiles" \ -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" $CMAKE_OPTIONS "$SRC_DIR" - logInfo "Compiling sources..." - make $MAKE_OPTIONS + logInfo "Compiling and packaging sources..." + make $MAKE_OPTIONS package - logInfo "Bundling binary packages..." - make package - else + mv "./${APP_NAME}-${RELEASE_NAME}-"*.{exe,zip} ../bin-release + else logInfo "Configuring build..." cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off $CMAKE_OPTIONS \ -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" "$SRC_DIR" @@ -497,7 +496,6 @@ build() { 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 From a63ba6bc4fdd93e010ded82664a19e5ba16d2ac9 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sat, 28 Jan 2017 23:02:26 +0100 Subject: [PATCH 30/49] Update translations before merging branches --- release-tool | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/release-tool b/release-tool index 282b4db0..2e3dad3d 100755 --- a/release-tool +++ b/release-tool @@ -232,6 +232,13 @@ checkChangeLog() { 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 +} + trap exitTrap SIGINT SIGTERM @@ -308,6 +315,7 @@ merge() { logInfo "Changing to source directory..." cd "${SRC_DIR}" + checkTransifexCommandExists checkGitRepository checkReleaseDoesNotExist checkWorkingTreeClean @@ -317,6 +325,25 @@ merge() { 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" @@ -440,6 +467,8 @@ build() { checkWorkingTreeClean checkOutputDirDoesNotExist + logInfo "All checks pass, getting our hands dirty now!" + logInfo "Checking out release tag '${TAG_NAME}'..." git checkout "$TAG_NAME" From 05aefc6489aef67077464c2554ac4cad65961395 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sat, 28 Jan 2017 23:17:48 +0100 Subject: [PATCH 31/49] Show error when invalid command specified --- release-tool | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/release-tool b/release-tool index 2e3dad3d..0fa60c43 100755 --- a/release-tool +++ b/release-tool @@ -53,8 +53,7 @@ printUsage() { elif [ "merge" == "$1" ] || [ "build" == "$1" ] || [ "sign" == "$1" ]; then cmd="$1" else - logError "Unknown command: '$1'" - echo + logError "Unknown command: '$1'\n" cmd="COMMAND" fi @@ -627,4 +626,6 @@ elif [ "help" == "$MODE" ]; then exit elif [ "merge" == "$MODE" ] || [ "build" == "$MODE" ] || [ "sign" == "$MODE" ]; then $MODE "$@" +else + printUsage "$MODE" fi From 00cd0e1ae3f6005551bd716413d03b709e948db5 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sat, 28 Jan 2017 23:24:12 +0100 Subject: [PATCH 32/49] Use correct AppImage-Recipe.sh --- release-tool | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-tool b/release-tool index 0fa60c43..6090077a 100755 --- a/release-tool +++ b/release-tool @@ -540,7 +540,7 @@ build() { 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-Recipe2.sh "$APP_NAME" "$RELEASE_NAME"" + /keepassxc/src/AppImage-Recipe.sh "$APP_NAME" "$RELEASE_NAME"" if [ 0 -ne $? ]; then exitError "Docker build failed!" From bd2edea1c9ffd158824940465c4b2fe36059c839 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sat, 28 Jan 2017 23:40:57 +0100 Subject: [PATCH 33/49] Ignore .github folder in exports --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 9f713b46..9d1ecabf 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 From e94dc226b5ded11611e554da672f8cb56a531e0c Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sun, 29 Jan 2017 02:23:55 +0100 Subject: [PATCH 34/49] Downgrade to Ubuntu 14.04 --- Dockerfile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 44400993..422e4da8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,14 +14,16 @@ # 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 \ + g++ \ cmake \ libgcrypt20-dev \ qtbase5-dev \ + qttools5-dev \ qttools5-dev-tools \ libmicrohttpd-dev \ libqt5x11extras5-dev \ @@ -32,7 +34,7 @@ RUN set -x \ file \ fuse \ python - + VOLUME /keepassxc/src VOLUME /keepassxc/out WORKDIR /keepassxc From e326e2c6b3524f19f357c1f6a3d1612eb2aaa052 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sun, 29 Jan 2017 02:24:12 +0100 Subject: [PATCH 35/49] Bundle GTK2 platform style --- AppImage-Recipe.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/AppImage-Recipe.sh b/AppImage-Recipe.sh index d9cbd565..1c31e4bd 100755 --- a/AppImage-Recipe.sh +++ b/AppImage-Recipe.sh @@ -46,11 +46,12 @@ rm -R ./usr/local patch_strings_in_file /usr/local ././ patch_strings_in_file /usr ./ -# bundle Qt platform plugins +# bundle Qt platform plugins and themes QXCB_PLUGIN="$(find /usr/lib -name 'libqxcb.so' 2> /dev/null)" QT_PLUGIN_PATH="$(dirname $(dirname $QXCB_PLUGIN))" mkdir -p "./${QT_PLUGIN_PATH}/platforms" cp "$QXCB_PLUGIN" "./${QT_PLUGIN_PATH}/platforms/" +cp -a "${QT_PLUGIN_PATH}/platformthemes" "./${QT_PLUGIN_PATH}" get_apprun copy_deps @@ -62,6 +63,13 @@ find . -name libsystemd.so.0 -exec rm {} \; get_desktop get_icon +cat << EOF > ./usr/bin/keepassxc_env +#!/usr/bin/bash +export QT_QPA_PLATFORMTHEME=gtk2 +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) From 08d68300bd92ca8a579ee80f120aae8d2733f2f9 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sun, 29 Jan 2017 02:46:02 +0100 Subject: [PATCH 36/49] Disable GTK2 style again for now, since it seems to cause trouble with the tray icon --- AppImage-Recipe.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppImage-Recipe.sh b/AppImage-Recipe.sh index 1c31e4bd..9b463902 100755 --- a/AppImage-Recipe.sh +++ b/AppImage-Recipe.sh @@ -65,7 +65,7 @@ get_desktop get_icon cat << EOF > ./usr/bin/keepassxc_env #!/usr/bin/bash -export QT_QPA_PLATFORMTHEME=gtk2 +#export QT_QPA_PLATFORMTHEME=gtk2 exec keepassxc "$@" EOF chmod +x ./usr/bin/keepassxc_env From 9fe4504623f85b86f6c6db053d335b7498b6f088 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sun, 29 Jan 2017 02:50:44 +0100 Subject: [PATCH 37/49] Fix heredoc --- AppImage-Recipe.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppImage-Recipe.sh b/AppImage-Recipe.sh index 9b463902..a76bf92e 100755 --- a/AppImage-Recipe.sh +++ b/AppImage-Recipe.sh @@ -66,7 +66,7 @@ get_icon cat << EOF > ./usr/bin/keepassxc_env #!/usr/bin/bash #export QT_QPA_PLATFORMTHEME=gtk2 -exec keepassxc "$@" +exec keepassxc "\$@" EOF chmod +x ./usr/bin/keepassxc_env sed -i 's/Exec=keepassxc/Exec=keepassxc_env/' keepassxc.desktop From 34fa4561062766f7b0eab5f1cd838b5956902119 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sun, 29 Jan 2017 12:52:05 +0100 Subject: [PATCH 38/49] Add summary line to individual help pages --- release-tool | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/release-tool b/release-tool index 6090077a..d1d4f69e 100755 --- a/release-tool +++ b/release-tool @@ -71,6 +71,8 @@ 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}') @@ -86,6 +88,8 @@ 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}') @@ -111,6 +115,8 @@ 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}') From dda9a951631cc4871772a0a8aae717f424380a0f Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sun, 29 Jan 2017 12:53:52 +0100 Subject: [PATCH 39/49] Move packages to main release folder instead of bin-release on Windows --- release-tool | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/release-tool b/release-tool index d1d4f69e..173079f1 100755 --- a/release-tool +++ b/release-tool @@ -518,7 +518,8 @@ build() { logInfo "Compiling and packaging sources..." make $MAKE_OPTIONS package - mv "./${APP_NAME}-${RELEASE_NAME}-"*.{exe,zip} ../bin-release + rmdir ../bin-release + mv "./${APP_NAME}-${RELEASE_NAME}-"*.{exe,zip} ../ else logInfo "Configuring build..." cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off $CMAKE_OPTIONS \ From 0c54276fe207655bee3ba2962f69fec5f405819c Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sun, 29 Jan 2017 20:46:57 +0100 Subject: [PATCH 40/49] Support building on OS X (untested) --- release-tool | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/release-tool b/release-tool index 173079f1..0784a445 100755 --- a/release-tool +++ b/release-tool @@ -510,7 +510,26 @@ build() { export CXX="$COMPILER" if [ "" == "$DOCKER_IMAGE" ]; then - if [ "$(uname -o)" == "Msys" ]; 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" @@ -520,7 +539,8 @@ build() { rmdir ../bin-release mv "./${APP_NAME}-${RELEASE_NAME}-"*.{exe,zip} ../ - else + else + # 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" From 1da87d1d19d0abc16701a8c1a41267b23a22c9ef Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sun, 29 Jan 2017 20:58:09 +0100 Subject: [PATCH 41/49] Only create bin-release dir on Linux --- release-tool | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/release-tool b/release-tool index 0784a445..de5c723c 100755 --- a/release-tool +++ b/release-tool @@ -494,7 +494,6 @@ build() { logInfo "Creating build directory..." mkdir -p "${OUTPUT_DIR}/build-release" - mkdir -p "${OUTPUT_DIR}/bin-release" cd "${OUTPUT_DIR}/build-release" logInfo "Configuring sources..." @@ -537,9 +536,10 @@ build() { logInfo "Compiling and packaging sources..." make $MAKE_OPTIONS package - rmdir ../bin-release 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 \ @@ -555,6 +555,8 @@ build() { ${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 \ From 2b18089641338f2991d0e011ba30fbd7a2316dfa Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sun, 29 Jan 2017 21:40:47 +0100 Subject: [PATCH 42/49] Patch desktop integration script so that it finds the keepassxc.png icons --- AppImage-Recipe.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AppImage-Recipe.sh b/AppImage-Recipe.sh index a76bf92e..58d9ba28 100755 --- a/AppImage-Recipe.sh +++ b/AppImage-Recipe.sh @@ -72,6 +72,10 @@ chmod +x ./usr/bin/keepassxc_env sed -i 's/Exec=keepassxc/Exec=keepassxc_env/' keepassxc.desktop get_desktopintegration $LOWERAPP +# patch desktop integration script so it finds the keepassxc.png icons +# see https://github.com/probonopd/AppImageKit/issues/341 +sed -i 's/-wholename "\*\/apps\/\${APP}.png"/-wholename "*\/apps\/keepassxc.png"/' ./usr/bin/keepassxc_env.wrapper + GLIBC_NEEDED=$(glibc_needed) cd .. From 9a92d200011b64d524d7d9c87e1468bb12335262 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sun, 29 Jan 2017 22:19:44 +0100 Subject: [PATCH 43/49] Revert icon patch, because upstream patched fast See https://github.com/probonopd/AppImageKit/commit/69bf4718bec50b0f83a132aab0ec48bbc5ad9472 --- AppImage-Recipe.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/AppImage-Recipe.sh b/AppImage-Recipe.sh index 58d9ba28..a76bf92e 100755 --- a/AppImage-Recipe.sh +++ b/AppImage-Recipe.sh @@ -72,10 +72,6 @@ chmod +x ./usr/bin/keepassxc_env sed -i 's/Exec=keepassxc/Exec=keepassxc_env/' keepassxc.desktop get_desktopintegration $LOWERAPP -# patch desktop integration script so it finds the keepassxc.png icons -# see https://github.com/probonopd/AppImageKit/issues/341 -sed -i 's/-wholename "\*\/apps\/\${APP}.png"/-wholename "*\/apps\/keepassxc.png"/' ./usr/bin/keepassxc_env.wrapper - GLIBC_NEEDED=$(glibc_needed) cd .. From 80fc8d4da98404061e46a6df5d81e37a9e9702d1 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Mon, 30 Jan 2017 18:36:11 +0100 Subject: [PATCH 44/49] Replace /bin/bash with /usr/bin/env bash --- AppImage-Recipe.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppImage-Recipe.sh b/AppImage-Recipe.sh index a76bf92e..2ed3ae93 100755 --- a/AppImage-Recipe.sh +++ b/AppImage-Recipe.sh @@ -64,7 +64,7 @@ find . -name libsystemd.so.0 -exec rm {} \; get_desktop get_icon cat << EOF > ./usr/bin/keepassxc_env -#!/usr/bin/bash +#!/usr/bin/env bash #export QT_QPA_PLATFORMTHEME=gtk2 exec keepassxc "\$@" EOF From d7633f40baeca6ba288a62005c12ddbcc1f87923 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Mon, 30 Jan 2017 21:28:59 +0100 Subject: [PATCH 45/49] Add Windows installer branding bitmaps --- share/windows/installer-header.bmp | Bin 0 -> 25820 bytes share/windows/installer-wizard.bmp | Bin 0 -> 154544 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 share/windows/installer-header.bmp create mode 100644 share/windows/installer-wizard.bmp diff --git a/share/windows/installer-header.bmp b/share/windows/installer-header.bmp new file mode 100644 index 0000000000000000000000000000000000000000..f9e17cae78804af8a9e648ee9d0d9db129eb56e1 GIT binary patch literal 25820 zcmd6vd3Y36y2iPG-d)x}5}=cGlFr(l-j^hubQVAX(NP?Mdj*6m;DCTcAPFQ8AR!A& z*hEHUxR3zi=qQZNqftk>;|wScZZo(rjyfQVfT9Rl@B4kHs=CriH$}Q#@_f{Ds=B)B z)cMu>EvKsSjr(XuKmN00X~4g&_~*jEf9f|B%YOZu@cDp$75{hx7VfRLX))K93CZ>O z4Xgu|7?HZOZeRw#1eId3R8hlfl=e4!Ab+&ilqdxpyy?V8)qqDQCbIa?$vTh!0 z-#kuBm-@VqrSayx?RR~C;gqPTuW72UX`0{mV0EewOJPm-U^Raa>>joGy07A+o?F!W2eiAo+*>DTG1rz` zlk4*ue*dtF;s2$tFTi#~L;jTJx#2LxC=$l5E?``Auu5N-qoqrI-p97-7U$;i@0|Ke zFovzItzUk3XMaLcV)%(Z!Za-Fvu!w(Lpuk&HMp+0X)(=Wndc=g(~j<$A;=z-&h&>3sv z&04zD=Y7Bsr+Dq-KZRg;>FU?zwUy}(W4hN2hEc9~C{SQ5Yz8Z)M-54J1Wg9WYGj7A zaVkM0ZiI_xP5NR8o?TrKk`}{_n^fnmSLv|jgTra-eAu2`@0}b5LnyGF0q#Ba z3~m$ShGNgw30k_;=Y7D?v|-et_gjK7gkSjT>gAfP^N{WY#f`kHb_~T* z%=z}gU}zp8XQY&uLX(uE$|f<~BLv9_jUqxyG6@<}r7$QAzLa^2oaXvLuZJV`eX+EaKmzoq`$~qLjlobdy%JGl|NePVzhGNwp z4>=49)iEwfLJ~UJm-d+?7;d~pi#ayjoLKGY&hXWgwYk`yRO6Yvd$x9lsJ=S8I-1{n zDrv2U05*=((t!x>V_iQwqb~o+;|D|QFd(O)NGO$vf}y{>kQna4b~%+HzpAv+V2?^p zwR=V?1MJ}{dzW0VyG4Xy^^Gya$Q}Ru-K1(a2GRvDpzS7#;BH`OtS>nD?h7Fp%C&__ zsV&HtvO5*=kh%FoqM%0`K~Gc?Bum0b)gaY}A^?g8lLSCv;Z+!5xZ!3k=2$-_agDp~ zr7hyRv1~c_D2`pKh-Yh3pAW3-$);`>+{iY@Dk4{NLOGrSg2 zdX?FAH+FTw8EP?%x4s8M zLro60gYBdnWcc>y?_(gd8j7@>I^z478wzpjo%rITU^*0YZ4WGakmlNQv&38*IZ~dU zA&i2d91ocxX_De0XQx7e5>gd3#uzwH6Fd@X!V8A$$7nJ8y3zX8&bpU2t96)Moo%D- zu1ULQX^)3E!iQ&0A`BYqd|FN&@qKXKyiF5MT|6C(p_ucYKL3BjoHyoac+Q&>8c`q0 z^aM8{LTs%{LQd#`0sc}jeht$%6K&D-w= z!>)Esc_~KjvkRvRx89LntGB!Iy=sMbN-I$cV{}Y5qh!XG~PbVd2W>V;1&YyAC=*48#mdj_lM)!Q+0=e{~OcKf8%>TE5ij`%(aRra}0 zmUW^&?dVc>Olh}wcKr484@ccOHY?j2=S~@r5tA^{LaB`8PPxpdRdr5784L@Or6>nR zcBlTNl+aNaqzFkgq>y+6S!nMJM>*DA-|P*=`YOA^5SJ^a$pA$BDFblb2A(iozH<4F z$EGE(c4;|vWcP8d8y&qYy|!gDW*Fc*diECc53L<-hmZf!Gb-2OwU2Nn4M>kpndxC2 zGB-g|iHaNsrC>;H%0;`GoXsE@f`C}|MA7Fh&|$+UEoQGT)~~eJy(sBWTsmUE^mnbb z=U@ceg1aAsH)Yo>+!R_)9ocJKFHfzH>&?{J2VsmQzP|pNtx>vDWv8;DrVHjE5GOER9Ro+X`JnY`U8aLv2;# zL{;H`av11HIVt7GPn3KLL8>xRsuEd{H2jh~Rf3_?2<24e5E{4-7}ghSFn?8b>y zSV%R#YQmeNV)xp@50GJUamWQBGn5EGpUQ(_wW#{g2E)2yEoQ4NN?2j5eIWu2F><&O zN}j5a7;5W)mQHHfC!x|hdv6IGL1$ap1ui+u2~?MMn8dJ1qeYjcEk9Tj zz77S$g-@18Ej2g@pwl7TR!gXVv86 z(&H=5%}1Y%6vN#|o`|osM3<)H;5bF-K1AwMoXfDvb?lS3gI&dU_(MX|axg2$YRz}X z*b@e1M5oQpVFjWTWa~z6x(Iy|LrH}G?35iQb7T3*Au(c(6g>O&v(*-cFz7_T!Zvcb z@&0`!Z+?0lmyS7%p11FfZlOt?uFkhUdwcf2MY@Wt#1(eN7^){BI;s7m_~qu?cie*{ z8ZUqZ>oaD%>$b1g8x58MClZ>0nKAZN<7+#}~HR2RPX z)KUb)wS~Q!y|&0)ogY({QLu5s)5o8~kOLt654=sVtJl7rYOrgqI3Nyt=-n558*hs( zOULPO(q20gp^XJ2mKj$+-#{V^auN9G+_x8IJ~%tomWemo23toCap=5{OcuVBre}l{ zyU%&6OMAZ{g(i8)Lr%d^U=$6B1II}=ExA)UeWBI8ozE505O%NL}GX2Liej{;IvEAg9Ls|9UZ}{3iRaL)+z;Y^t!qo%-6VT&&}K70ImTr1vs`JUvb9y!s* ziNs4b&;0qh_+>^zg&mzyIu>@rms08Q)Q$S|+-JD1o@d7v+()P?J~(|ce|(WS$FBFJ z<8`)}{G@{2Q~eC7IHZtBPF0qo)PM-dmoh^@liEyA(jmblBfm-{8E;th1bHxAlP}C( z?aNr{idkYf@WwN^(9X8bA3%ZX;l$~1Qk?q08^4KOVnAomC8c}eG;o|DixO8JT8o1R zd$@wLf75zp-*ZnU7?a$^xzUdJo06kaW_So78|R{;N~FA0db^DRBfSkJ4Ctt#0!Y;$ z8;!Tnq+fO6D$vi+Yg^;Bukl(}=Z;*Oy0K*&d>W=Se=r4h!+4F}+eZf)Tegl^nu0Fb z*W{slVOshMd&+X_JDqz!Y{Oehk03_}xw6fkMY zzSIwpqap=HwL2x@aau!aluLUtRTGG`kD4SMGQ;Y;=*3C1cF(6tsLmf+f!#1djr=!z z&w`l6$zjsN*X`ALF^iKH>|csoBbW$L*;-Ono-5v!GB7hX#%J(5QokDM zR~MX@%I=hNR4J#~Lys;I>%JyjxNzZ{U*I{ z87yb(Inv+j7BK_x^s$4{4<({|=w#o_wmNrMNn+_!l@MXD87AB^c=_V*7ydE!?g{2> zTbefu4=Rw9=5Cu*)O>Ft8HYq^zLX;#J6OewlJis-79t)BDY6=+)RfV&K1F`hN9P$J zVEEG2xx*KyOy4;h4(x|aL_^;KB27h?rae9j-9sn)z!2X`US@LCc6oSQgfGTve{=3DVo!vSg0_SLtdj36C0!MK>9dO{K;H&~D| zMxjYA4ynTMM@0^ggpmP~2w9&IWf=2N;{8nvL&W>Umt#yfra?48r^PP1nk^K1|y%0hGLXgLhZ!B7r~ zjFIzFPDkaCNW{1-l*@de6C~xR$O=>d%I;Lm+4yH}`Vt{^x{2GSw6@VBgkTr?<8G&p zwYA={byED2R4XxbYcP*_r8{b2T*L3^t%F{l^(q&INZ54fiTKPUPqBBTD;ZCkVtmO` zNg+gtC1*3xB$X8GQB@+8)W~5FB-OvSkk!bVWXVB|oXrG43t06}0%$_2(`{~f7#A8W zTqxK2V>Qq*bZ<+`!-F1(wXAe&FHu|;$2|;MlH$_ z_n(PF%;jTPgrq>uOF0G#9THE%$TMzIJQPU`2@>h36zgP1Doc?{3L*n>ir>$WAR`qL zb-JS;)3cbLR5qi&Uqohep8D!Wt4W<=-{ zY!!8hjml~t8#>*0SHHs>&jHu>lUhah(8;kIZU-Fm3RegN-jp)a@Dly+-hL4PLwHe$ z6B9FUef*ARv={Hqq~&HogqU2jtr!dKbt?2XORU`p93Ns%BH>n9>6k*cs9d76x%-9PrZB%ln`IwJt-SUF=P8r($9Tjx>AYL?J@I6p0%JM#-0w zk7ZxlEh%NE%Fa_&B?@BGq-v1jA#sz#pd?5Rl2D)u=?x|{vJTY)_S0-bhcCbP``CpE zSrvA3g`=lsEw>Gt7d>a!1E>|Sa|nR=2wwDEynQ&n$mF(|3mlZnBn?TuFGoQq$3r1J zUP^fkxlYK>|9W^TzJfhNOg4cZ&&r&K7md$I%pRQ={O-Hl+Z*H=|Hy2oWkeB$6U2 zh#jz7I2iHuGZ)Vk)sE3GGT`8rWsZ>x5@P4aZEe}E<>2%J zen;D>%V(#}n~A5q-kW_RTuJ!vyYPG>d&8XsknCPy6s5hQLxE9-CK4L)93N5?g7l{~ z43HT5;~`ZQ!hebeAj_#KGmJ=749}NhXffAX^K5O}lFX>N!($(aOL#Cj`Jprz8d!~* z8=JP&SbMM@&tI`~NTr1O5a0d#+0SxT`;to1Ol6kA_YZ?3J$URnY{y^dy91wMP7S}W zU%a|3E+f%9#)o18x#&$vQRcEStUv(}BAg*A2`r1~k(2CES$I$*xXGyuGo+9xV3Ow~ zU&^W@m=yZRi;5WNFAhZ+!P_Mvg)PW`p&CEb=mq}_Up#&B%+I$>x#_+^NhPUSeqw|EGELXhlEWq^Kn zN?X)3ZmJ|EY=(W(A%+d(ho64ky6$KF?;B(+HD?h$ORt8HutWd*2H(E%PVs;w^r-iG z)8KS}{NPXjJpJEB%#UAtuny1nsRKb?^;M356X7Kb{C;A4^P^Cp)0dr@Zyky}CMilc zK2vs~EJE3rvKJ+fDg{GIP489L7OX=8%f6J*2@_F~QW+saX(>xlijY(jLVjEv3XHTG zrrrC1mcdX)MKr{(mP+@P|HsV!rcz5*sl`-k4q;-4f1laEZ2xkcuC2Ep{so*uy(-?V z+41r(9Tn~)A05>a0P%GIZ0~5tf35!5fnEA6gWc=G@0g%KR1(qNMBmPPNQBTH%dePo z8rGgaBo;}N-<;5hoJMl0G)F3XQK3M}NI4#gr-;1DzLXPG_N%fE<;;diMkF3iTZjta zx;M0XM9~I^Kb&|ycJ7Fjl2k&sRD+e84JE0==Z*Z+2caIy2AoK&qI>A%JD3~0L5~2E-q5(0J*wJ^Dms)xqH21E zpb0e+My?Wt2t{qdI`qdw;Z9k5vH~d>@;h#lgAJiWO)52;Zq&2ky(uVR-mV1$XALHS zEzQzs*m=OL!3%ajh_B-yz2XYC5uF4*{DmmtmG6=Q*@bT1vm?QjOaKer(JnoHm5sO6 z-0SWX@?-6Zl7cc)DK(XXA-PahPD3OGF{&!St-B7%ok}r~@=}=mEKz=XAu2xK+paaU%ay?Bmjd_R8xi2q&Yi*L?1>}ZV3 zOoR)?|80R%5DElCSMBY^duM{41dv>))y$pWuiyUz{EgkWf+x zDJK;aL`7*z?}C-yktV&R5WX?ym~-v5pL0$~LK3|11-JXR_FA)yImTLR@2BKB&4>JA z(*O4<{?`ltKM?<4ivKtKpHF@2|9t8TXnpEaXJY)*|Mye&f9t@h|FUB~IsOe8nEY>m z;ggy0F)+~l$pYM|41BWCK88uYgEzNNG~H@Vw_)4a2A9q1{)uXBtG2y)Zad3nrTKr> zF0zvNS%S=o1G12Qu)q+YBlCTYD8j98Z}y zbUL2kKbr#;zUi{aLSF=Q05S+OC9*P$$)n3STk5c9C{@ZHp^~Gs#6-!sxWO5QoFc_3 zEIm7d`;mrBEGD%c&rUPT7-p8#p%O#ns$>lIF`8l)MHOBVN;R670q{cF-jU=Ukn+^d ziQ+^pVPLtk)H0r&7OW2m5+;=}xK{~DN}bCc^)j8cTayz=l0iU97*gcGsURtPCEAjG zf=ZPODj2F5Lck0d*G{Ccxnb1NSLIv$bV+0N7`!}A1QBmhm#Q$AHaM~E*KtO@L~gSV z_C^<{U&+S%4Yq3}7cYcf4zg6WMN7;ign5JQC@}3Rf?nghGFp5CZ1xs8<${Y(;Mi%AF%Sk+2cH5ePL@cF5nK2&iy5SJw)0zL zniX8pS3N$#0&0a#fbp=!Xz=guR$6(~Ny`iZn^!8dD%0@$U>RTy znLB5ZH45b1Oc2w`&n!I}V5n2D?QBCSUE+;Ni<^rPoS*g8Kni`^euv*Uq{Mi1bS(iSK$In z_7fFC`5Al`#umT046~S)B{pQ{s+F)A29^*zL(E>d&`TnUWQQS;aEu1}VO&7<)Y0P` zR6zAn%ZN5FZHR5UC`;DRjI|V&G!RlRLrxcZSu}&;an#SV63JdA$(X)TqXDo`A}z>KzeQ$P9_C_s$!$0$01Se|4NQG=bPN$nn-ASph&6~!6=)0>(W212V) z0fUa{=cOYr2|<^9Vrc#pVTH}lim5DiPP5C2^`ussuRaSE6_udEKtU3=f(n)vB^wW= zp{yKL$5H2sWqMefFbHnG#sD2PsR}mCz-<_fsbNDM}Kdk_~wQ$>VrBE5sZffYGH;$&%?{&!LpU z8n|?237&~~Y48k^A<3{=rv+iB4tiO!4xs2)2?_|DY*-fRA}{tBDnEOC2P5M+vs3Wc zW3Vlw!}IJahZvZGSvaHArk72~G|H08hZzKimF}()TvUQ!Sow@>VXa|VL=lbcFfSvS zVTFx_7=ThaMGq$`Egh!_w2C6Nn8Y~>r!g^D;c6#V1(GhrPg)W=ktj3`U`#FARfWz< zr@}3onK@Jog*XNqoS?yq)Wf`a`33*9)M1W-<5TlE}@)C%HjV zb}}iwtU5L=H20B14z}6#a%aF)wn8QA4iFU>h+l zxMQ%q790kaUEm8Flg!GudaXtS=HQe1tduNqHxP9?8&T>f8zhrBWtQQWuc4)x+1ME_ zb}2H|w)qgF3YgWE+shJ3Ek4CED&QMg6yLC!1O}V6h-GFQ}U;<2FI*e)?Vvi*?R;^Ma+0ZhSH(0NGneG-` z>5L!8!_;I(Fv61Mm;py!K1O3qqbuswXy9QegJ)-k4O9kW@>h@f=B<81^{K*6v}CS`mgr?C|h^?KeIBH-dMN4gXgaWsOcoam96G2q;a4P|6f zkZf#QG{xjuF43^;qE2U}a}1u{bLGVrj5%Q1n6Dxm01M83Bn^v3j*=!KyL?tu23wr7ST7j> zh)r|Ys!$wprP*S}3}{0nDsiQ{QWcs=3hoGtTrAT?SEUSSsSOp8@CdP74svV!6R->n zu;DlI6;n3m9xlcb+@nB?EWN_fK&F1h%q~qSBz1o1#kTdhG202VwQ~|gg&CL^Eu&;| zo^n;RZHAC8dFj$r%Y+LREJy)WAc@MDF?ius4#@bb$XQ}_;V$;12s&lyfK9k078+(y zA~P})25Ct!SgFCV)J}*+u(k)0JMl8&5r3{G`PW!}svatqvDD_w( z1LLH*0Wky;V~<`s0+qooNAtX?6A`kE<!@EBkjGaxl`MJh5zU6u~Mjz`8tG$r!00(!2*jHMtdrDJJUvh~o| zY6*BMHs+FS%)ohI+mCQzGM>H7t}e1`MW4 zUI~vdxTU#!Ps)f+JBWvefp=O`U4~;6+%+qg*aMgV3=!xDZkaQ6S7LnuaW(4@qAO z;sA0~vJ88R>_w%dg_oJZX2IPn|MtSeAH;uXpLgrRNN0cphD}}X5$wvarZR`Z1*Q2V zJIh4@fb7{QNGROf*d#NO4Szvh#icMHWAqn=Cv4WR5)ih)7CoBSQHups)kSp*l>$Z; zMakBKOOGjoFw2vA%p!~9F-IrIva{b|AP()L#tJYo(-^^AzIM&o5P9VTzcMd&LVENp zxNBu0Qz@n};A|-h*vcD{8D>T>g930`eVVi$*?bks)K6g8pp7MU1M88ag2={3JzZpX zcR8vv8*)^YJy~7J(P8UxqO!&`os^DYQ%tq9I8jkiMNEvSv9)%Hmf}QY$&Is@tTk0Z zNEp+7`Q$NcPkiaEe@R(f!Vx`huYc#p$8I-+tnkR_wJ19~VPj@8P+c~fU@(;O>Rc#Z z*?;xlug6T|#v+%@am<<%)BdB)2RJcHr(N~UtN%eS{dd>X55N-c7)thDV<0@$W;iTe z9nA2%IKw&Tkx|F8$$*g}Jo(BqSOu~NMKDIv+!!%olQ1YCmQqo;g#|YE6`0CJ%It`P z1C91d#?X1UEJU1q2Szy{1nD;8HT|k@!bTuWLzn5i ze2R~1$m&((HqTPcOUPU>ZIBu;oW13fZ+!H2TuL3BQ~#5%8d1w8qer7iq|NHQTNeo) z`cX0ts3y!P8JDQE8GsF>To}Qs%O^vjwGF{Q3j{$FAE`x}qsj^?`5H^puly!vIFX2) zWs_OLtJE6Vi;|a3#sW+wm$bI{u61~wdd*J+CMODQ0K;S`Em-EA0_MR$T4jh2RGJ2b z)={rR72wwAWyWDvH7FY3zD_TCigis(^=r0FQFW*nP+~M4?{Pl`1l{1eO@!P>|+kn0wl1 z&=0^o2is~yY3}QtX0v?qfHV^{0d#57iz1s{PEHZriiH8tf&u*p@QMZwF(lsN21Ha? zo{EV{I|G~5vdK^czUsHaryz*dI4)rf%w#jF^>LFPj`%lQ-lAfC{37q;)4&oF)}%4@JnTWKCML zN21u-N24c<#Q-X`2G72t6Av*Nw75`s`VW}FPs;1mmDjaYfD2THh-O1pu|UuX>JSzJ z2?hc}S-}3m4RbL1{Tt?>d`^#LQ?%G&CPi>57p=-n>+GIu$OT(-3C18QY0XNrGObJ&-^nWC2vW1&}&l*IX20(wEX{*Zc&&fB~CAoe(9owxy(`IG2fn4T|NQwDpec zL=svfc*TDrlXDOE6pQAynCQit4GbI&v!ZZ;5eYF0Gg0L0dKvd0WFb6Tl8l4$%@-O0 z*n+!Pfe>I$_sRr7(xg2ZQfdg%P_mGqjrfT3{Tt^1r*=T zDuTIdB~n-x-u}?z>$)fwGtl=0m;p18YbGl-pD32ay0)YdkP}8W$b$BHw=A-Zt4N5^ zh-knps=2Jh?eZxojh6OcsoF&#BBORzv24optG)#r%_2H3!>-OAQ6ULnmU5LEMRkQb zDZ5z;41{wD(249?AlRT_j7ua1T}Y~R=TNOWF+XO_Nw5SMX$=Ik) z>IfcyP)JY?qNy{~AyHoP+7pouseu8bwl2sLk@M9P!4zK0eQ)QCvNncnUGp9d7x0D% zaDgv|I60xnGR{N5KtC?gfF*YT)xw*RVYXa0bMn2K%7Q@I@JwTqTV9}oM`&%YxoiiU zqD_xdahQ7-&^8bpdtK8VF&m z0+DenpIWIk?hXkI;KiJzBMOv#I&)qb`4x2Ofe;9h0$v!E3m_S6CK4F1&4y`#A#1B@ z*Ii}OKIBH**L8Y*&2`P2l+DseYggi50Y5({2&sWMrPs33SzWcv~kVzFSl zqI|4eQ#Rx#BpC33SIib<;bma+evF!LX-K5SSyh;UinS_?kff|ean8(wwA2|c>3)rM zO&6REgYW;)8tcxzj7*ULJX)tX8@3R|Q5Cbx5)p~QdX1K=a5Q3hX(^O0a}-^y-Qej` zlOdDUQvg4+kLk?v<<(7Vu}<&v#v;?GUM#< zU{6Ba%i7v=5(%0Nc1hCWQZzbRE6vLc?L<*Z#}p)lXH8n%xC<9cs%JCIUTo$C!@mE} zQJb@AP0Z$wGL0X7hK16_~(Vy^8NrmO^E0$UQl76SFK< zGE=C0&u+j1i47S8KC9988R&oo(jrKLRHo#P2t`I2&|P^WyZ1XDtKH^vu~CR382}+2 zDFGTrxKqk$p9hg8&nhIecFgJ0>tZwwSy)ZH)MJq?PPlU@YC$8z-NDm!hTU^ytkkj>F3Hy$u~MYI;(H2HHf2u@+8AInR{WUuIkspU0OAiM3AU9`m>Ec~ zF&T?JtJ-Q&X$4N>-vEoEQcTgFT#}axY_s1wooBVJaHpL;KB?G)LoriiG*VJ)mkcKDp%TGgP6k=d$?lGNB70 ztVejtbt#Cvq&v?hE;>_n!lNF6FG{wG1ioA{=SK86fY=qCQpHc_0FVfwnXuWJHL4Nk z>@pTeWOQS@baHIBWKnU@y`pVBU@w6e7+A8FurEg4-E?<&&am5XE=3h2L<>hYHqD|I zH;XKeEsfIG0kDy%Z~#tqF>lpRcgv+X3SzzmbqZ@BJq3&lch}czw;=(bjV#rfyx3om zV4-|g6Sj~$50(gyq9~Bi%sM@OB=XL<Q!Y+&5 zk9({v6M(pM1+!X|5&fyr+SgH~>|)G$a6&}GAvy{Rd5zPY{=2B|)gc*_N?aZE(#;7=9SoLTqU?>-}Btq0yWIOA= z=};qx!WL(@EGAwNS-IpGd6m)KvzY962AsfB-rQYFCi1et7)=e&5;h8+c=O95Z%DRdT;R`h!CrB!9I07Y zDzZd`C4eP_xSVm-w*Uigexfrzp&$=^T@2BeilNLVhS_-jmuQ0hg_9@}8Xh%y-Iw z7Ec7y5{&@kFj}UdKW8HqrG|Z!Q=~?jn>8klf|@glst-=hKD9}DMAqISAtr^nVEaGq`q|w$Og|>n>Es%s6@9%Af5Cq34kO;N{;w*Pn zZI_t;6_a18<(=M!SG!sVr8y)_LS;-#CY9W}k(U3K7~99$#M%D3Ruz73ZgHXYbpmBK z+C4}P+ce_mo~=Qq9_TA3)5le5C~xTFdsyc40kQ`FzNm4TW*Xcv8)cxgK77x^w%1lV z&|*ifAbTuYxl=1N)@e98X}Q)yLW6M0cNkO$)nrpqNZOsClvWweDN%(nn9&f7MQm=| zrq(G`>jw6jL{!CSlEbuSQCg3my z&xa5JiP98B5Lo7%b7_YoMqQxjDfGfnbmmA(2JNhC+2=F@m9`YPgN;I7CrMRE4EmOf*!UGpqyrsB3 zm9<8kh}pusbMi%9IpK!WEz`!H)-^`Qu01(Xga&j}X=!30a&C`Nz)R^c)C?r3C^(mU z+vgS`DUi2qCfXG+BuJK(T2pQY%x7jyKTnqcqrf01Fo;Isw~QrYW)I68PinWceZNj< z+iT=YERmTSt+eKD17LaWFTM3jWpK>uTNfq#ipI51epm&z&0JPJ%;XaQE405J7S?RC!3g-fu&mj_rY21ZnwOKnw{Eb~^0aAflX}b;@TP7?>ttMBr?CgR3RYvev}BK3QV=vi zI5bUruTjlt`Aq?4SO4RN?A9p`U4?y_CMTU#qVTQb| zjjcd|oLV;KY}^v95kGD#S@RO{5Tvr=GS>g4WOV6XBmX~OAY zjip3L0+|}c++&c0nXe0yjo64$q??)^Yq8@2Po=SkvS=-MmHLW8`NC=AeoafUQ9&um zj13Ai1gnO{hV*d3uB+fy2x&~|;43zQilVv&_wbwN_%A(j`%*IG)@GUJ7}G2(M806v z*)EpY)D#&uXqz-dWzn+fYRlY#anzMn#zNU7w^J&OXiv+SDsFExc{5sb>x-fQ$s#Z> zqUByDHxohDwEKZ)-sOR(ZDY=Xw93r*Buoq(`MPFg!^}BsCFd^zc+0aHn5Gj@>28js z2?>a`i9|Ki9*f3|U)jriyEWsI>Kj@ZOD~-9c)-+nGwpXj#e_k|v6th~#TJ^RH(GMc z^5q7O$MO(bL2|8~G=?&{nI#&Uu9labApwdCxm`+y)tR~we@sG$C*2jiq#BKvPi?KE zr6{%z1P4)R)g)%}hSfrP!OWH!Q)twc^Cm*jZ{={t;sD_(k){*OQcvp!X!j*nzM)iD zq8w9y>NtBZrD8N);@&M7Dov9uZDlY^v=qByh?tjVamWuhijv&AjfZ;0v2 zEIA?6I#FQ_WfkLod=T+lYGV4}6S{O6V74Jv6h)@}qNCE0(i8yGa z+Xu>l&A7EE!v+np2QW0F+yF@(bFQ$6$f^r1ib5cx%e<&5BV;7lWp3$h;|l3~8W5^{ zEemqxRg(=96p`~v!G`B@-ZWtxfYp-H6%FGAmzJ82HcygvWYtHN7gfVJUsJ#!HduwQ zwUll#Rz>QN*ESlZMrHS<}N5tt7d?N+Y` zH}IM;(PL;uwUGnxQquxDT~?8$HWf?=YC)W2yFzPrTEE=$7ev?wW7OOM##NAuv^HsM z8mL~_D3lsZogfj0dC?~Hwd%qcgs1+}n`T(BjH8mv6H*U>%nsj{srk~*ZyZ*OrWrRk z%p5!inwJ+sY;hz`h~JkSBVvIG0P5xF75MqjQNC#&QtD`+R_c@7Cqqm?m42aeu zn+w~HiY`pb(Om1a6Si)EB1zz>tsDcxAl?uOsG1ETRCbZWHV>N^QBfcyFa+^~6?spp zC4WSOI)$+cX~FxJQIcmanV`JmS~L$kEb9;a0!A3 z#mQ)4e3}Uu5wa~uHViY7*pT6|u&{UIus59MjvzTmE&+W!?ZFL93tqe7<&3{mBpXJC zjb(vLzN5BQ8NSHkE>=QvSC9Y(oK}UfnI#L%FO{@>XGsPnkoU;vC^R~G|9=r}zfN#-FWs#o!??Q_g*91mCu7-PH!@}}x4d3e3HUmbMSv1@+)r)>R)E`JHD@tBc z$1Zi3iG3!(cm##DP8vt{yivp~j|wx+ZV>Q_O9YHV#(rsWV5TF_r5a)8_IbwT$Fe^W z5;jG;G(QaqbICn0cu$NnMYKJ#U3!1=LT9v$Wt^y} z08f@Yas-T_5G!IbosJ@)X(~rUSTGjZjZE_C2(7;$A%Z7IWG^7G@XRqcwswgDI{NhR zT&AN%HUI%>pqtrbAh0xAiBt6=i*q6X0mGq2*a8d;!X|P>mZIOZ%P$3kxg3utjrhi4 z6iC>iz8c|PqS}YT7?+us@oXeSf1c6HL0BJ6Q&cSR45;Q(prV&j1gm+>2$iO;9KGHW z@}lZ&G&+N73>Bl%8MG8bMG1u*Brx@di;zba;?d##MVUo^xRlEbR2Zc}T&932th1Dm zM2cW9o1A4Z${X2RK{exSy_ZBu!2v@{ECGwlP~X7k_JyUSHySODLyd~kG!AS*f;h=( zu^tjwDktoVjKKhV(r9X^SnBc>|so z6#?DYvQ${&vd9wl0+TFLqd*lCsl=6KFN{@PIc8-EWf%GK9Px{2*6HagV#bgg+o*!T zzG%oR&PC*irXI0@wmZhCkb6LbEigr#)R`KQivZ@Hy&r5_=Pc%}5iiRUe#x`x-UA|&0&dODZUsce4P{E0T@pPd(M&XOjYa z&Y}@ZKEqrjaxd9L7SC&!s$D&)U0@iL3MVt}$Ocm|c*GP`oehbhbxH-hA_AkO2<6}@ zk;jkG0*oQ?OG6?^MbA_q7dD>NZ#^W?P$0@AT3dfV?&}yRTt;NrWDhvsGQqM zssdH4;b-{*tQuK6F?fI2%LY7<4Fh0HH7mru+&5GZLRDljExwVKd=XIT!a#T|3-cPu zi4!NhU|znS$Z#^U+m}%(n%SfpX>3Qt@sd-&EHfK7kmB?LByl1YGUnyQ2(kJYHn2rT zNl@f1QhQkHB}WY@odu75Kzb>1@?xiSFH@QbcQOgd!N8a9CUnM36p}?#F3anvWbYB$ z&@`Emv4RI{lcL;G#0BMeopog zUk2uz31JK(KzN%ofNZPtB0LntlcEFLAIJ=|oQ38;sP(TloyrLlhoHHuWYl)(G z>60xXw1(RYMq=0@DYyYsPh?4|MFTdGQK3?nsH1}T&x+u8dqqoOH-k|xqB5{Usnr|g!?vjhOe=Q&H4Cv}4uwYX&0?Lo>@L`%+S1 z%k9mJ#@5N;8co5i!A_AGqXh;bx6u^D^U}DDbcQ_o%ZZew@M7?cOc;mU%#|+L@XG?f zdfd$GQX%e};K7n@J1W}gQqj_-!s@KfG?YPiu!C`JpAFcA4{ zWCj(NX(-WN^pw3)KpEucJf^qHAkkSI*^na?rDl_7AJ{UDUS^-CBV$<*Wn`$Z%!4Se zhsxMikk+9Ft(fMyDgu@=8nC=5wM#Zx$tD5As?vyDQAI(KM-!6B71gO;o)xVOqadnc zG{Yt&gFtWUj0T2u5KMHcp~%Ya?J&?~;=kmZah}B~Qs-gm*#6P3Wxz0BmoLBvdoAow z5fVb{@o5(GjhE!s!pbE%78A(=I$EQeHW}y1CD}w$i=VrT>J<)yx`>3R%Q4_mJz2jG znY{rOkte$ z7U%=jwL%PV<~Ubv6v6Z zrJ|7~P4-M(^jl1E8=b;t8Hq}la!ECMXUw-eW(?8M5wbdG&qwoOD~7TX0)zRM%`}u) zjj)l083VZm1d|>ui%-rKHUz?DmXt2JSGr_ptRr?>kR(>D6!s_g)F3T%&xEKte8}7+>y& zxd@e!CMsH=-B`uu=LpEWsT~GI3RMaN9d_yUhL@81v`LVxA>?i^UyGA6Rsar5>_5Ku zswet0-U!ua9Y+le8X0n*#^_;xl*WugUdCUEBxorDX7-AfV#=oCvXIO|m0S!GGCH=D zD`C@+%*uI7ajd8Up~{Tl&PnF3jH;Ag5kDc%F>};xyi=D6pXqQ&hV1k^+b8gR0^I|84x&cMRP|2_6YQqW8i1)46eF}Fwd2EP(}1L zB*H-76flM|8*;0Fv{WKbSFop0o>XQ+sL1GvONL^Wj5hqn8 zk^2@n1AyYAKP$(uMJOc-Gu5JcbtW(yWeVb9nZ_HcGnKQ^Fpk2_h3T>&DQwCWVdHSG@Rg~-EQW-E_72$CaE6@|MOIR+ z1(Sx<85nECEVU{ULYwEsEE4VXQp`BB5QZWuB9Ft&96UpLMz4`(ax`DIGxw;dAjFeu zZb#wDWl4*$Mgj<74^bs@qB^3rQITxe+|2uF;0mU4%u9}nToh}yNBL0*VML8;xReU6 z+IePb3`N6c2$6G0*{!1DQbYwImc`JrA6{{=ge-^^%S5R(;So+oqcMvD9t&m0wso*y zVIz^~wYscu^G#+-VcA_)xLaf=jRuCz`wQu7XS>Q;Y7?-Y1jx}qSpvCW6|P~4GeVzMmd@=}6;1>y& zg3-vlAQ@eRq=3;-Sj7xZESH;+aN)6x)K{t12uFfp+l|SF!y^VG#D>hC0nspEC_HdR zWco6@u@ROEZj>ruWMmSf-qAoTmP=E`(s^Em3J#f~bcC8=CSaCV@rlzGLPbTo zG*p%}W{BBa?ZyCWcl=a2UHbIURG}pRxIqjVMedvNX+2T0D@f8Jkr!CX z%`Bte<5<5<%ZxEkp&qB#-Dz*35-l=zHUeA9Ye)i&0B8jzLfWdxOEaR-W^e~13B1R_ z$RMRg7PI#%0mEEO{wPeIalkFc$#`qQnf{byz<` zMURTLj9He7dKIlWrzupp81mr9;uBs@g~e2A4RMrJ`@slsu}k%f{0gWv3l|^KWlERg z$Wk%wth^rFMwJvjO6_L^3?voVMHKCIB(vy|y$4j%7?O)3_&Ik^?NlHO2~AVUK2-vM z*It+*3nvni(%mF50IEnmXy;(OZN@T7AZ3Y5_GDh{F~AI%3beK^{ao}+r;7z)%m~#s zj0i}Vh)}pkr`KY}G&4)#absn)vwE0X?y|RY!k{3!%!8|qGY?@)aW(@r4H+;1VCwQw z$|7LNKnU$JQco{Mu&~5qz`QisD|MhPlIAf3p2t*>$j(l=n~N@sIFZE}Gb9|Piwn)6 zFeAv+&iadh(WxNTpA~CJ23ur?DvV*ngLzvbufedMU>o)=7EO*c_Sxbwi-4hTQ3Zno zkMJ{WYNv`)PZ0-Th{wIqu#8kHBBvpTTKML$bEpYML@pI#Uag^~u+bo6GK(G-s2-!v zfJF+cQKMcCjXBE0*zmS`vNPjE*t|_GtUrPvnx)I4!JbEEmPfF5U{J=)l1X(07}~Fx z*1YJLzH^UgIA=`l4-W0|^&wrp)bGG44f{>(xzDJoJrC}($B>S@4?J+U<^y)^yB`iT zaEVR~!2o73?);_xRbL+h_r`O^Ak=_G#~`eNs}^hfye;~3Y{3Z~)t|`B(Iyv+T3gX@ zw!>2>X`GD^8jG2@L{CJsAney-@u?U96r7_mMHQJ|nKT|v3(D|BM$xY9XdCiK%}`l8 zDceRa;~_!y-~zx12wN_Y_R|L~o7#8Y#JV4ktoquZPBVJ%cSO%Uhj-qkao^AN+)Kyj zd+qhPnmz4M&z{zgvvsVY{C;2r2qpI1%YGgq&T*Tb!-Zyfks> zB(!FRn^-A`0_JBJ@JJj6|G7;PA~VavCST9xQ_I9nl%|F|M*O@V?WBTA)+pMF$xFk4 zx-!QCfa*0fpF$PsEL3p9Xv?SdyLeLV*(1ArssFy?s`nV$=?e|}{%5z{KhtgZ&sOd6 z*&cg9*)R0kdzadMcdPC2#rpmCtUqAyhJ*HP?AW2P(|%2z_wU_hzostx<6xsRIxz$T zn8A3rx(>V4?gMuOK_~=7SOiB3BlheUT$Wd3}<#o|FA#oQB^|@eT^_jyCII0$V@8@gx{!G{1KHX{8|Lpd~&sFcaORs&P z)ZR@UA>ToLx_9YY-MwGW9{p=;n(J!%*H!nc?a`+vj_yr0-5aazutrl&k3PMsdiR6@ z%wP-`IKmx45NhwHgAf+MKU=*g6h}Hp|M|LozBuTh1CFk%{^syLb0!R0R=nmCY~!3< znMSGPa*xxZ+M~v_^s>24o++-`IIC{&1kxaJo~GK+)SM*i#1>=kWqO5OOEP!*1;X8HSN}a|Ic*U?bDri#ThFO671PHo_vlpHty8bAP_tohpME0-4jegj(5PX<#~+FV z4P2rVLok3DjN#I~p&IT8qG$+<;7HM-sT0zD0lH(c0|s>OvfHlx_CIiPea*Lq17w-2 zRBs58%ZGN`gtSSHXc48!ASX?24JE284ANyDAv&G#lp@(g%+SJWv=kLCrE-!x;_Fak zDVy49blSirQyR}1+i7N>J%)D168?_^zkoBA@UFG{?$gwPa)c`rXriFzf8ui!WTXX32_Qul?oqSO50bYp=iSk2l_P^DPhD zfddU({`S`EFtp;=YZk6qa{i_B;BxABzdq%}>BGhy0+q29AS{9-1rqfd(1@f+4FJ3L z+81Cx({*N%*PZctOtesx{ztE&6->OHJKbUF6qnLnQM-%Ebca?P#R-Sgm| z9(wMvf4=_WEAPJk+Iw%lx$bTJNa;Hp-hF4od+%*r_uj_!*|83t7=i)JUKcquaq$YrY4PE+G@6~YNZuK1iPb+#7S-}m%_13R6d$)T!NT9VjG>w01dY>E%G4AH#GR@1i@+bkNx#vd~6l$k%d z;AdEse|qFEPrdx?zutKbdOu-^V#m;Lvi{g-vDlMRgN0mt_@ zA_zhuAi^T}eGmT`iOz=Zr_4m^Ud{D@fhEti-&}iO@2-0_9R#S@-uWO`zt4UXYI=TW zMDrpkUgAN88-Zd~B=8&*q0K|$rcS#gx2a)fVmtM>k*iF#f~VBX8<5xzU;?1vvPoyA z5H{hA2nm1Tm6<8a^ua5q54>zL?s|Bgi&s6S@)xm5?$^6>xBflx)Wj#Krka|5+@DYX z!I=wJU-pN4Z+YgGf4=tK8*i_txEnXUziHDZCBp*C*Zh_DEb6iD>?x;LMF z^?9TQfN%cfJ0p)8gEekDJVAEtSF?Zb&UpAY#lMI`P!68o23+QfyEI%8Vk4c6rK~>@ zZbAo3=9PMK#iNq~1@lRoluo15z_x5Gi`z58N)AO>Lec?aj=H{Rq{wV&NC6eudOR!* z7}GNjS~3;yUF>tXsoa$hU7!muh`RUZ&9_@a53J<@BZqwD%u|+Kx$@>e-Tv$=&tth^ zH{S66hWFopANoSj)=TxHsGNKi7H9Mz5z?9N>j2>SH(t8qulE2FfDIfu1Rw#mXaD-D z{=N3^-F5f6{XW-Y58TbMz3(%!Tip*28@LpY>qXUIOss+jG(bpBms_UJpf}N&*h>v} zw-kzLLDD9m)R9P$d5#{JBQ!Dtixa?N7Ny`ZF)%}XGZ7e;6XF^wyKI@(bl$iFj;a4V z)-oQgtM}Nwem|&OGoT(zxC`!#&GqA^O+5R8pZ)RPTOWP?$=BDtwGR8UZOc1O-z0SF zMw(IF^Ea#q7(jaTpHBcDK#rR>sn>u8!tPgx5)SO!4dtLHD2{h&4m_s5>AdknTAXuN zv&4y5B{L>{zG|e1)}5P$xG-~pGKNdO%vD~nNu3BnR7%Pqngi}->s>gr6-q}<7#pIy zYQ?aNKq2Z^a_CBZ86ib@3I;9(myj z+>WtVV+AE{cbb}8l?d<%a^t$!-+L2^FZtyPylO)s9eZ|Z7}y(%@7vUAm%4qq#=Gpc z*P&hU&Ra}=7fzRn)kJLS@kT~IdUzP~5{1lXNA4?Qg{6xf9wBp~L8M5*;wf0p*t9?0c{jZG^~Hw$yY;VW9Nec<&#nj7bQ*W;M7+-Z+fxrxW!{H* zBi<_7KSpChUB3>n0sik#KRkc+;_=guJg8?El-4w)FCH@YXgB~R<5eW~v94cf1{Gq= z&r%H>T7#XT2xdY_tfaE(l6dw;NxRqt6g)+iK!vO`QheoYEj0UcS%o2x3|TpYqeZ~! zxI6DVx`*Cr+_k;~-h{-Q$K7qe?g!#stn(JmdEoKCz53o8xDS%F?X5BmW%{iIYXl)-&G*h;$PWwvmm! zfXc4l_`$|aA8h>KgH1d(ZFv9P_uhN+owr_n`}G&!eC35VUjFABFFpIli#X80B|0$# z0|*3T{#78H;Eoqp>o;y#ho|jre8DcoQ4~sh_O<7)xb9aIXC95WUr;#ScH6&CS5N^$ zKnvdL@4xV9l2)c21X@p_j;kPzc%Bh1z6sL>{iXHmQ-%FnQAK+#83va#rkC&gg^SQs=@Z?>q9=!H~`&XZR*TQezI`8Bge}4Q8Kb>~n z52yV8`#8|RB|0$#gSq#wUj5*;H$Htg9O3@Ln=eCX#YAwtTeD%^dZgPnoG9>(b#LK0 z1aI^;A37MHt>6&_Z?W?IRlE%EyxYEGtMMtG)~w`{tRY9CMMy^BQk#v?*E);4C!#T+ z9m$@TNh24RBIcHKw6IBEj?&Ab`5B2c6=yV^JI&32!Nno;-f# zZ`Qu__ABqka^6}FvD_#z-Yvti@%!f5xTjtFRslYMP_8wodpCmJ2#pPOty*$S)4=i1)$p z@Y$iMbN!Iscn|r&n$CEm>8^j=hnFYZAh-U%_t$f$#rBLBw$Hxt!fzhC`6qWSI^nvX zjQjQ12VQ=B!^$a5tEcr{Grj-XnayjD8?g2`bPl}ygh87g z<4f27^rv@T`rF5DdG3vu-p75Pw}%gKi{E32W$r!Sv*G9J-!2d^L62cMRL zB5dZn4>+*?>`~mAxmZF7k{NAZ%dwhV8jYIVl!FT`B+h-MmzJR|PbM-0#pkoMh4M>^ z7~6ONU{Bu3{+?%zz=uBgBEc8x_QAtv?reADWw^(!a$Sv{?J&CG#op)8Nhi@W(zML;M7MA&-H zpQ!^bKVi((XZ+~)1=l@!$Fr}$uyGS^>O`~EFB#V3!?kxo!hc zxp!&Ux97JHHocM%q}S9J&(JX~G?R}4NkLONwY4@~8uO*2luC0TE-AX=%f|X;jB_cY z%=}#ajiH~b*$bce;DuGkKHYI|LPdQ?48WTV&%OR4Us7%H3umZ|J(Ew)A8dMS!#j8X z^RIL7TQ&7}-|fBf*j_D1*RPn;d)2f7YmTFKYk5%X4?86Ehnm4aF=3AsNQ88~Rvg{1 za_Z5)J?s2?SKj^4zXKA00Uki=VeCT}yPIuXk0(+P(*MvwlpF6b^sU0v8sr9bc+JA) zLy~obrs6zx;*w@@=BPXlM?fQSc`x-wk+~OC`m=nAmf3WGz_3zyD%zbCwI&bh5 zCv;viwr2UH-m9h#STl3*7V9fz2T{J`p%)Idw?y&qJpt+ZubJ7j>e!m)M*_y+E53Br zt#faB`rbD;ylska+VXo6=kUf2Amn#<+%WR!u?JLl0#RL?Yj$ne57gmlt?IPFq?d;= z&$Ak)z7sA9LhfWQ7YZa%B95Y2D9XnvVKz_`L!2jBr$sX}<;7X)+yyHdIB_JQ@x`8_ zzkKW+f4>(uXuNjba-s2njT_@z>)*NYsk^`ahuM8sP49f!*j~$zgv0~a9!I6t96xZ) zaa(g7KVbEY`V~hXdCgfz{`Rc8mZNbwa4Ul#sU`_v^lCY>^WrgmS4}(Prt@!n=AL)f zzl%+M%U`5Z?)B?I3*K7*MIZ|A5_j!a4eGd9I`7uw>qA{`=^`n$%GhI^n{e5zaIv6W zg_@>&rkV?F`ch~HwDoI|^PRzSML@!sfluf076EVqEig9?>DQ&M`?04Sci$s_#)p9K zzK2)3Tl$&>FLtm}?tcF7-@18D|5ekfmLAd2a?Id0#}9!tt7mS@fmyM7W{)LDOt|_R z554l#L$5w{#IMh+UUI}h%p4oqhM2%Jc+Cj_R<-1a{;Q{-b@SZYpZzm7B7on*1r9<$ z3&_D6hSN_yzH?o7dpEUaH8@W6Ecdb)FtmrC7+m@ac%B?!)mp4sJKQ*6+)$dvISMKkl?IQ)qn7zr`#1gAIJj zefo{(=iImI@Lzrv>NK_-GjR2cA=|15L>#nw=73exdoGzY{;IFv_xyu=ruyK6KfUnK z_^ZySSvnCz+aPpuF<>(Q5;NzJUz|AmzLsa+c!3EvZu-!Bx1L)-4k!XqpsRD8R(A~^ zb@-^(*a1)G(k>RA>7q3b^=)0#Knq+Aty7-SZ)cwdoC7vb%+XyiZFrP62T4x@k4yu! zSI+2t?zk@w>S#jSJ$i88F7@4KoO;6jkNp)@Y|&c~@8K2L1~ByYx_5r}_-#|K`Ci@f zW9nOu>W{CiK&jO;w)ud2|5ej_Ej#kaUw`vY&p(Lm0*P?o3n%x#@ZizE`Bu+mN5Wt` zWLY__9*>L5kDhwXcYpop&2O)ZE&anbGpue5A9U_`($)Ch?4cPj9g!N$4$nNrLx7AoQcJKK-NH<_}tZT`mQ*3z^WOPVAYIm zJ}C6c=~!`-e{nQi*f1CEzx z?@({HL?6Vx8851S`{>ORuKIe-k|P?HA2VR(jDf2@lFFPXt7bN@nqIYd!ZE-8)?Z(I z6trWv%<5leD*yn8kZf@_pOme<$4v4wI252(B2@Au$UD#*m!EFczNVZvj1rx|?)E#sGws0jC17u4j?&YVdc z+DR|U8mdX$q-Ijw!{+nE+OWhBUh?A}jSn^Us6PO2fp+ZKwa>xLzq|bge$&9d?pGme z#if?jb8o(M`XA2gcImi=rAH6OTBgd=Kk|VIGo$mOQKNr(>R(>672VD!7r=?PK_J9m zUVLQiFHh}q>1a4^7YqaOM&+vGP*k@?V^9CXxzE1&BC@nO-*Q@X@5T$UYw!F6J|4nb z6Ciev`u*_{38=0)bGVq7K+=nC=^kpbA8osoWo|!Y~Uz)3kgtNN&bg#6R*u zrB+PuxbW~1Yrk^e3w-Lvt$DLp{IT%<7alt7ijz7m8U+jp^3jnX0*dOrd@>50cFp%5 z`qvWxgD;_P9t@}hfuM24Z`M}z=?P--O+}E*cP;wt589>9&=oUt1<^(ihI~|wq<+#p z^_T(ljmbb?1}Psf75D2nX#T-;|{;5|>Ld zTiT%)Bc4QwW)onO9T=@O@{V5rXly5l=zm}dBf3GOERD7L-|AKX0l(o=Y^`$Mb1 zH$YJDAD_JY@GDN~zG!TJ{_%GT^wEz2Ez|lhpW1cd=y6w`{+E{?){6d+{UuvtSpIPN z%S(?Oan2Mn0!PpzXpZg7jN9rE1aGNxwSd&+R{q zwGAtJ@$W_e@2@XCa^$bh?6GKE|7BCQ7dFj9lstU(3D-SwJF;xah676PdFX!73zG3| z77+fQc*)aqFGvvDX-OuTl6+dxxQrg|`PTME^p`hnO&A4lUNCv@L%ZW2#^Mgry}4Ii zb0fZJary7B!I;My%`Ybu=!SH1W#Q|MdLdZR;+* zj!sH$VMCkw{m(x*@v74ic6%X294mQB*9D^vUwaZBS2!ojFZo*|#Os6?-})E6uZ4eN zfVVF|JIIFw5W;T3zC7*L3@LG0^duc)G8CgRB;$MzxO<;j6Y!=tzX;lR0JfBF4b?}? zIO^}uJdAJGv<6wx0|_4e*He>zd3yJSqxvnM46V20fq?Z(CY^uRQhfOgx5A3l+ig`+Skt!-zdXR*8}IG4KRNXTf{O4u zdivGhty(aq-?GWg+f``H8EHM|uI2Bpe{VZ!z3HuWZ~y3)i|a3g*4uMh+J-g-By?Xm zdivGh`RAK2N$&O@)zFW7A9nIJ*Zcy6gZ8fdYxo`1uDe5yAPwsRB)oji%)|#SpXZOp?6ps0$KFHxb?9~gzSoV{1)G17g$0jV-gxh=)35(o zr;88mv-IfZWmECYvwaUxSAW^0*>^5|cU_@1Udcjt9UppUtXN$5lnV5odBeGNizjSf zsEP&>`Yt`X)BM9uyY6Riy!R$3+RV;Kx!>Ois?R+8tbMy4fX_YgV?Gc9@2A!O7@vY= zVp)4wrJ!vXm`zCR_A}VIYUZFN$L>3-2j%#*I*+%lt!|mP~41Ho5<@W48OzZ`slHi;qZJBQHMV0D<_hL;nFrg_@hcgx0UV z`}#@0JG=JM@!K7(0)mRB%O-WX_^=D^T8?ie<9l>~-5x5C45~pm{((&AUfuBH5)cA% zKom$5a%1H}zI>OiP`TAmWU=FD1bitB~^cGor@X)V^ zoZmcrbKRvAY8Q`hUN)uQ(qsDXfTQ11X}wJA8gEv-^yVwe|8n)LyOz$qr)Bm%OYeB* z0W9;)+-oYTz&{6gz-b@4S`ohuy zy}M#*^ZN|n9-&2QtFo(JL$;QE4&*&*54?0L{v8&+D~NwWZtr_{!8e@${`5oo zmS6=-`6ci_{>oG1e{pJ;`6HW`PU*Ykm>qQVS#oskBCKm^%`de)`10eZZudC@_M10+ z&kF|5y{iRZ@P^nqyv?-n-4c9#{m5UO-ebX-9fVn7K}(lQMvh&33LacR*JdDbXybZ3 zq=9^V=m{b4@4F!iq#3k$iY;w!!nu65%uBMysBPD_b=z__uT?zPe&-Ope*bKbJ@FA6 z{#h~Jp}q9-Ww@Q;p?5PIIKGX8C#4_VG_U)DQGG5uy6=)5c=TCvRPCY(v4>OZNB;HX zgey+%zF>6yrAJgR9JA=&wOgw-z70P9@>AfuLy;pm$mu@+@E_fD;k)bJ-L#?IZ?o{O zy>SCRS6Z}oDSoI8LO>3Pf`_Qu?;ToNw(*E%6CiEl_IRc>PJ{BSz|N05^<~HIKfWj4 zCBvew8BmAsAsqdsshC|`vZ#Oi@J;=fPOe=vz8_ZfWk>C}qt9hW)-D`B`wl&uN$aO3 zUU6FWf-y~(PVBXC?4tWF-#V=~eel@7o*upC#2T#Y9f=&lK@KQtUV04Z+QOxc54a&j z&jIxi1>arSe?l+FRH3XEY^f6xWAU2MdA}`ouJneVj{ZXZzW7WHU+Kp?C-_N_rB|-N z?Bc&eZ2!Y7tgNSAeQwH?XH?H04Iy?kUF~ZND?c^XFv|~rbk8$yb7W>uVt_^AM?n(o{mR77*?bXHpTprSBA-C<`LzhoK zU{W1EImOS5;m4iuZ&L6xagRLz1QvCBLgP_#!+QKnnD@`W{j!?*qZ=0;(QgN@3vt!X zy54rJ?|AxwLt19`TrhS=PgKMQMIZ`vfizHue=4{cx$)2W@dIv?Pn_1JuBxhkEq+P} zyY7LL>L8YG(2y8vx}dg}DQjBJ)}RfK<2WCzr}udi@Kr|4oc(%t!HULf`30*NV_t3M z)q&m%z5SW{hqWAwx!Py(QGFL5*=GkIy%$gFwP4)&w=c2haKB$qyy9y;=8po0SesjQ zzeWkzmwx}qEd!QJu3b28M}tRvP}FzvQJ`!1^67Uzdp~w^U}#tFb?;$c#gEBA4u}G2 zAQ0b1J8wcP*yBoCw{?$}<*=1zxr!f&b~(N;KD)#(f8r+~@GtN11c({6xkdfndtm9U zb?=<<`*XTncu3Qti6~(waMfCCJl0OU;?y4V58no@*V)G(zkcYCbqmMi9<&qYs>yv6 z=<0mYh%>K0=N)_pbrarQY5V0ichrsR@#Bh+1Miwb8VH21<##=yAHJ>t`M6GP*$#!2 zwr10ItyRqBtO*e79Up1!ISjuAx;uV3P>NhYk6|J-6Uj0@}S(2 zo#qbfvuNT@J9;lVqSqy(&%dqM!>RS!ulBh3u*L=BdtN+h(YNy;8Vuyz`Yr&Aq3?3$~RBJYwhahI=rufZzy$~+Bmdj2G^{qX=hW` zmbNcvmmVrh^yB)^o3vZM1F){~Nr#>RT7I<}KLoLvw;A#8(DCorzIoj_`<_3r&!S1a z7ap7mu2C+hy6a*%L>sIi>2NLmTFgtG?*)Mfa@U+I0<{`7;`}y1#zye|MRC z2yPR7F?)A#U7!vGo_XClAQP0fCpUz^CyYz4T!~j+5C}3stlj$`(0|^Mc6W}go7*+- z;BlwMHtsuo>go0EL;kbt7x5lAK0&MQQ;XkOz4LGPZvOof&~n!^f9|(vQn$H>^jr9` zYyBj(zVIVy4W9W-{=o+?zw^qO*Phe;!b9ulkJ-^$gE|tpXwu!!{28g+)0+Jo*W))) z4?A)+?%MeBA;iL4roGM@p&K;ME-RnVyzO^t8=?Wfe{$eab^JbX-G2DZB0Pbg`IEDr zd+i0hN7r6zeB6Lv6FL8u#T{l1Xj(A7_ksyK^FYR)7acz9*2VVuwY}zg`SBxGom4gN z(1uIKR$n-B+tzg|7M#EG_N(8$VOGtBBkM02yQ51(IiT_UTNi^;5Sw6BG$02=fiw^Z zGC?dzcF@rc_`XWEhpQdB`{Nlma87`g#|@Z&%ohiC#6KZV@MB`w#`hT1alrh^($NH!tZCoYil)l8(vNHW z$r!vZiZ8n0?;Rk{!4nR<>Au@Gvw`dVlWQKnspi7NdR{b=RLmd0GmobE<9c2=vW;~; zcSOS_W2)yJw&?Cv+i<_eIs&EmFcgI2b*43(tG|6uMPyF9FTeKBR6A?ZSH-Q zjqCn+->vvrb;ty2%Vtlpu^%^l)p6avG5~M8;V*08N13oc zO#kZf_>J`TzJG!j{Ps!v#EIP8&oFSONJE1NR2pYRwaPW6;n2m2)w6~389zqmI1A)5MRY5Gg zse-4h?k5e9rjXLb_4~E0@mSFq!b3Yg*uu__ziWW6f8)N7w@~oDC?4|Le7T2Cyvc(N z{E#KnsxCN~JL>##JNamwKNhQC)~%Pmi)UB-{2)HleEIPSt4``Z_u%?VM)#O^=r(AL zx2^CPj1_(TqZIm}^9OUMS^%wgMs=XEYVN^@E}ikrYtQpNw6;EGLMKFlGwqT z@lBOZyFtET%kj#~m+8ih9*Ld;RUh_uwzGQsUCF6GT(Rj(&nt6xKx}~i3%CB_40PA{mkGb2TweHb3 zK5|?C`A2j-e+cT@nWZ6D&v}P|(yRY*1L(zX(^ov0>HY#yAPxQ&31os;xMhE~`yP7^ z?c99fQM@DD3ohzQ-Dl!F+!i>_QoP~p(YrQw_2tJI#FE_sM+DlW{p4NTN z!A8i~ri*v>(RlHgnz@JCy2kfNZ4al`Cw8ARy#At5JuWz8Yxi($n@|4hnNdq;?0;@E zgx>iTAtT7_HfQ*>%fJ588~?&;YV&h1ZsVKY$6p%4AC`hxkPJ^*kgxi*q1wYCXeROT zsgsSB$910B7Y}Cm*=PK@4cx5pON02l1KUVDTCc}v>$g6B4{p}ga}R60Xza&wa0y&+ z=&YMBP4AmM{PN@DSA3=0?1Sqr991>vkOg;b-E%nB5q^Al?(Iuz=Z(bT0_`75UD&J+ zTQcqTC+@M|R%%0ShyrQwXEJ_x{!bwmB!h5}uglE7wucvYXJ5|Qy?Zdeadbp2UTyL3 zGBh{fj=%JmEAZAR{(4dC`-SZv{1vhlf4Zvv!qK(!4#yt;u^bH-jjq1nkXBlMx!dgF zbr&99HD|=uYK_wHJ16h0U$^M4Rkd>uubMrg=_1Izv#SH8^%spw2FN#+kQ^PZKb2peBsf14etVD{4rT*4c}{TzY*U| z#xp=g3SHvg!r)cOfBo^|Zs!kcxNvmig=0RJ1F5U$K6eKW4>iF&`cC!uZ7T>j|_JYH@oHrD5H|^X4v4XmtKm5P`co9f$@7Hc24FvlA z?KjjyujU2_$EA93*ZvnCQjZ^avyQT$Ui;&=k@Qm z@enAj@!0UfYcHPktMB!gJ>0a0+#kcyaN(%xIU{D>ys(wlL+j>^teSoBf;(4kt=1?I zFT3zGg8wbO_ZL0RAKvM_LA`O4qTD;FMo(BUWu=p2SdN6zVpO51OdmHhs3-^(~ zJ^#>zmM_)JIb^3X&^pl$(ppvPccfSR>6caK59@rcu&&b# z*K6%1_pI&s^MReu9gLlPr&S7Ko99os(;fa<(`G*20)XE?cvJQ4gKOs;+A#O9O%kPw0B?;Mxlg?LKSRN7P#Hq2aCRg?Fv$c+LP!p~iWmcTTY&w)*@L z*FJdjX0(Ptkm>v9osF-bRpXnbbsexdcl%1SR`Iaa+_a^yoppI!$Ei&)!*5vO3uc{q zb-UoQi|`g)ds^e|2E1Fl?4B#Ya~;<8zn#{{cRhE=cF`JZNB3?#AuYUfMa}ucd(0ZX zbLs`Lpm)W+zr^bT5T3T0ya9nA)9l6b@DBta9OT1-?KHKww2sw{$1~r+wWS|0u?|}A zS$`m0@Z+)gylgYyMZt{>Ka_j!ALsX&J*;jn*7eAb>!_a#LWa${X#xJ3jNVJJbq%ct z)n0H&_gOTT0?Q%xUrYs{P>+C7EP``A4_|OmkeUDuD^Hfxo^DvrheKqU(sqq zAjq`h>NQop`PZ58eI#hzVN6ZFuBrQKzU;!;UU474_rcw0*4PO`Hx2D~<&D2-@5Qx! zl)34Zw_f|&)jxpN4Hu037>{~vbhFx8*FPUrJNuCC=WU;L%_p=?oBr^~ZHF(OhLVwV zr$IzX`087);ZHKR`IT|K#D-Y-nNYl=0{O6D_a4zrFRx7Hd;{10`uIJEbi!7@ zUt?#u;0KPbx%GPdNzgX`!-j^xbMfLEFVDRE4DgJ?KgOf(faChJ^n}a5@zNVFgK!{eQx1HW z`**ipkDp7z(#9XJgw}fw=`59bw&fh_noAo!dkpL-t>H3w)UfOCzG<^sZ~Eu!FHBi= zO4WHo>*pN$F&=eu4(V~;kXe76pWm;?Et%2foPoX0Ke+q3gQzwB@O?Wsqc+(~ouHTB zSlG1bmdEcJaMAb!{<}~8oWpizu~p{{nX>Au&%gcxrfYjzL#F?~z4P#os#yE@`#0S8 z-uH7AQc2Gy+3aR_vzwlf07=9Kh@hbKCWs&>cpbJwKd2>>KOtyWj41YXQJG|$|-e-EW!5JZjXRkF?a}IT!bALzlex21^Zk{YX8% z&appGV6M(x@m@1+IH$cxjeb9CS+6eTu9PJ=xfb8p$|Z%pw?8=&RzpdHdMyCDbn%@} z-eYATTLU6eS5s}F*HWDg zO&=+2xUC5`{P5``1D_h3Se=`^sIU#4gG#wNw`Rv9?s2oD2YRm>YO2bzE$or70Ib)b z6yC-njH=o50ga}6^~P840CYq}s&nxTI7eF)(_EbglVNouHvB^m8*UReTp4Zg>rqV$ zt1bngcH1n!(YD!7KOY^mZoGLx*W?EZ+R!;3$cKiS?GL+cI9NYE#8jDStLdIFKU?T^ zQ?RBNL!)=wm+y9csCU%7RM(n zjHxcs=VI`aK9`}+5@Mz1N~zKA&_c#iLJ6$KQeN2pN@HUwr|#P{BXNFC^1}Q!bdH5R z%=2>^((4Rcb+?47tR~uUv4`u$&t8Yy32j}p zz^XUQMhPD&0O)nttyV;$ONx7pRIL{FtjLUKF-H2!xtXslfY0`YJ=z}Y2K2f)SgZYd zZ+!7~){4%e(_}P$?FY{!RAt(#^Bgrj+R6cFLREJ4w&h0K?8~EH_gryjSee6A znGsp;TE6p%CbiAL`acKvc3ak$=xtRfl`o8qugZMt-|BoeUvrvEo>;~-w3z2|MFL{% z;53OWu!1t8W?GlNy^@}?|9vj7{`$zb1j{4U{nYu>TfTVn**!0A`sC$*ef-ki!(UJn`PlGShkG#g zhm(hId}LruS!!DqO63btzM8P-KO& zwBcez#WhzP63*f@V{PGn-)js1xbpeouWnm4)LfZmU(l_soNYl~{JhNSZQ@8zwYFAG zG{Yb5&--D3bx>BXSO%u>Ze2Mjx-6xw3Z?QjS7zS6YWTjNzV>5n;V@%uAy~W0Zc+8J zG~lX{ri{6Uenw4SmYdQ%CX;XJ3a=zZlgkxFD!+C`CFI5WLRC7O8HLJB=!RAH1-0R*mudA zlbq!Pghs16S`C-Unp#xiqP`y0?TMk0x7+`bDE9ay=cLeITl4%n+Vj+a4Q7PmU?kUl zxXCfUd*Zz8wqi@mV}0q$sb78`yJ4!SEUm2yMZT~!d4BheA3YC~y)`Tm1Ny~JKKC?W zp`IZrF{o-}KS#GGhtLA2c9zw&X2TWGQUHV{j~kxJMnAe;a_^plE16i2DrVCf-lr+{ z?!Eub?!smL;>yz7iY;KBP@Y-iQC~MGnf`l<`#vt5nYw9aTv>*#?esdXJiX`g{{Q}N z4@_>bVX5}+vAJ^Y;!2LIQhWD+FMdR(093TcWZPRCD-+Fc(G=HR(&T*Z*XB^2#B2x0 z6>>ejk3G3?4>C@iIeGV!W1{D{994Pts$9GHZ8aydBj1=gDHX3S{N=Y_I2Xl#OudC9 zk+Z*^o$}nQsM$^jShq#F997+-X1fNhzUSoGQ*1SAkW{c{Pc8t-J-IeZ4vWJ%Q9Lkd zat{qqO|>P&%7zP|wb#^&Ue~+Aumf!AvZmSDCG^RGy*~EZix%(YD|26-j|y1l<=89p zY?ZmK%-bHf26^SplSG&vt=YCbv^0st$%2f?u64>q zF0j!$uPdgq{FNG}w_vh2Qr&y)(;Ps?DQDNGgr`>YmRRdrtZBEd(MshvjLLal~LY5kQ6hvE8baM_|R=Z#R+6Y4b_^ZGnJ(R`qq7K!d`C%oQ4_DJil!- zFmWq{IA7WvNb5T1;Q?Un?rmvCTUm8!*rmSbKRqJiHalnIgrvrBY8EFmR4rfF=-D`I zB+OshCu)`x#BKAsUTf#5%(a$hhZHAT=Ve!IS+xJy0d`Z;cy5p<-tqMHMU0AH{sTVm zKlb&!*J>=~nISVQ){1NghU?lF5H?25PA*v5=jhqvl&itUk}%-%Y@A9C9L{h!x~2db zz(w}81EOK5g@U`Sw|W-(UMK~?JhtmUv@@@qjFeJk`#AFoeu*BzrOTx+l}D5&SKA|k5 z6$)r6&x)9tH2dYs%iayRAkR4S%4xB7b;<)Y0u#sON&~K2Y)_?D&p@xEpE|EMxA@Ub zi_NsyGUQoydm2B>Zu^&S$InTPo$ay%bj7uDB%UxQEozp1(!WX%o%|7$5i?5d1GB6A zMB>z#WOV4%50jrQjh>kd0oS^WrrEB9xoK~GwbPpcSA}Agug6(-79hLM*(I&2kJ4E~ zd5u?38Bm^iEA9@;jpe{Y&QNmZrg26kmo%x(KHj(rD}FkCr2mScT}l$|71_3H<`5?g zY4w)H=PsNhQ}oUoNmmK6XLPaZ5J~jvr4S1sd+$vFE0UtJ&f@#|%u-hDN28R>*qd zTOWN#F_!`a{4*&;ohg0ufZmOqDSc7Q@9+8klk^4oVbjgY6tsaMF!}MFYmzFmO*5U2@*G=P%ayb|3)Xd+o|sWx@bSUM&y)rf zj{9a51(*N}$NA1CB+PL4lk#sB*IuO-iy zb)FJ!r-cUAD0oXaM>$+gMvs>6c=WeRzcuwD)H+~Y{i`70(r-(5tbnRGObte?C5lNR zf@$Shou)+3c)ko~`shMpz!|$<`{D+mVcG(i01L+JOOGnCIt11kSMTiQnTc~!gQuEizclZ{ z@8W_R9|W}8C&*y1W#$Xz!Bb-`b5kMm>Pt!rFfFpgVlB_ub-=GXAh?)=c}xHba7Fwr z>bls$=>VsLC^6kwt2GYL5^Do)9)wSF>5YgA%T%H>%h3?pXKM7Q&yQ#q-?l z@=S41k;ytYy@iUO88{{CuE$0kJtM9KZ*Y@Kt3hmJ1Q`%>2?DNhhF^2Zm{#~|;tmsm!tvn;CEd6SnqdXHiC!Jjf-X~>) z)$QZ~lHS?BE2$hiW`WeJDJgL&W>l$_M)*5li#waJ(hU|90RRGk01htuV1XHvlC;2s z^W$?@4%PwHG*`Xq>a414L#eg;ny)nH;zwrDw4;a4g+=N-lK~rOwY+FXzuXR7VLB{&BP}UTeOA?!hvWjJ$lNvfT zw%@~pe&h^oFWpQZShUV32xA<+?<04IPK$-$t0`}ENn+Op1-rlXb-WdYg84x3==xPO z$*{K(>G1%>LKMIfsDZTxw_AS;1*Q^c@m0x#z*pIDt$u%cWb~abhSLL_5!Re!`f(gP z{Pj=YAy?k25{O}ZgK^{~Z!C)`wZ_kM*yf}q!GtC{V#St-=r4WpGP*;r*0+GQ4zhJ} zfg}inyca*-Y?@_<;AR(=lA20wOSe7Zdu_8)Y9P3K+%OI$<3d|`EplM=9nKzW83RXQ zjhb4hvqV|iY9N+yD`fR&;VELtkZF~@uX8fR$wlX8R2xBB^TK-M%15mYLpaHkeT8>E zIwWFxeA4XZ0BxD=La9fu9e?)1SqxeoCbz3s$UA%C%&0Z@h8D#_c5~pKoeHA{t{iss z=VM^)t#kkl+fV;`mTS2Hg36j3_fTpEwA1D>iK)=!SJPRAyACd%5?Lt>3SXkg2l0vL z9;=758APbIxa-_$a+vSBXUM)o`+dx4D^f9TuYUSQa(PyCi3O#$&Pnm#CCyEZF0u0a z;+~gDrv+!Pys_~r{UP#&k2X`l*cpOs|4T|pm;;NPuhY0Dk%yCW6%jK&c;pcC|Sz2_WbTqtrm-oOzR>q3E)vze2l}jtf|Y zc57ywb{fxoH9`uYmFxb*knmfR=zLLYJG-UF*y3rB`>ERBAOF7BgSQ9WA7!7DW}W5o z)5XuQ#S~i~*z(}zE0?`>J$y62mA^tHq*lMaD5f|uVTR4mViF9t1Wk(S{ZRk!PaN_` zsrMfIlIAxM00eMgZ^L^fb$@bLZG1K0q#}zdyB4e-C=*L5yMZ5nS#`|%7Gx`>caPI52v8mlM z0D_(JfZ`~*(STBa&=gi6sG4d@qj}b}UJsyax{fveqk60!7IuqOjJUcYlDnnG+Ty$Q zD){#odoKJgZY%S~RGO(`{_FDPC0myTPmYW&w%BH+SY|nWJDAXAa_k+C+|3f8Hk{+| zRw!@&m~m!x?^u3UmnpFn!FL(?3M()I!IPqvZGV(SHSozRALW&BECT;v-=4f%^JDGt z$=%Wb0yx5NN$S34xPg?e$f8mf&p=trDTu|d4!W`c(qF?}Jg1iT`lln5+M!EIEJqrK znWLsxmL55KoTU*y;D$3uEEly;-ca0WLO8}bX{O6k>h$3fX4oUAnJZteQI~gk<86EM zXIzD1&f8oaIW2)A_$VNsl4hpFm)HU(hE3W~EN-#)*(4J+_2o;jdiuPXT@oU>o(Txp z#RC?AB7hT4tAf>-i3D0AsCS};%GXh9gh4pnUr2=*W5JUfVVE05macb(F;>HR6^2%x zefNHKan2If%KYoQ+?Y{+X6dY0^ z_@zBBW4qTJgsPFKPnU;_|H~oWeJ|s#z96#|^Qb>hhs%*#X zbc*1kfD}JyVtC)B{SW%n9{cS#*h@dF-|zt(1rOU{J`eyz*5W%nfR-AnaFkFA)t1f; zwD_u&-GFse*A(O&_@;KJG<8Z3aJqM`Hx@RxaijCsd4qo zg^wt5iJjsXK5+AAKkS7ejT^3uza2eylHE73nxkc5J0mCmh!3#bn^(AAL(>B<>7%q& zAgeo!yhP9~8%CJg!8`-xJvO+@jfrA;k}HB;29E5M>>KkoZGB0Nr1|Es7(3Dn zW%qZ#5;Iva#5c$2=D^s0ZFzxnfRnRR*@*+&VSbmMi2$MDQVTV$j@D2XIO>Y5Fg1R8 z@O(BB**N+JAr+&f{ESYox{X!=J-d)Jv4EWU(>{ax?f&W$8j8Lt=c^h$erw;`IaNJ6 zjSn?Xw^>UZi6wSV7d|O=!c$X@pF7#&L!+uj;MaI)l;9=>fZ*fjPTcp@WQyQfBAx-- zJUxjb=T#NF^X0q#08Q4g6vp+-xb=Hlns&f@s3QZAAhg$(! z|96AJx1j_#j<$@;{ky3XojbPfrsQSBITOcDpYYAGgTCiPAf6Toi{&WkyI<|>v7l$b z_%89qR*Qh!W-upgQcUr)v(8`m#m5SqCPAPA)51ZINE1to7*mXbnXq(R@kH1QlbLFyL(Mg~iR1F5>PcuM z;x}lxk8=A9xb*-$cshx-2&uw&eo|3TZQ(ga#iF~-a1ASzXb{F_CONyNMcd=JI{C=i z6ODGk47fk~c2BRxeS;>1#TQwGb{E@l=3-lyiBYqkD?=_?3TrSG_<}IEf?oRV+%M;F zJn$-gy>54HD%=o^#hATmE=8aqszemcG%YdczOX(^`u*qoPlZM^XXkxTLIEv0S?5l` z(r8;8>}Ba7tcKwXVD?xuOk$xS=z*_>js~J9lnPnnSI1O^)KXL7^)Mu>M2H6(ft}Js z-O+RLym!SP!~h1uMwrRCH}lA_7be5%PU(@N?JYsM z0libWD8<}3Mgv{psCf!`a0`P0vGGtV1sPJun#7HWJUEI9*}aQl33f%(7l^UNW%bP6 z^8R+36QWM^DY}$Pbx4n&IX-dilz@9f!|#tyDsfnfZDA9li#L{{dRK`x1c(~{;;*lL z^4gF`hB;@YhfIk0=RKV}j|;}}5IP|ePlWCD|7;a%&Qg%ZXm*QhLr)NV_LsBMHDxigR-Kb+N zp;z`6P}~3+fGW5NkB^Kf6;{ZG7Pxju;b}NsqVk^_5kEW=d84(!v6dW=7@cIgWniE8 zKi|zx|Av^+fFB%S2glMa%gx0}0pmij=V240CqGqm>fEWTY@3ntPp6O0er~R*$Q&qT zDWS-Mt{INJ5U{}UVWw%;xtl7Ec;95v0No9FK`^&+-2YS&MIiBjdqZe`KfLXcbH9o$ zDhN|)_&@~Cs z5qE>@0kn!Mg^Famtr`;1$Hrh9ojyk6*=YiWcaI(V@zXQQ2hff_DzH;<+V*-Nj3%_;oZ9De8MS^~&3AoH%>po>k+ozb7#2{#eU&dm<gO8QD`dAGlsx7cFcVP>89BdPjm`u2g~4-b4e`tf^S`S?{%c%V4U>Nc7T zK$91G0lP*{9!n1k2ExWpPH`GWP@YLF8q|62X|Z)(u3ZruAPT_{s0o{|L#!--j3~bf zgPm?WSQ z6sgD}u&$UYB!R(NXr=HMsHp(%uQI%R73oMxFf6=y;FBakr63Y%*N;D#Ob-Y(_akhrAfyk6(aJ@yE(skU7CR} zpSbWdmvr2E*KJW&6O)GVS+;=GNLYum*e2&I4ofs8C1N7vCCOM*nj}NyMJGOngjiiJjYP4z9MC`SKU%L3? znWJl7SjPkwx&Y9oOe^eyX|N6kO8AuF)o=t0$$7zc4G!sJyh=b7xDq?Jk`hI8x-f`f z>RFd1%|}Uvz+DDVujH%eA|4baTACY1J7?aknrieK#B#)L88P;F78{p8SjCPoj%jLo z!>19R0*b==wLPyT6(vQCk7yp&M6^yzdUemM*9dD0&fS+VVgADl8C7OvOz1*}S#+-w zgE*Qjc?Nqe1*-*1H9gfI6lAiYB~cLmNQDNCM=b5D3@Ol&is?>GMp3Vr2qZ^2nT|{o zAEnX8uO?T>j3h>T;dfbG z-P0KvVmC{L0DsBtB1Z0Z0nlLEnH5_%wqz=l!BDxN`jLs*J6~EF% z$hts7TsHz5=(>!l@WRN*SZ)H`^vE%lqpILR$q_l@JsGHQ)>0-*pWnn^g!z=${ zH4Ce2{2W`W4Q#`2La+b0a^T2;LCXi5rzFNqhz-9tB4T``+uiH&jK9Zg{am4?S??8Y2xxP*42D|@T)Vwo##Z&rZcbM z7<75)#eDtaft_FMeC^}c*1WZ*`lXtpr>Bj5eC(iQgKl5c@7C&DZ=Bx~$3wrx{RS-? zJa*N%X-`jI_|n3)Z?Ad%lh=2Bx$Eo02e1;&==3#A69)(VahYOLSk`N)-SIS8VFe6f zHw`T@hCp*38mO~W(-a^UK5nFxn(Och{!8Wypp`sGL?rH7MCC8R0zswbN*JWTi1g)y zf>J%pOjY6vXdwV1BQ3i8BvO-K`Bk|q;=8XJ5<_@{lA%T&B6W|RCC@uDD?*i(7ty}ch!n^($SX{PuoG6kpj+_Le%F`YW4MLMSU z)Ml?VL?k~o+y#dvE_ec>2Rc9_0V)()FQNctEqTx(5Ybggm6V>c2&^frimoB0@RCU> zN;LhIA|yk~SSpyz*E4sKmn3*J)ptZM+y9z6GtA56dsK#*S#Q>vhb2U@i)8VtWuJZb z1$)y(r_8?@6F(uqAUEp#Osdgf04-|yOJa}&qy6BM%PATQ_$elZjY>}tLnVoUEG1%v zN%o>UB$^ta=$GK027%NR9bd^?;UhIvq9DN%K7jA^Nuxo$j?S3hH|!=0r50=2UE$&gwY*eD2${>{A_K-$pWO8Hx5o~`G!#U%VEr3N zzk0lymjdL8#)j1814q7J`{Me+_l`(NwX*RFqwVaL#zKxxt_U%{ln}s*>-5S#0#nHu z4L+TpnraK+1oyPBis2fowND;6NppB zm;<{(8GGSeHKPaA=Fl4iKoZqGtuKg#)D`Pj4IV!N;wb_J5$m!t6FDGGqAVt@EaUUK zKR%=`8)2ZXU&beclk!35w$AGMU>Ok`e$XT8Uu)ZHg|ddb$_o%xg>Jlx2Tx4ZtDc}v zp$H@6YPjpduwe`pR_wr3*4R*kvu-WW6-kZ8aRIhWCx?Nsed)F#chFCH>!Y0qj(kVA z z6~(!cLYN()v3I3qFS+xE^*T=k^Oe)Mp0s5$ zRA%kr;}J_`Cvo@4RFh116`&H=8)in)+z+Y<`A>~ZE$bC_OOl4TH8>y{9Z$4mIZzPV zuIyH$Ps%dswJ)rH_mlTI$(-3<#wE~nbjs+dT_fBLe94IH=qZym1RObY{Qb{%b8aWY zfL(9yPG<^IA(#_{AfB6X1j-0-QZThuN?EUhr$*}F3J$2UyP2%mtJ-Foet9rog%@Ew z7+zmg1O&BWDRZIns`beO&017x{zf9kgBvvsR$UH7S#-kmj8y6B#B~c~{@UTr88?OZ zvWaFJEV2f3M+rS~nlu~&V@s}|Ui`s1l17#WUnrk{B8fZl-1O_Y!ak>)I0WLi9e3h^v6 zpdfTQlIc>(HkdB+wK-t$wl2zAFv&g7DEhNu^Xs5i=e532d0^y;vXVMo0?9=VimJdW zO@8&n^7z5P zwzjHI?kL{j^COH$7JAlhIvYsY2(M79^XlPgNTXZcFiI-CI~FWrzY+&_(U_sFu2yI; zoEYkgVTPB3vKe=xyUOJh9Gl6VCi#7O4jDhP{K5IFH$DCOu5BNE^`HHRzy0aVF$QO7 z$%7Xv$+Z=%aHan{tii{>qPV)xv91pzW z$tMC3jKFgqsJMUDRH9=yupT5xvLbP|&Sj^N9hs6f1TYqKoxKZ${cnsQEF?c6VPtmJ z1O2snxej~lyf&aObx^4PT8n!gQq)k$+IFB$U~in4DEN!MNW@#A{Pk}juWpYGVM><+ zt^=~7#o`>P>7qh+gfZC2=0Q`2IKGb>lc8o#g5u&K&`C@YyJP74h^yW+9E@h<^vuh> zxkvY2g$M(V2VU{y69EWDV4@I_=p8G)VCBVz`n z(9(d&dS!2*ES^%`&~r1Q)WQ1dQ>c?*8@v{-8`brpfyu=;!dvEfMTO2<9XZ^paOX02 zEGp2)Wm*}FOU$xcvy(wexGbmCP;T_L(Z-`Sg_939npL7@CC_6dBvFV+bP^${E@`nr zt|;)OQLW}~$W#pX<(eRav!Qa94k&oKRs*{s1lxdJe+@DKz1zyc3_WqeLsVY}#*;DQGboC<9vpMyV7vcA2{ACse>qg~0ItbiZ(3mhH zE2>{|Xn}y9{tmr$+;xsF;#s|_2k*Zku#81`5+SLmeoks(+x_{B0CZh;SDij`ql{`v zK3oM$+X)$sva3k}X#M6czbmt*AJDMi79BJVPLJ;Iq}v`^Xy)8|Hm3yU!~|qU;dtN` zPd*WVU<4)#5s6MBB*j^*O(LKp@Slt|yCv*uW#GTthO{5< zwOave0%2`rPntksJD2v>XW%u+@y`OspM}nru~F9m=$0X@ovWlGul;kc{Zsg-0J?)q*o-J0b!BUFpsf|B zwae?Dsk|msb+EcV^E-G_pC=tY*OCJrwNjIfTZfWfOMz@M99K_F2cTEa!@qP2?bArM vPpaBLfB(|5*TK)$<3LA6&}>y|A=Y)+w3dw literal 0 HcmV?d00001 From 040b4763595f9ee69b5249918e3d1760d402e242 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Tue, 31 Jan 2017 19:25:15 +0100 Subject: [PATCH 46/49] Manually implement realpath for OS X --- release-tool | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/release-tool b/release-tool index de5c723c..11fdf501 100755 --- a/release-tool +++ b/release-tool @@ -244,6 +244,26 @@ checkTransifexCommandExists() { fi } +# re-implement realpath for OS X (thanks mschrag) +# https://superuser.com/questions/205127/how-to-retrieve-the-absolute-path-of-an-arbitrary-file-from-the-os-x +if [ "$(uname -s)" == "Darwin" ]; 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 From e12cd83b80689d78e64e3132f3b69cabcd255f0b Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Wed, 1 Feb 2017 00:53:58 +0100 Subject: [PATCH 47/49] Check for existence of realpath instead of operating system --- release-tool | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-tool b/release-tool index 11fdf501..be6bbc7a 100755 --- a/release-tool +++ b/release-tool @@ -246,7 +246,7 @@ checkTransifexCommandExists() { # re-implement realpath for OS X (thanks mschrag) # https://superuser.com/questions/205127/how-to-retrieve-the-absolute-path-of-an-arbitrary-file-from-the-os-x -if [ "$(uname -s)" == "Darwin" ]; then +if $(command -v realpath > /dev/null); then realpath() { pushd . > /dev/null if [ -d "$1" ]; then From e31638d3dda560a2f08ba73e5669bf254d6c84ca Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Wed, 1 Feb 2017 01:03:30 +0100 Subject: [PATCH 48/49] Fix formatting and coding style --- release-tool | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/release-tool b/release-tool index be6bbc7a..7bc54cda 100755 --- a/release-tool +++ b/release-tool @@ -245,19 +245,21 @@ checkTransifexCommandExists() { } # re-implement realpath for OS X (thanks mschrag) -# https://superuser.com/questions/205127/how-to-retrieve-the-absolute-path-of-an-arbitrary-file-from-the-os-x +# 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` + cd "$1" + dirs -l +0 + else + cd "$(dirname "$1")" + cur_dir=$(dirs -l +0) if [ "$cur_dir" == "/" ]; then - echo "$cur_dir`basename \"$1\"`" + echo "$cur_dir$(basename "$1")" else - echo "$cur_dir/`basename \"$1\"`" + echo "$cur_dir/$(basename "$1")" fi fi popd > /dev/null From f7e9f856687a80ca9517aabf375201d22d1f3014 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Mon, 6 Feb 2017 19:52:21 +0100 Subject: [PATCH 49/49] Install Qt 5.8 inside Docker container --- AppImage-Recipe.sh | 16 +++++++++++++--- Dockerfile | 22 ++++++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/AppImage-Recipe.sh b/AppImage-Recipe.sh index 2ed3ae93..dc30cb69 100755 --- a/AppImage-Recipe.sh +++ b/AppImage-Recipe.sh @@ -39,19 +39,27 @@ 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/* . 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/" -cp -a "${QT_PLUGIN_PATH}/platformthemes" "./${QT_PLUGIN_PATH}" +mkdir -p ".${QT_PLUGIN_PATH}/platforms" +cp "$QXCB_PLUGIN" ".${QT_PLUGIN_PATH}/platforms/" get_apprun copy_deps @@ -66,6 +74,8 @@ 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 diff --git a/Dockerfile b/Dockerfile index 422e4da8..9623b60d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,17 +16,23 @@ FROM ubuntu:14.04 +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 \ - qttools5-dev-tools \ + qt58base \ + qt58tools \ + qt58x11extras \ libmicrohttpd-dev \ - libqt5x11extras5-dev \ libxi-dev \ libxtst-dev \ zlib1g-dev \ @@ -34,7 +40,15 @@ RUN set -x \ 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