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 d982f22ca..8c4a21f8b 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 149b61ddf..44e03d2f8 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 cf2c9cd96..ab9924a75 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -42,6 +42,7 @@ public: public Q_SLOTS: void openDatabase(const QString& fileName, const QString& pw = QString(), const QString& keyFile = QString()); + void appExit(); protected: void closeEvent(QCloseEvent* event) override; @@ -68,7 +69,6 @@ private Q_SLOTS: void applySettingsChanges(); void trayIconTriggered(QSystemTrayIcon::ActivationReason reason); void toggleWindow(); - void appExit(); void lockDatabasesAfterInactivity(); void repairDatabase(); 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 44e03d2f8..9bfe4d549 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 8c4a21f8b..cfc87c101 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 05b80caa2..81ac4a236 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 81ac4a236..188ef1586 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 985374c49..f08b432fe 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 f55fa2027..79e58cecf 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -163,6 +163,7 @@ private Q_SLOTS: // Database autoreload slots void onWatchedFileChanged(); void reloadDatabaseFile(); + void restoreGroupEntryFocus(Uuid groupUuid, Uuid EntryUuid); private: void setClipboardTextAndMinimize(const QString& text); @@ -190,6 +191,7 @@ private: Group* m_newParent; QString m_filename; Uuid m_groupBeforeLock; + Uuid m_entryBeforeLock; // Search state QString m_lastSearchText; 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 cfc87c101..26d9d2283 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 19c661b38..2f8fd5e49 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 9575f077b..322b5464f 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 3aee19e3c..444009932 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 2f8fd5e49..8a7281c8d 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 322b5464f..d9cbd5651 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 8a7281c8d..58f03534d 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 30332c71e..22ca342c1 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 000000000..fe9ef9123 --- /dev/null +++ b/cmake/FindLibGPGError.cmake @@ -0,0 +1,9 @@ + +find_path(GPGERROR_INCLUDE_DIR gpg-error.h) + +find_library(GPGERROR_LIBRARIES gpg-error) + +mark_as_advanced(GPGERROR_LIBRARIES GPGERROR_INCLUDE_DIR) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(LibGPGError DEFAULT_MSG GPGERROR_LIBRARIES GPGERROR_INCLUDE_DIR) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 22ca342c1..b843de85a 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 a94d65eaa..224e54d1e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -28,6 +28,16 @@ #include "gui/MainWindow.h" #include "gui/MessageBox.h" +#ifdef QT_STATIC +#include + +#ifdef Q_OS_WIN +Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin) +#elif Q_OS_LINUX +Q_IMPORT_PLUGIN(QXcbIntegrationPlugin) +#endif +#endif + int main(int argc, char** argv) { #ifdef QT_NO_DEBUG diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0ea73b2fe..5840a5b4b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -92,6 +92,7 @@ set(TEST_LIBRARIES Qt5::Widgets Qt5::Test ${GCRYPT_LIBRARIES} + ${GPGERROR_LIBRARIES} ${ZLIB_LIBRARIES} ) diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 846e39230..83f00b4bc 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -20,6 +20,7 @@ target_link_libraries(kdbx-extract keepassx_core Qt5::Core ${GCRYPT_LIBRARIES} + ${GPGERROR_LIBRARIES} ${ZLIB_LIBRARIES}) add_executable(kdbx-merge kdbx-merge.cpp) @@ -27,6 +28,7 @@ target_link_libraries(kdbx-merge keepassx_core Qt5::Core ${GCRYPT_LIBRARIES} + ${GPGERROR_LIBRARIES} ${ZLIB_LIBRARIES}) 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 4ac01b3fc..933686dfa 100644 --- a/src/gui/SearchWidget.cpp +++ b/src/gui/SearchWidget.cpp @@ -41,6 +41,7 @@ SearchWidget::SearchWidget(QWidget *parent) connect(this, SIGNAL(escapePressed()), m_ui->searchEdit, SLOT(clear())); new QShortcut(Qt::CTRL + Qt::Key_F, this, SLOT(searchFocus()), nullptr, Qt::ApplicationShortcut); + new QShortcut(Qt::Key_Escape, m_ui->searchEdit, SLOT(clear()), nullptr, Qt::ApplicationShortcut); m_ui->searchEdit->installEventFilter(this); diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index c23226a28..7d6677601 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 b2b06e7c8..ecc275a28 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 66b9362a6..ae60b596c 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -115,7 +115,8 @@ public: CloneNoFlags = 0, CloneNewUuid = 1, // generate a random uuid for the clone CloneResetTimeInfo = 2, // set all TimeInfo attributes to the current time - CloneIncludeHistory = 4 // clone the history items + CloneIncludeHistory = 4, // clone the history items + CloneRenameTitle = 8 // add "-Clone" after the original title }; Q_DECLARE_FLAGS(CloneFlags, CloneFlag) diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index f08b432fe..6e3398c2d 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 cc94ca9a9..819bda5dc 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -364,7 +364,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) bool groupSelected = dbWidget->isGroupSelected(); m_ui->actionEntryNew->setEnabled(!inSearch); - m_ui->actionEntryClone->setEnabled(singleEntrySelected && !inSearch); + m_ui->actionEntryClone->setEnabled(singleEntrySelected); m_ui->actionEntryEdit->setEnabled(singleEntrySelected); m_ui->actionEntryDelete->setEnabled(entriesSelected); m_ui->actionEntryCopyTitle->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTitle()); diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 7d6677601..0c776e021 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 051aba199..2fd890e01 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,6 +172,8 @@ set(CMAKE_AUTOMOC ON) # Make sure we don't enable asserts there. set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS_NONE QT_NO_DEBUG) +find_package(LibGPGError REQUIRED) + find_package(Gcrypt 1.6.0 REQUIRED) if (WITH_XC_HTTP) 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 b843de85a..69af4359c 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 90563a23a..e55c336cb 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 d9ba5bd83..c823b3de6 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 083f1c033..b896963c0 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 69af4359c..8c3948842 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 01b8d6137..67b0e1746 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,31 +1,32 @@ -# Contributing to KeePassX Reboot +# Contributing to KeePassXC :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: -The following is a set of guidelines for contributing to KeePassX Reboot on GitHub. +The following is a set of guidelines for contributing to KeePassXC on GitHub. These are just guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. -#### Table Of Contents +#### Table of contents [What should I know before I get started?](#what-should-i-know-before-i-get-started) * [Open Source Contribution Policy](#open-source-contribution-policy) -[How Can I Contribute?](#how-can-i-contribute) - * [Feature Requests](#feature-requests) - * [Bug Reports](#bug-reports) - * [Your First Code Contribution](#your-first-code-contribution) - * [Pull Requests](#pull-requests) +[How can I contribute?](#how-can-i-contribute) + * [Feature requests](#feature-requests) + * [Bug reports](#bug-reports) + * [Discuss with the team](#discuss-with-the-team) + * [Your first code contribution](#your-first-code-contribution) + * [Pull requests](#pull-requests) * [Translations](#translations) [Styleguides](#styleguides) - * [Git Branch Strategy](#git_branch_strategy) - * [Git Commit Messages](#git-commit-messages) - * [Coding Styleguide](#coding-styleguide) + * [Git branch strategy](#git-branch-strategy) + * [Git commit messages](#git-commit-messages) + * [Coding styleguide](#coding-styleguide) ## What should I know before I get started? ### Open Source Contribution Policy -[Version 0.3, 2015–11–18](https://medium.com/@jmaynard/a-contribution-policy-for-open-source-that-works-bfc4600c9d83#.i9ntbhmad) +**Source**: [Version 0.3, 2015–11–18](https://medium.com/@jmaynard/a-contribution-policy-for-open-source-that-works-bfc4600c9d83#.i9ntbhmad) #### Policy @@ -49,35 +50,35 @@ If we reject your contribution, it means only that we do not consider it suitabl * 0.3, 2011–11–19: Added “irrevocably” to “we can use” and changed “it” to “your contribution” in the “if rejected” section. Thanks to Patrick Maupin. -## How Can I Contribute? -### Feature Requests +## How can I contribute? +### Feature requests -We're always looking for suggestions to improve our application. If you have a suggestion for improving an existing feature, or would like to suggest a completely new feature for KeePassX Reboot, please use the Issues section or our [Google Groups](https://groups.google.com/forum/#!forum/keepassx-reboot) forum. +We're always looking for suggestions to improve our application. If you have a suggestion to improve an existing feature, or would like to suggest a completely new feature for KeePassXC, please use the [issue tracker on GitHub][issues-section]. For more general discussion, try using our [Google Groups][google-groups] forum. -### Bug Reports +### Bug reports -Our software isn't always perfect, but we strive to always improve our work. You may file bug reports in the Issues section. +Our software isn't always perfect, but we strive to always improve our work. You may file bug reports in the issue tracker. -Before submitting a Bug Report, check if the problem has already been reported. Please refrain from opening a duplicate issue. If you want to highlight a deficiency on an existing issue, simply add a comment. +Before submitting a bug report, check if the problem has already been reported. Please refrain from opening a duplicate issue. If you want to add further information to an existing issue, simply add a comment on that issue. -### Discuss with the Team +### Discuss with the team -You can talk to the KeePassX Reboot Team about Bugs, new feature, Issue and PullRequests at our [Google Groups](https://groups.google.com/forum/#!forum/keepassx-reboot) forum +As with feature requests, you can talk to the KeePassXC team about bugs, new features, other issues and pull requests on the dedicated issue tracker, using the [Google Groups][google-groups] forum, or in the IRC channel on Freenode (`#keepassxc-dev` on `irc.freenode.net`, or use a [webchat link](https://webchat.freenode.net/?channels=%23keepassxc-dev)). -### Your First Code Contribution +### Your first code contribution -Unsure where to begin contributing to KeePassX Reboot? You can start by looking through these `beginner` and `help-wanted` issues: +Unsure where to begin contributing to KeePassXC? You can start by looking through these `beginner` and `help-wanted` issues: -* [Beginner issues][beginner] - issues which should only require a few lines of code, and a test or two. -* [Help wanted issues][help-wanted] - issues which should be a bit more involved than `beginner` issues. +* [Beginner issues][beginner] – issues which should only require a few lines of code, and a test or two. +* ['Help wanted' issues][help-wanted] – issues which should be a bit more involved than `beginner` issues. -Both issue lists are sorted by total number of comments. While not perfect, number of comments is a reasonable proxy for impact a given change will have. +Both issue lists are sorted by total number of comments. While not perfect, looking at the number of comments on an issue can give a general idea of how much an impact a given change will have. -### Pull Requests +### Pull requests Along with our desire to hear your feedback and suggestions, we're also interested in accepting direct assistance in the form of code. -All pull requests must comply with the above requirements and with the [Styleguides](#styleguides). +All pull requests must comply with the above requirements and with the [styleguides](#styleguides). ### Translations @@ -86,19 +87,20 @@ Please join an existing language team or request a new one if there is none. ## Styleguides -### Git Branch Strategy +### Git branch strategy The Branch Strategy is based on [git-flow-lite](http://nvie.com/posts/a-successful-git-branching-model/). -* **master** -> always points to the last release published -* **develop** -> points to the next planned release, tested and reviewed code -* **feature/**[name] -> points to brand new feature in codebase, candidate for merge into develop (subject to rebase) +* **master** – points to the latest public release +* **develop** – points to the development of the next release, contains tested and reviewed code +* **feature/**[name] – points to a branch with a new feature, one which is candidate for merge into develop (subject to rebase) +* **hotfix/**[id]-[description] – points to a branch with a fix for a particular issue ID -### Git Commit Messages +### Git commit messages * Use the present tense ("Add feature" not "Added feature") -* Use the imperative mood ("Move cursor to..." not "Moves cursor to...") +* Use the imperative mood ("Move cursor to…" not "Moves cursor to…") * Limit the first line to 72 characters or less * Reference issues and pull requests liberally * When only changing documentation, include `[ci skip]` in the commit description @@ -114,21 +116,21 @@ The Branch Strategy is based on [git-flow-lite](http://nvie.com/posts/a-successf * :lock: `:lock:` when dealing with security -### Coding Styleguide +### Coding styleguide This project follows the [Qt Coding Style](https://wiki.qt.io/Qt_Coding_Style). All submissions are expected to follow this style. -In particular Code must follow the following specific rules: +In particular, code must stick to the following rules: -#### Naming Convention +#### Naming convention `lowerCamelCase` -For names made of only one word, the fist letter is lowercase. -For names made of multiple concatenated words, the first letter is lowercase and each subsequent concatenated word is capitalized. +For names made of only one word, the first letter should be lowercase. +For names made of multiple concatenated words, the first letter of the whole is lowercase, and the first letter of each subsequent word is capitalized. #### Indention -For C++ files (.cpp .h): 4 spaces -For Qt-UI files (.ui): 2 spaces +For **C++ files** (*.cpp .h*): 4 spaces +For **Qt-UI files** (*.ui*): 2 spaces #### Pointers ```c @@ -165,9 +167,8 @@ Use prefix: `m_*` Example: `m_variable` -#### GUI Widget names -Widget names must be related to the desired program behaviour. -Preferably end the name with the Widget Classname +#### GUI widget names +Widget names must be related to the desired program behavior, and preferably end with the widget's classname. Example: `` @@ -175,3 +176,5 @@ Example: `` [beginner]:https://github.com/keepassxreboot/keepassx/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner+label%3A%22help+wanted%22+sort%3Acomments-desc [help-wanted]:https://github.com/keepassxreboot/keepassx/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22+sort%3Acomments-desc +[issues-section]:https://github.com/keepassxreboot/keepassxc/issues +[google-groups]:https://groups.google.com/forum/#!forum/keepassx-reboot 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 6e3398c2d..9b19d161a 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 ecc275a28..46e2670ac 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -353,6 +353,12 @@ void Entry::setTitle(const QString& title) void Entry::setUrl(const QString& url) { + bool remove = url != m_attributes->value(EntryAttributes::URLKey) && + (m_attributes->value(EntryAttributes::RememberCmdExecAttr) == "1" || + m_attributes->value(EntryAttributes::RememberCmdExecAttr) == "0"); + if (remove) { + m_attributes->remove(EntryAttributes::RememberCmdExecAttr); + } m_attributes->set(EntryAttributes::URLKey, url, m_attributes->isProtected(EntryAttributes::URLKey)); } diff --git a/src/core/EntryAttributes.cpp b/src/core/EntryAttributes.cpp index 195a8f14a..b633cae32 100644 --- a/src/core/EntryAttributes.cpp +++ b/src/core/EntryAttributes.cpp @@ -24,6 +24,7 @@ const QString EntryAttributes::URLKey = "URL"; const QString EntryAttributes::NotesKey = "Notes"; const QStringList EntryAttributes::DefaultAttributes(QStringList() << TitleKey << UserNameKey << PasswordKey << URLKey << NotesKey); +const QString EntryAttributes::RememberCmdExecAttr = "_EXEC_CMD"; EntryAttributes::EntryAttributes(QObject* parent) : QObject(parent) diff --git a/src/core/EntryAttributes.h b/src/core/EntryAttributes.h index 1c0ddaaeb..211b6d483 100644 --- a/src/core/EntryAttributes.h +++ b/src/core/EntryAttributes.h @@ -52,6 +52,7 @@ public: static const QString URLKey; static const QString NotesKey; static const QStringList DefaultAttributes; + static const QString RememberCmdExecAttr; static bool isDefaultAttribute(const QString& key); Q_SIGNALS: diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 9b19d161a..46b15f5da 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 c823b3de6..f3535f9b0 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 46b15f5da..ec65ad90c 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 c0360a36c..01e152e2a 100644 --- a/src/core/EntrySearcher.cpp +++ b/src/core/EntrySearcher.cpp @@ -42,7 +42,11 @@ QList EntrySearcher::searchEntries(const QString& searchTerm, const Grou const QList children = group->children(); for (Group* childGroup : children) { if (childGroup->searchingEnabled() != Group::Disable) { - searchResult.append(searchEntries(searchTerm, childGroup, caseSensitivity)); + if (matchGroup(searchTerm, childGroup, caseSensitivity)) { + searchResult.append(childGroup->entriesRecursive()); + } else { + searchResult.append(searchEntries(searchTerm, childGroup, caseSensitivity)); + } } } @@ -69,3 +73,21 @@ bool EntrySearcher::wordMatch(const QString& word, Entry* entry, Qt::CaseSensiti entry->url().contains(word, caseSensitivity) || entry->notes().contains(word, caseSensitivity); } + +bool EntrySearcher::matchGroup(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity) +{ + const QStringList wordList = searchTerm.split(QRegExp("\\s"), QString::SkipEmptyParts); + for (const QString& word : wordList) { + if (!wordMatch(word, group, caseSensitivity)) { + return false; + } + } + + return true; +} + +bool EntrySearcher::wordMatch(const QString& word, const Group* group, Qt::CaseSensitivity caseSensitivity) +{ + return group->name().contains(word, caseSensitivity) || + group->notes().contains(word, caseSensitivity); +} diff --git a/src/core/EntrySearcher.h b/src/core/EntrySearcher.h index c7075dc9b..4e8d4eabe 100644 --- a/src/core/EntrySearcher.h +++ b/src/core/EntrySearcher.h @@ -33,6 +33,8 @@ private: QList searchEntries(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity); QList matchEntry(const QString& searchTerm, Entry* entry, Qt::CaseSensitivity caseSensitivity); bool wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity); + bool matchGroup(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity); + bool wordMatch(const QString& word, const Group* group, Qt::CaseSensitivity caseSensitivity); }; #endif // KEEPASSX_ENTRYSEARCHER_H diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index ec65ad90c..f1ab04109 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 58f03534d..000000000 --- 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 000000000..8e3285b1a --- /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 8e3285b1a..7c2a432c9 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 7c2a432c9..ef842a66a 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 ef842a66a..282b4db01 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 282b4db01..2e3dad3d6 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 2e3dad3d6..0fa60c43a 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 0fa60c43a..6090077ae 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 9f713b466..9d1ecabf4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ src/version.h.cmake export-subst .gitattributes export-ignore .gitignore export-ignore +.github export-ignore .travis.yml export-ignore .tx export-ignore snapcraft.yaml export-ignore 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 444009932..422e4da84 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 d9cbd5651..1c31e4bd5 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 1c31e4bd5..9b463902d 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 9b463902d..a76bf92e1 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 6090077ae..d1d4f69e4 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 d1d4f69e4..173079f17 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 173079f17..0784a4458 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 0784a4458..de5c723c9 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 a76bf92e1..58d9ba28e 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 58d9ba28e..a76bf92e1 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 a76bf92e1..2ed3ae93b 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 zcmZ?ry_3QK24)Nl3>pj!4AU4G7%UkW7#JBOz~akN7#M`OAs8&cz`$TNEsz*wIW<7N z$FcX&%>V!Y|NQy$|NsB{ZXeOiFU z#$$>gt4Cx1|8F%dfEZ*s)nC2Gq4(enh`+&ccvPp~*>bACMz3Sc@?1*d@aNB;plJO6 z|KIaxSaro=kueaYn(2aa0DheFN4P$B+|g-GH~R8qtOWxlgm&d6D@Lx2tE2;-}H$5VI_Yk~sYF^XK2+fBygfzvkjrtEv7ble~y= z64`1@CwrSr@;ZF?B%V0@@Z(cWSA&kZzK*?-h=Gi_rIIr^4#6>*2#(1_=jEW71V>sT zDAJ%g5S;fQaR|zm369Ia2%Oo#`7quQ5}9BTLJ@_YX))FR|8F_fpBQ8@#ZRrWE_zuu zdHL|ik01a3{QLj^|B7=Pl{&0+`&_K11`y*OO4OK5^49HhS#@DE#4ng->Cc}(KmYvN zH)oQLxxRs;sfdBBn5jHC@4<^xP!5FTLs$fYMZl2>jWlp31Z6yMd_uw=#D$ci5Ry7& zDUpGJ8Hb>JNO2tg`}6nz|Nkq`ZBp*E(d~7zp6XAedni$6HpyGN*J+reUzk9wDF^ioJZ{D(D7V-@`I^T+Ss zzyJULzx?b5jV?RAK4~CZH!=!Ugci!; zP>b9+1Q(%y|NZ~B{Oksm4lBK0XRFD6lt&JcCYVk1*6MaxaB3A1ai|ZELr`T1?p}a; z7@#I7G!sI~Qc$B56o;VL1GR4;y$Wd5fuakPGr^4*Cs6sB0Lp<&AtWebVUbBrGxh&} zi%CAjAoGddsvS1n`=>$t{r~^}y*CeQcG;Ux^j7P%iC&Tgt`CXIhoDv{tO!+Vx6tW! zw3_Tkj2oz>#&m+GcDKXwbHtRT2Jo^JoDU)OAt>*GDnoF)6kL9S+NH3_gtcs-MJOo4 zfJ#(w1qkl=fkY6UKv0x|q7EztVvri2|NmP|^d<(GPw-N0w?>V_Lz zfhtC5_W~@E;Jg$Rlc4AVWxqry1H?`MwM9X32+4Thz9KkJf^#D|@rE4FREfiO(&F&{ z|Nj@CTv2Pc((QJ%oa95r$RW}wlYS5V9_KArcH@adSl_|`-nRgirOT5*1t}=|AqrA( z1VUpE#D+9zKxHX38bQ55P{jx;Q6XIlaFzqdCw1C178AUPLFWCQs%=)?`=&zt4K7O$ zX?EC}_j{_fSw$}C)2eNjX8oSTSWR^`#=UOFeQrmH>*MR%8$dHPzJ^^}3k%d)|C;2a+B!%TmZ# zTYhaZXspf75Hi*V8m9pbw1Im1p!^0JJB3CgI1WK@q-+D$ZXV@ghn+kL6YLI1tku*U)?KG zqp8$vDp;lFH8b+}uiyXw5Y;aI|NsBvch6j=h3ItHQ$2o&F;J`BHh6y0$FHAo#o^z7 ze_wui6Ot6EXJM#nts`t8Bj=$FDoa7#LQqd1+(v~&8nj{r6CxU=prVyP6yb_GxcLA7=6xQ-AhTX~m1gtqz0iF4@87>mPp%bjsIS@5 zUbMb;(!trke*OOQ`wwQC#uWMg|NrZcuYG1lX|>xBV>Q*)C^wtsuC4y}_uv1&pm{V* z7ykM8_r{ak?x8-0HYSQznnJqL%3;Rf&IC9DA>I7t$c<7+e1fJcpb-ekgwXmB7J-nV zZcsIt0L^tEHZ|hV-Mo)5GV66yX*Pq$;m@BxA;V<?)mtrz zv6{MSEc!g8YIVByPWku$-|t_)F&+8i$B$ose;>Jc!rI-=*xpReLQP0VN;B0OoDaeA z25OyxN4~)g8dz}(8l(Z|Mo>2&ocCZ1P;LY@NP2ATD^ zsWh2&?}6q+h}VC?!Ove15n{;yfB*mf^*d%!hDx&;F;-JojcJdYY=i!i)2ne6q(6TA z`1bq9hJ9N#O?6G3Ev3wr1vMn}ik(4m2%Z226{O(C4LtT>^&vbTf@2R{6T%}CTqS~H z51Io(RU)N15EB3Y|C{x?5ra&7T$P(lyY@`R<#l3W52EJp@4x^4{YzO^px9tcjMdat zquXJx-{o`+cgF%UX)%52Y(*UvQx{7yQw0GH3Dag@NUs7sdjZLfpz;&iKn2x=kbDRl zqyaTPK_Z~M2c0|w_X9!VkO~k~go0(EF$s!WNQB@aaYg?B|7JaI#30jdNE}Y0JPyI* zH2-o}SIE^H5MwoU)u=aHxK9mx|M|oJf7mO-zkmOJ`0=T#vtH9!N6*1T#0WI;WHTuY zJlYNENn6%>cAX5B;~scP-IEgh8SLr5GJt*sHS)G!A{AW^mx=}7#_ z%zNCVs&!@^S@iG!zhA$w)ZssV{P_L%&(%ja9erI)?JX27HH37e6ax)F18v}r1+*-M zG)*B{5k|rqG~fb9#y4?&r9Hpvt zlp2hdpI%F?I9zgSwNitzRF#flhof1y8-6!XOV+T%QKQB3+=ENFeDVGJ_wT=dY}mWS z(ALDn(L&lxSx8q(JJS{vhmhVqdh8`Zq7Kr&0o8b*{scJoKn(Ek40xOdlKnsmKvg0{ zocwr$g zXx8P5)iG2Obe5~tZ{5)g%7i#P0UCe!`YyFNQ_ECO$KFWPL|)8X!G2aWC?A4yAY@@8 zbZiFRCh6G0-DC2=9EkLCxtbYjU^20JAs4^s~T?&aYOcE4_&ZeE{$h^x{ zr`bxpTqA3F$(v7aK>hf?|9||%;c;9sMM(U|&maFm%W3|<{`4kmMX7j&x=ypTIVc*@ z-Gk{uGDM8p9kiOQPTxBVibHH}gH(oB9^J6oHU|Ns9Nj!~uGzkk2|_Mx!8RMQ01 zGZZzJ6ETo6?+AqFLvW)s30fb5vmdm30g5+pG=kbOh?)=_!{AaBBo3aw2Y2>iSqKuJ zAQC1*POK^@n6^8@k$H!+M!l(0jsE6~JO2Ow|M$;ds+XU*(!r0PKmLLj*=)M7U8z=I zqu$KC!x`={iWyFt4d&L}?vGwS#$S-$dVbH*&(+A*OvPG9)KFI1QO$Wt0wNAUJ1sy} zB52G85{GbdDY%OWiYsuN22`$sb0Z`Nf=Ezgf~GEDBj4b~QRGA#T7)Yon6^1WkXgHv zZj+^Sg~pmQn?Qxz?|*d2fyhw`TI}{4RCKR7vr(!-L$}G&tlbIXHVR3zb|lQ+H2(MjaYN?P zLg{jKN}>*;*R0(^x?HVrRpr0`|8Ql(@87@w`1AAB^>b#tv@>%)&vy|X7O>Zq8zT1%KI3TlXJXWK&yQqW8jC@Vtq9*CO=DLlbh4#Wn<8)#V+ zS{;bUji4BVY=DAfLQvF!3s?{XC2k=lB=Mwzf=R2bL9>-qsmg}4p!Jrx+H9C1O;eFy z;1Pfg=e9_dsTwp}nY7xHhfG`TBuiDR);EA=d~u90{rK_Y@4vqf-#zt-3^1}W)wVYj zGnE%Lmb054<+?oC1%yB=X&^leXw-oVSSTCZtbvZufXY(vehXOM166vUNCU?$IJ!XP zD!3FSK9F^l%j&vacR7Qf^?Z`_Oc>8^*#QOrm5_o-+%u7|ChD2P`XUj zq{YU##hQ3z+G;CRsvIyS?ANbf|Nr5v|2ECojztYSnRZUe_2ymgA41Hrw1q*9U83<)Vz6%^EK z4EA3;1j>Ze7|f<_ltLOed#@f)t0O_CK z&p*EkO^Vbu)6;h_6*E&1P!~6<@_?0}kZ1&FKS;*{+&~4*`+}ko6lvfb2r4r{)4-7W z5JEy?5}fNGv%hFd5yL*Kg1i0uFx;urMeFe*X%Y7Nu07Wde#r zE8}K@$l9pMO1wz1cPD7o0oJ)*xUW9_{IYE8Y9&22Yd3p&OLbvAX=x{QXqN&MUEnwb z_2@y-2#z|?I1Q|U3fq_uiZsZU3-D+SINl&FRX7Q1+<7&loFgG`%k#0%xar^i5ciQ^h=0%gL#e;&Pm z<`Ws9XJKgQWF~H|$ge79RO1QGgrL|1)rX*%1VtdYTm{V^!WZp>b0D~92q{h>%Q4^) zh{S+TA%aJ{L0t+EgSaRnSimYMoW2X1i=o@ND5=Q-(y}>q_l$O(k#>!daib-EWZG=4 zSf*jvXmR7|ZBQP@ZQrk7zutZS*gtQIlAfB4yMwHSs<5uKjEjads40^Kk48w;fifPX zECtnypd~e+JPC?Aq*@U)#0H8|P~HR0VZhrokO@Oj?FW{EH)22{1fz(c0tJQdKfeF_ zOVk>B+`&OxnSX!(egE;@XF`x-nTAoLB_3qlXsKCkAX}=k^U@yB`~q_O2Ne@I1|Dn5R@$mMid?e3JRcM;9o?ov4{Ac{v_n&lhTz{ zvLz};4HmeNaigVPor!Rs^yEFDA>Tj0|KKWIK&uFT{d)QJZB|8|ioS-mhrN`AvY@7f zQlJ4W_L5ydjZ$!Af*7E(6x^Qxm8GCDZ%|hQ-k^cjgy7LMQ2!8=*}&u6AO<)lL9G^O zuOHM)1lNtQ+z5_UJW)hYNrv+(ULaMd2#!E=qXu+j++bl)Zz_^2 zUA3nE-~WIA{^A?*1&^$KUbSPLg06~j1-E0s zxe+p03Z6nljzdsW2D~BzBnZlc&=^aEv`E1VqCoi#oC7hNH6SU1afM3(I3MB)yul;> z|NsB}R}ab*DI3pX zO;$yolCG+ahl8|*ijc09l&y;Mq6A1BLP&5;2rT80fgCr~d|NsBq%Lmr2_UaWnhIM9eWK?gi zUuz(@gNn7bn3;m0ri4Xjkn4&RNbEsK7f{Ov z6mO912QEUvt1zI=8kB9Mut)=qvN^$KFTkUw(6SU<0fHJaILcC3-AGofQZ*k!8XurV zaA1sUvW|>&@&Et-XYZanb$O_iX&Tm=LXc6NnQpbAP?ki-f?P-!grLcve||l1?ufpP zv8la&@=#&zcE<=Ub-QkAP}p*aw54F34>0ZE+7&TjY;5I5j1TGDpz5(BBVG4MJcQxg_NS8 zgH1qXCNwL83ruhXf&}652Fi-y2n4NGBq^fsD5fk9A>*0<{{Or4{C@YgzTipW)-4W3 zwPr@OX4WkZA(O&;xAot9`2e&&jHEGxYk_0P+6uxaoXOQ z#25}*fdBXRv(K**^HLRcRP4Q+IKL!%MY zs{my@=)O@%xeCs5D9svha}*SZu%Z>zjsZ_Rf&2K7mMSPK#yf$wUx3O_5QD7p6FEvL zi$hR9`uDGn&0X3Rdg8gVGWm+i#p>#1+UjN6%Ejt3`HJGXGTN2;U0Zq}N3-G0DflG+ z|NsB+)nnUMC$&;-gKA^ZY)PX!vn}VggR?o#ISGh~kTJEFU*43rRx9eN+Iu=FTWN`# zD+;KI8&!LPA`ns&u1IlNkpk*ZfXdHgSJ0FbQqvTi4?)LYLbD$z_P|3mU!4A6}B(e)|9a|IKH&%o?oa3zS8&BqOFq-+Xop zG%NV~H?HAOh<88{_~-YluWx&1O;j<^uy(W4v@?*f09At;Nmi~aQrwm&gIhBxU=oxM zLA4@i?j9C_;A#+(EunD;ntlScMZsAS!2lPk;K~pbZ=eW7DN&)jxIwXpvMLeO$pA+t zxiLvrK7{_Z?S;LnqXi#NrSY>QbWrPPo z6!4}BRN7bnqM8tO1v$pb32t1H6|TR6{~D zA-F#Q9qk6sB7!>mpiBskLl6U$Gr_eYhyluqpt(eF-3W;XO31{bKfnJFOwh<73h755 zyLntDS5C833lw#gMg{~?m61lNmTaE<$y@j)Wzj5xaDV;$_2>7W%a1O<{qz=;;)vLL z1t~(`e0x88)dDSZJyUxNQ&%fVOJ!kQX>l_J`{~hcD^lDb)!_0Jw-w3Y5*5^xafOzm z$Kw8KQ+tia~3T zNxVe?QhI}`Qyhy6A+Zf1K?j8Y0~Mjumd;i-PzObzik5NC=Nm8Dma6I3Q}<11La1L z2)smvWIKuU~pU-KZ*uH z2qX*wH;kr)0SE!7(F{n}5E#v91CYpRT)2(KDamn4MzKSN7QFF38f(~NjoRUi-4?u2 zGi3VpPu#>lPYC*#q;#0boxmm{d>1@aRT_1rKR z!hbXzXc7w}xwIL{eji?i15L8VXb_Etzz__9(TZ}kG#IKOKw*tcq4vR=*++BqP)*N+ zHARhv=V%C!9|9y6vykEoLc%RY;yN-!=1S?z=xttbkhkqA<0?l_Y z)3Cb-O);EHoUR0?r6`U>(*@>&6yT3Rurkt|2X-gOWJgp7fW>hc1yT>O3z>v#fe1P- zjR!M8im@9`I5oyQE`t~XbqUx&2peK7gpEexmO`=-$tZ~S1Pl>~ILHlfui;=2Wg}EC zk{h5R(5wfx4wO$prokNpXTZV}tPEKTlnv5_WFVA_mabq5pt3M-yc0N_5q7~H31>iU zfkX#XJ3>AF!~-$}ss(N%JW0R|fh&MBKz6}oQG5**1R0AW?g$fcf~Gx)EW$t}+aXf8 zNT?R5MF`Wt;$U0AQeZAp$bbbg5)4Q^NF_)X#>V3kkTRH=5M^NR;7Xq$lR?Usf$asE zkM1dC1CiKZjiB@fQV+HQ!~hwEWIIOs!R8FGrAQ_t%mI~GU|D!{L(K;Zf(=hVmx81v zh-kAN5mdm)NJP6As5;-aPSrJ&#fDL@#At{v(ykO^=r65weJWG2`U zw2TP~NsyL!sCJ0Mpdz3ojUo<_1(A?&fLjD{43rBp6Ql+#2r>#J4skrh86Y+|#6S#i z0D>9ukPrrw$X0{2gEgYYCM+?4rNDMAjdxmxPy;a&A`522&4DV0NTCG?Ob}!aSQn_Y zfe1pl5H?&FiXh0d5W_**(E}eA{z&3*C&3v=+Tme{$^h92vk1fn$-=dR7;r%l1Duc` zZU%9YLK`B8odm@W$S6!%upyw-2$KT)6cqAcmqQ!|i5dtAavoR;!T>8kNg)t%WQW1s z3}S$s0kITQ7epN760ot3OQDVdi9kXdA`T%z7J*EI$fD_ma}l@s7|?0$Btz9F*jsHiA?_HNtWth>K7O@)RWVLbD`X z3Y>h|AAQG+}RWUSSfXsoK3Gy#U zF^CIxA&3Do5ULT03sMI44nzS+Jwy;}J4g`1#ncNDf#&@LP%(~d4%iA%n*qdt7m08N zim@Pw<(oNH5q)AO<9xfVq%Jhbu-l2f_xq6QmOA60j98H-IgID1(WE zq5xtAR0JGWU`K-TGFT9l;8E1TybLl5WEZF?0BZp;z*?X-f*gY|5EPgYS+Hqv7a}u2 zY_RN7kY1QY5M6LzL%1N@!C4GsHOOlqy&x&LE;s|M3?v0*VEEbztsnwB6{HSqDnn2rUqo zKr98TSq2I#kVVj72MfZy0MZMI7mx`cT_9(JWudl!#6eoX;z+i`5&$S{q3Y3Op4F3ZICVi)!j!>G14jx>5LpdW0mvw*Gr(a8 zsv<$z2do982I5~Z7g;-m4RQmL?Vv&!lt@4Zf}}8Xfei%7g6u-g>|pAqP1=$GUg1v)q3`h`S5!f%F5JOl2G83c#ECpjA z=><6zDH(&!f#xr;AyD@~k`07}Iv8X!L>7?&A=<$jA)yDc5z0nrMD`2Z@34FU_Al5M zAY~xKkrE8ZK$tn;a6obf*j})cKpH_RVZH{N22u%1s4z!@Rl>41B#s~?*c?zGfGq+m zLvkBL7H%@gG=y~^OTmVK+y<8dGazn2WNnZjDC9vRAT}s@fC35}BOr4?7J*_4>;SOi z!Ql_$!qtH60u@srLm*B983j@dQU+HKG74N2qsoG8frx<20XYVor$E|4E&&?|%0Y1L zs7)r2cCZwP0SYTQ0E8K_wC>s6pyMhCsA{+yjeih?_xf1KAF;6kP;b zK!LP_90M{6qzu{_1aZMqa5sR>0a*lPz!NzrDS{jWbs?yR1=$5M0c0SA4e|~oJ;37& z9DShRf^|PZhC^Hh(F;l>pmYe*3vvcHFhNov8&Ta1wg?h;5CvdIg495)14j_l9JmzN zd{Eee0u2<-Ag6+4L2QU#h!&8;pyt3V0y`e09%LP;SOBR3IT93TU^dJUh}95fFkN5; z5TiiB2;o9Vh`k^ZtP$!wutu;TED%6;LG1-O669=<6xfhupxOr(s-U6?EDK5=5MO|~ zAYF*i2BlAs2uuMeAfGK44jhvEX7596R7}gP8#HKEyh3Btjw%p#W?M)M{AxgCh|X0$>KnMrg1= zaw}L1$X;mpfFc*95}bO$3}|@*iDj@#P(*{h0Cp1CB_Llw3WjNtN`LQSkQq|6od_O13ckD3pC?g43sc^nihAT?kQ!V(fhJ46Z;Ss-zc z0#G)AI1+3-#C(veK*<%9%)o4jMWEIlhzrUZpm+!Ch5HT^3m~V0-3$&-kOx5`V9&x# z28RI5d0_W|l)+`e>cK{Vy#q1_%z%al*eH-Lg!e&-79+u^AKYzxc*-~a@PfE^4m1mZ9-8|*4*_&_`e&Iq7T z1xtaP2Z}h52_QBoULb6+r64V!JPuWl5)_~)gSi}LBgAl!GEfo%MH$Ezu**RXhC2yl zI5^C~F%FgjSpl{V zNG~Wb!D$tgK|rnog&x#Euy;VNMWi~AOCaWe-2)0?P}+k;A1IVyBB0a=b_3L@;6fYX zS+Fla&H%d#WDeLOh_fL%0%9h_gHRF_=1|)~B49(nzJS>XHXJGiPB0*Mf?N*L1@b1y zCEy?h`xjiiLtO}R4A=`GWnf=GO$OTnb{HgJK}La$1&PA~3StFZ1nd`3pn*g{`4OZZ zY&a-X;b{Pz)1grY4R)}BAlt#Zz*^wWfH(}q1%(4dC71-61C2+pEQk#X1hA_>&HyJT zs0bv8V1Wa17@P!!JV*)@o)C=?1EGNf_723sAdi5!5C?$51R@Sn4|P9MaKQouWDZCd zC{2RR2Rjv%PGDIE>}9awpppS>8pH}{G=Xy^DCa`l3@IzX%Ajrlc@yLX5Eqn$KnV=w z9gx?+euuadtQ}$|I6c6WL5%{L0J0Yv=5QZ@#KBPmVt}tg1j&Mv8N`K%&;vOhtQ~F| z*o7cnAQyrvEJ)yhG=kg)3T-GGtN>INK@$VW9B_Ogg)qo5;D`hH2$UW`RzvK9>4KOK zAwl+n901k=G9S!W=ayG~WaQJ{y2*^_)L2%%MT?n=a@JTA?ARb2XX^6*uiQ*T0oHqD*C|T1Mv~Wg;2#{ zH4uk^#GzpeaR%565Wj#57LXt)34uZnWF5#cpbQHU2l*Xr4nzt>Lac)eg1I1NV2eP` z1~DM!LrAb=z=9xIutt#kp(0SPLE{(dey{_eVF+b|q64G_9G+lHL7@Rs4+?fr+6Kvj z0tCW_=0`|U2a%wR25~3I7huI8F2n>l2@(ed3#b4Au|c+jECL4+NFz9+L5=}?1Y{T7 zG4SjU5`p*{oF75vfSnCyfD8dKK$#017oa2!atSC&K->@33yNPb1Ed(F9pVg7Ne;0H z>=KYhkmJGb1jQXFG(g6Jw1dMIq#YEwpwtLrgPa7`1!h3=2*@Ik`5>2r6+^9nmEaFGyu{C ziWHD6D0m?b18ao11gsvEo+0H7#6}PauH!)3Az=uLZd3z7$qZ~PNCCLWh4}(x5jb{0 zW`blvkq$8mOo9RyA_d}ta~a4nP%U6H;qHXxeR#Zpq71GQ>~T;WfdUg8N?<9F`(Y+P z*dR3!DR^u`JP0De?txhZb_pn)AvQw78RR376`&XgM=QvYh@b=Og<1p-TafQS!4AsB zAag*1phy9!fwG}okollw3APbr1tg(D`~qTw6u=z`@)XFmU~@nwfC3O?AgH(lv%!KO z5pZ?@#Tv57P-lY*e~=m|7o-ss3(!b}WId1-FzpaFsKfvVI4BBWQXsvMC5XB%85<4I@AbUY+8xba8H-NN& zq(F+H0RnRaniR-`$f1e|5U>|OTyT;DIR+A;a5Eup21gJ?87QBEy#Tcuq7qJm88Byn z91Pb5RSyn2kozGjAtcxtAO#=>*zsU1K<)vRmY~>&+6#6GNIN*LK@5-ra43PbfV~MV zPe2Aj1R=VRNr*VecCd9&SHajwW|jJ{0l68X5~KiX3&>KiMvyGX&0ukmGN_FR>mbUYu?d%j6#@{wAdR2| z19k@7D3Ay!kRh@lb3ocbYT&xSQlQ9%iXa&a&RJk7Xk>w$1TqBSHLxty7LZ3ExeO91 zPzNK)f};RpJ4ho$8HfZ28rUTuLm(~zSp;$|$Wo9Ru%{qW5L+PTLv(=@!(0UsfskMu zL8UR63%3-+0BeC*=eRUs?wOU>Uf%u>1J_>Oo^Wg)veh7GgPj3R6(GkzQZh7nAyQyl zAl8A@LtP6B0Vo$_DXOPH8bM(WauSFQG8tqhNF_)kMv;Xmoj{I<+6dJJ5`kC>Cqd?b z%!l{|WD7_HT|Gz~6sicf!9>8JifjusNYPDzm=AIQ%ohkRgGE5W1!90517bjY2dd~n zdf^U+*afu(>K?FTpz1-!f}9F-7*svjScumkA`mOUi3Aj$U_r;F2_-w*|NsBL^3oQl zrQoRl|NsBo(MLIfUJYN5J?TlD7Xi~x}YjC)Id~1WTCEw1T4rIU=crWfKE zXfB2t0#dvz;o-X{58pj;T9$yz%TCJ@CLEmy4Je2?%MwmMzV!9iH>aiebfs>t{`%`1 z#E}s4+RNLio2wyiTX|{A>o0F1;t$_FftLVq_kfha{Q|KUnOv3#az9)FJS1^2Ah8CD zYg8?c;JEw$|35VJmL(jxdjcLiD=%$MBTpfL(&37o4I}H&t)Fz7OOQumY#02_P@L2gUWuOIr|LUYc;=9>U8g zaSb*f8rQIZD&E-vaSU=m9k_eq^_RCHYw}PsEXdx(tvB|4{q+qL@?cj@I680Z^?i_J zfQW0De?caLbb*s7CSxf$48g8~s$81j4Al#k0$YK|*C3S;aY$-Og!t~jy%V5p3l2|c zT!1x#%mitH1_(6hz#>jd6H+%-Bj?1|U*1A|4T-zd&0tw@kvIIz?g~l}~{XqNzDo7D70fz~s=s+$dKym%ERH~~1vv~J0wB{sg0PAM z9JbK721OPmF2LGBTrdOVI|u`;3!)vC0GweAC$MKh=7Y_2UJ4B&aOwiN0VIVm6Xe;g zH}*lY45X-8d1(v8w2-xVTW{=xgd3~^0$Dxx%t}bGfBp3>ZF9BL(uAf1lR*sr_j>vsrhk={{4k&Og>j+t!j}%`ZZ$hj{-2^fpq!{KAP+Y@3yDY(ZX(Ecr zFf&g-z63V89i(?zA|hUf`>!q+V931i`JKM49assDeNYKG^iZd)xfr1?t zBcM=)g#1!OodSz5aMVC$!EEHx8ywfpOTnoEEDp`l@NfVt0A&Yo=7Kv05~QF^3N;xN zu%G}z&DW5!?*IS)2kxB!)v6$4L81EaJ*WbG`0fd~kj#e^*O0=>X=!5f{we?e|8Lqq z1(eP2op4&32+8b_G8tsr-4hTkkb)gzOUcd-M0`P9wJZTr96{2O)3QWoqWI`ge zLR<J&yR3K^~4e1F-=Ywrr1xbfaVADXw6ga3s*$Si`>*ZaQuRjB1i!|5rT6rSOnxkkYm7x!$cB6B2dMk z2!+KCC@vrw1X2xw#9{dtR4gw|eEsDu$ZcS~kdO~qlkWs}GbFB^!KEU&7EVM?`Kg<$ zLGAOk`Az$w1-KJ9QXuI88bsiX0P2I>J>k4G5z?Fk8wK(Of(y!-lexPuB z2TPNn4A>5-Xb@#MtZamr2@tE{<~uC~r6q_<;Q87K?qyJs2vPuPPdYD4m~adf*RQ|4 z1?LM;?FMo=sP_a4Mrc5RErkZuT2S8vDPJc*+T@TPjq@^4JLmP6w;%^EO>|xgYOFqd z{{$Qbi4WgDftCQ!^aE~af!H9sAR*ucZ+(Gd#~Ex0D3HNv4;eB|NsB>m$#q*1X}^Ah#~4vKfVNs zaZqrrfi}M(r}`~?dCG;rER(gjJAP!B@H;UuUD3JQNvFBRS+17!_x*A0hI}ma0Zo- zV3&ZUKn6k*C#Yrt_pd>202>Q(704Wr8jxL}LJ4dySQg|AP(A|N0x}R(&4Jtu@^vC8 zi-FWb^nx-MD9k~o!K6TjK=i`YfYpPQfz-gf3^EpE7rfjCnGZ4$BnUDDtbHj`g8>u; zU@pj2AO_eYU^8J&8gRJ;il&gYpz)KhzrLkzt^vgnEEYh9gPK5zAVFv>fGmY@VQzyK zHO?U0L6rqa7u-o;&w@li&Vcv=9C{#eY;6yaGH_f&8DM{b!vtJFLG^+{7-RyxzU}}! z3FJ-?16Kb3|NkFe8G(u>Pyq-EZAc~pl?<>f4>k}K6d>=do$CpYV!vY|WK(xS30H+X0>H>!_*dk~wK+->01QgUD zt3hD}W`klMtPBzeATBgaK!FD1f`S5+x*#e+B-EQAzktkf1~EY55L-Z2z?=c;g_Z7X zM`!^Vn*eE>ef{+raUjE?nF8uOu)QGdASs9y zARAF+L0Ji8I9M6TYEXQEoB>h~ay%&fK`9*MY-pN6vJPSe#1M!|5D5wsuz?^$Kn8+> z5f=3z#ZVJK=0n^D4m7Y|K*}ImKrsmRCWs3%78+krE?5K)d*GwmW7%IvJ2!w zkiDP)2j?bGE&vHa)IhXD*kJ3x`4r+>ke5M86r>$gq(F5+6oW{x*Fd@;9zk;t#72-B zu$dr-K@u&+gA|+4$OXF)loG(shGZp>GKdRd zfd+COSOgr2;9vx~791=P=Yh=u$%3_j41~A`L57Gij;UJ$vWFgjpO@NvPG6ZZI zBq+c^3ep7;2dRWu0U}{31gsw7LAWw70~#ivR0pvgqzhsi$W^QXuUh z$Ag>-atX`|kT^IPAqhu#=!_AR=J@!bL#NhKPe51I|#;WB^hF5rmKs zW5F(gO2G|>C;&MR9CUD%5C+^vh%#7t2nrvF2sq1t0v{4?FcNADL_0(!vQr@<(6|6a zG$<~>3P3>$cN@qJU{}G^gAzYT5SolZ2^*{!>;Q1X3FZK>2uK+uPND9BgaF91pjZF} zAlPI`js~R)h~Z#|K@A7H3Zfn4HkblX2*9L3D#2`!fp7-c5J*`H)ee?I*aGGv%!HW? z5`nrC?0&EZLAt;Uh%A^33O6Kg!W4szg@q?b4aj!5AlSbU5s*<3OF@AKO0H0kfYm@v z2D2fiL1m#vf#L<^Uy!RnY=jzc@PZ5nhXAO+2N?oU1BxTC<3ZsM3PX?-m<>rP5ch!0 z1bYX<1vwAQg^5Gm4{-@N1wuqXBuEX|E)WB(3#<&}9f%0rMsOg5JOvR4D?@f0h>hw1 zsQGX~h*40DU`t_6h163Z^L6GktB4F(ZE?5>4Wgy$ZvLFUT9A*V5?w}&j5*};m$lDOOqG9QxOP?I0XIwX@p))PRBoBOH*`z>EbM1!F@rqWBJMKGb#y8xi(C^17!00ja#^&+_7 zzya9>)(&F8EQL58WCb{Upjsf>5e^2WUZ^gx2@quvy-*V3JeaW{Hz2Vg=0kLW*bphO z7Lax%Ga<4dK}d@l?goe&h(;(0PIX{IAl89(fx-dK0GWoV5mN*lJ|M;L@#p{l|3j1^ zlMs8G!4o%-k(RH&zBM110ugs!2AYXRHsRrW&_bHfwfS%bU?m@N- zJXa191bZ129grvkn+XyJhZRUKWT+GF!j+e{q;0MVU7HV?5CM<(g48>KCKfv&qb?A| zkiqzaUs~jpumAB zgUlL0hay4o1#*n@GSC=1vST2MS6;w(2IxkBs-Pr+2A#hnZk$eUo(Pz5156heYy5LrEPT!Zz33~`1i zUXC(a{`$+?P;go2yetvTBk}z_q7su0bxY{h!ipjPZE&GMV5lumm-CDmxF3&h(=JpK0Y5g zz(LM~c?VMdK&nx&I4HV9*A~Eo2t1DlZIQwg2AX z8jO$x2DTAJF~~-kO0X=(dXdn_8NFz)QHa2oK0oM*^V8k`3v_amv0B$G0 zgE|Io{&G+|8SZ|FX`s9h3U-h$oR%ZT+gLBZb zq|%)o$X@tq@M^58IVd$b3m)zj?afCW+Z!&O)K4r(xC%2WtW3Y13NVdx$b-T z{wY`xG`0OdOOl!QyCTI7kgj zT1BKxBpZ=a0wk=kk|3YLmUm)RfGh}B3@Tmm#5G(!IPReCM+zvg@1U6iBnyuPxO(Ku z9bCXZU3qB>B$h$uK>WBIy%V?f#(r3b7vw^aN=OVYOLSS5lm=cA3UM29wgok1*X4uL z2H0WC6DK0oePI8B90^jeJP91R5H5&>azW7n)(iF&L=YU0AT=OoKpYGr;SK|3XBTMv zg7iXkK@`IS1mapW667S1OF+6ni3emmv^5J+2?;0{uw$G-en&Lj!C4hi;Q#;sAEFpy zFPKb%CmXP0kUNQqYfv)}-p*g12=WftVIUWR>L9QPQZhbp?*vHAvcxk_KwAL7vHS$w z(g3T3=mmus$N{B0J0V_!HLH?fK?jmWVuJ%5tQWHofcMToQ3*2{xgKA6X$!B)&i-C_+K;1!gaUlc2xovI z8sbinkHFCl<|3zLa2$c01hK+-c@n%?JK^|z7jTCKZXif8)F`lN%M)LJc?Xe&W*jW_ zHAp4M!LS|^L_IVxK>-2^^X3CnCPG?Ppg;pPM_raB!5dCskANHrw{Gs4RglmCnG6aF zkS$;@B+fkxT6qqx!QVneAYltmNRV(&a)zaUP<(-EQiMk!&b#*VHpF~bK@GAUZZgR6 zXIDWC1SJNLX&^U)tOJ<_%H~jqf!qf2pbIE+;f@430FtwyTA+CiWQ#K>1Rx5)Tu_uj zHR8$FprQ}t42Vi-@`D(TtP2!_U<1=O*T9>^;C5*N$UR8yVOUN9IUb}Kmdn73^TFQ1 zXq$mz6XbVr$e?rqKvf>tkMOj2?d5GqZSI0v4ucaC$QST3_Tl>{X2*{Bj^FiSW3ME)d0ExRmA`v77jY^2I5E2wGAoqjZ0||C;i2mF0;K z-#>+01Zp&b$K>H6n2gt7-a@k&QV#-C8EC`<6#GeG;IR};1rU*~H}->q4pcfpaw5o& z@S^Ph|Np3k2`s+AX%g9MU~@ovL7@sx&)~=ci9k~bD3^h>gZ&O-K;j)zq`=}G66uK$ zEl`ypjbL*?sR9;y;B&DWJ=0Ejkq8L~I0-H@ zp~)T+GT=dGXmJf!24QTyu^*h4Al|{u*AS0DA{tbLA~))*0mN7c`SsT~ zuo|d#XWVrbs1O5{lOSg!M+(Fo2njU}6g3c6ffOTG)(7vMfVV)P&02(FP{2aWfixJv zp$$$jpojo5Kxq<^ia;&_<#$l@fkeP;kWtWV3rZM}3K}E}(hCv+>jD`9s@6en16cu6 zfL33-EC;niT$Ux4?n1OrUVnKPwhmMpgEWFngP07m-FaEkD$sloBBLF=cLLUJ1Q`W( z0LXbDH$xSJHupiid;qQ(y#4~}vqKC7nU7j1mF@y1RA`9}b|ECVAmI$jR-kYLM+zi6 zLr9Q;@DM}JgRpD}%I2WjArTUxAY(!55ra$r|NjRC3&_8qM2qaxGfyr-Y=LAVkh7P8 zqY~soFau;GD6&Atg32pU83I-Y&aL1O28R+jv4M4gd<1p?*kpLMf|0KgNfe~UWjRU} z6SfXiCxT4|IRg|3Ape400u3UNE|3+VyaP%$pfCiv2gG)PcotO6fr16(JCMIX-Uk&& z2%|vGhA9Ra4vqql$;fF2o-#4o+Yl`v8?DvgKq^7$0Tkv4|AIvz z6#>|5Aj3iB13XhdeE$@bk7cS zz)pg=4eWA=AS7Tx?gu*+Tt0yP3koq%V1nEOigB1vQR^>|MzEO>bFj_8f=mOs3T!^O z9*4OE4PM3Y20&u>fU*xF9vl(93X$6(EO!TUPLv z540%@wiK)p5?PQK2S*3kYIvkW?FBg-5)L3&K^*`O5U>m3fdd57w9F*O_zOFYKI7vui=3YR)aj$18J#(v_n!C zG-ROWfFcFt28i20jVMT1fz*Rkg2D&nW{CNq6a|VGkQzu(Lq$;H8lGz~8K4dqsN(~% z2<$J2?Of3zyu{!5EtZ5klR494`YMF6C?=I3rf|XqyiEI6)a$l zaL=MN8xGz(0aXcc1GrMydSgG_*`QJpECq@waC%0LZn!c`2B`4^3O$fFL7sxeDToac z2c>YRgTVm{at4e63R{RrzA&5uq_~$fEW

zL8BGqL5MP#GoTv51|sxAya_8jLE;eGAtX!|E)F&Xl#d`K7{o>h39=F59%LI~Y={~V ziEJjSEXV|~OTe~(LI4u}2rk4`U=pGj>M*c5@ZbX30{1(_Nnm3^ioq&D3P9q?pooSC z2uuyaIJ1+-mhXe~q3zC$}@?@wu*bQK(f*lDl5EQ>)y3y)PPB_mr>#oWGq+;9EKoAg3JerARGhM1vW4V zYzs&h6h26Lp|W6Kz}QgRLFoi+CbHoOHE=Cx42Ulvf)EmJ9o#)|23QJYJ}5hYk`^Sy zKq6qPL4gS|1f&2G1@I&aaVLm$UJgz_(C`F#6BGz=jbNjY%>>y3mIWyT$%3th1UrZe zu?S=i)W0A>kVQybkSszwl3th~NC8+qTqD>PkiE%>=m3QPTpXqaqz1+YSq)=D#lh;) z#Gz84i~zP2tOXJmAS)m~g1ZDP3pW829S|w7$snV^43I{McoH<(z_oy+z=nVnLktIV z;R+z)5H3=F2ZcOHFW6YP^FSidYz}oiB*(#XJ;*e0nF%ovnS>h-F%9k)2p5zvz;+?D zz_=il5SOEffYJ%bI=FXWvTzfy%c3fStAR5R+QBNJ;$SvN3eJEy32r+$=Ry)T#KEAH zhNKvz7d3W39tSIcSOFrz=?$t9*On@~$l4&cCA)%1Bm)t;z_x=-1E+yyNv}z-sf%Jk*a|KxnQVB8vq7p0u z3OA4%u)~%oxgsJi8N`M-5~?0#4yZ7PdIV$|*pDz9L6*XU5gfk|jbL#swF#2FAakHx zs7s*gAtr!=7cLIThM=$l>jJw9q!;82knbRF29XdoAa_E=!CnSMAILzEdY9$N;5Y@b z!9f8w4P+^p0ZuRwy{_OC0%1ek2GR&p35pkp2*f=gl_2wBK>=oiECPi?GT0W7lVDoF zY^eK@{0MSCL<$^|Pz4ZIfyBWs0WqK^gA_oxV0%H9LKGvDPzA6MfLI3&C6ITZhJdwz zOaOZgEC`ha34&$8egWkgXkq{bAlO(WE5OMK6dE8Cz#a!_N46AV6i6@Fd)rBDMgG5}HLy9+uH21pA$g}{Ornjpbkh&w?M3QBAszd%GlB+LyE z10jw9yC38js1~qAP+PzTf)s-^f{Fr=7Kj3{2qaPr13-3xWx?9P5eG6KTnK}M1>y{dGDv2GI1(ZbG81A2 z#5xcOF&36KKq7Es!J!8h0T~D}AIt^22c!(j0J{^U1`*~k13~!_WDdjukN|-=38Wru z8e9-+8puZ=U&H+f5r--RxgQj65bY47;3Oy#!R~}6B(TFEvJmYMT_84G3X+>3Tu9i0 zeF3o)suwH^(gjig4l%IHq2UR&5mflYy$P}i?01kFuyzmwiT zr+{R^rhznqbRnDvPPZT@fier&!Jv?VxB=vHu$jnuA=<$zL9PXH5sn8L0u^xq`w?t1 zR58dXP@IB77#u(Nu^kkWV4p&`AoIbBL5_zoz$8Q#tQYKAkkw!YSTDG>1N9@+Qjh{f zxWWAnHUaD-aL9nHgQx_@9o(r123QuP9c&RuJ;Z~sC;+L17zI&@>{>7zWFxGF05J#R z7l=C{A`nABA_y%YM?zIX%m>>GQVC-~*q{J}C_o}Xf?%US4u&}lN!cBvVgQY+off)tz2rN9|T0ji2%Rx?pFu+EEd;wMrHvys&nv7vm z5C_Bcf^>mh2oeXI1`VLDj^O4`w=1tu@_<;L?qb-2nkjLN@J)}C@zH9 z3swm-4WbcFB8h`^!Oek%49G`NH6XWv>;iK^42Vx5M!^Ij9syYd76CC}4uG&hfeiL0 zL?ws|HvwWG*lJL)fW@J@Ac7Dx!3Kg;DcDJ1WeC&2Y9Pi!d<69ZL>b(J5C%*!NF^u+ zSAZ0|f{g`7E65ozt3hg@R)ZBlOaPH!E=)bdY7hz2h+reT0U`_14ld*&eupXm8wh2C z(;+0pAc|pb2DuHU2F3dzL68Zcpo7^03NDBh5bZE=P&|S>4s`>_91L4Pf^ap+-h>*p z0-^v!LiB<|3B&~}MmPi2Qi%KEvXI~fg%!k|sD?n?2{sdpbs$|XFmae0z=9xuflNk{ zO+pS#SXhBg1}RR4#6CzI%7wcSWFV5^5OEL*7X$?e*clKJ5EpC+gn{IJkach?kY$n8 zzzsnP3XmJXYCxVsb}GaWm=>@s*lLIfgam7Z1UtwaSFlqd8evi(hat1U8eyh^5*0`R z#9naZg1KOe;JP5P5Z}Rr8p4Hm1VTdW1)Gm>5?l}_4mICp1;h;y66AQeN{AH@F2W69 zC&3MY*^XinL=dbo3F2ln?O-X81JJ}Fiou2>+ymx990o2!zz#suh{Q$F2#N@B*g~8H zAwi0fA_yS}HW}&$BwZl&%b}?k;&O-0o3KS+tTr`ysEg&^udm&tq z2{0~#4K@T4_;8QF8E|6}>cIgH5{LK>W+2=OC<81DQU;1;P=rF*aFe0Rz@|aiSV*wj zAmUILf{lfWAX)9QJOv~ORu8cVO2VQTBnws#^2iEE^9fsq1-TY%)N-gAkb}X7fD#_q ze7I4rD^lRLfMh{>L1v-`JJd9UF0eCD)quhWt^njhaIk<3hdC9GdXPq_OJH_^-3ihT zb`r!35H3hD!~t+AxLq)DWV=ARK&GW21VLPogCXt&DFB-QH5n`m6$D!WWrM{*E&Cn-6vmNCd1H zl+4fY|T|g)tyjLllE;hbRUG6hv@2OeM%A$ZUx1AQECP zSR>3+2*pSuV8=tE2BHN*x-Lg}2POhD7UTvL6Tr40!WO~>xfbL*u-~CRNPBY1_5cMEs2yw8l zAz=#>1Sxji%AtDeGStXbaDqta@2Z}YY!;oAK zk_9;!?h?3l5SK$pxF9M6lpw(h5C(#|V0)p#4tEtq4H_5XB#?Tre<89E5?)+`Y(a_w zkSxp=kQCVE2y-AF1iKTY63l>j5Mm364NFl75txA>5wKkd2F!efX>i5h=mz-<%m8Tt z`vSs%kPsJwECowJ41q)khz&Ce%7$~nmV)$xoeDJy>@^S@<|(MJ!MY%dL5=}y0T~Eq zfK7vFgo=Zupsqq@gN=f!L}J6^0%{|~B}l%8XaN}raT`Q0goKz6V!MG9fEb_@z9I$0 zg_?{MI1r=I+zAl`kzkb|2E^E8H@K5PMnQe*1`Y?e<;frhB(xz~Ky0wHLFR*90@4m) zK#WQO83>XEdj!M)%R+(|Vg=kN2m@{c$Wo|ckRcFRkd5Hr0vQEKS`al*_k#*{P_Tfl z12Mp|AO=JM)JGs?AQMvDAO?aA2YUo=J5&qU3Xn<&17sT5F(3xW5M*T_?|_v-1tDs{ zA`t&V&4IWC>~r6ei$E2a7|!0P`Be7O-hxr$W?# z9kU_@9D^XAf;|oq1X%%A3Fbm$8EiYqC}d;7X$)!|$QK~fKo)`h2=P9|M^H5&10fcH zRf4!6Ux2iL*kCDeI)V8Mi!yM+2B&R^JHg%o*#dSUD5yc+0mlVc1Y{IQ4a9JeI4DR# zt_4{Q@)5`!Q0Rg5g4rO`pb9{`K+XgE2&5R~PLLrWLAd#_FaZgIHA4IgA)(;_HUY#2 zr!lZyZeYKFoB`s(oDH%CoU=gAfC+*$f_wpTC)`+&br7pTE^%7{)&=q%$dNFYLvkEQ z0Z8w1c#MPH15peLBCtgeF34E0FP4Mj7sLe_2y!P#0Vs$-;SX{KR2L|0@4DCRd+Y{7aVK0*Wt zNG~{8z`DS)5JN!jbOj{NZF)LP)R)VAq1Z02TyuLB@ie z1d0@p2*g0JAE8{ZUWl2{!~oI`30@EhatTNT<^Zr4K#3NTazG9SSqh2^sPjPX2YC%r z8h~sB=>=H_u@{mVLE<3u!Qvn-Ad4U-fTTboAeVz|0VxB^g0lcP@IfIAk%B0OI0I@j zNEyfhU@j=M!7&B42o#uL5l9&bvIXQ9gdw1S0+p_aU;*0>3IUL@AZH+qg=hz-YLLr8 zsR$enAX`9z2@-@D2uciKdqE74`Cvmprh$9`&Os0xK^_4G7f20Q3c`Ro5+njP0b(-P z*-(A_PG;f?Wl&4x$U}V32hnTR?#f4nvUl z!QKSh0#gYx8KNHS0FWUdN5ZoP*yUiwAiWR}& z@FUa^uwIZBu)Sb6Lu`lI3o#!Qu;7dU(gn2z6iN^-LKiriKw3bif&2wm09FG^#-Jbq zhZR@~8tD*OP}vJEa6nv8k^{Q|ViZCP$XJj9LDliE*1Y{}LJs<-? z%0Om<*id7^j)6D;ToHg016V!6Iv5vfFNg~=1f&3LJJ^F@bHK?A;xLdPgbT71OpP* z%Yxj1P=H7w5chyA1d4%nR_DX?M?0}}EO5-bZf0nCP2 z0dg2vJv5@hiXp~A6@a219AaQ)5Mv>_K&F9ZLFyq4M7)4vAEW^6V2}bZ17s#dC5(iK zKuKina5mh7Ah&^n6do+#umz_Fm}wvzLB2yM1B*bpAdO(tK!RZNAtG441NSM|7La#9 z46t?(1DxYP{(=Mx$Wl-&fDD8?25be?YhXc;nPBytq3o;pE1(*a`4Pii(LD*mtq8@HCgbO0U_JXW{#15E5u?wsKBm$NK*$DPH)Xhk; zAj83SffR%N2r?GJ0Fw}LxH5=QAUA+zAq-?MAgct)!W;n72ucYlt}9YOYT(-8wu5Cs zE`&)zt%kW4WEw;Y*-Ydx2L~X?F%V^7DTsj}L9jV!z5}@mtP7+J>=KYIAeGRFhO37< z25d5j4O5Ki7mx`MGeL@xJPR^5#SNT z2PRw`Bm$O#iXgE;QV@$k;R!8K!Gfs>t3lpOh586;H7EoiL5i#ltPvsQwj3IaPHXr&rusuW}>@2xB6t|jB5A?>6t zWv?n_uPW`NF5{vh=dLC1t*aDds2XLek!Yop?Pyr;ZrQ7(_2v5X=R|9at~e`PFBJyByc`;M4AXFKIEe}!*6;Y71R+jV9QH?a!PO~*A zb+u>fIAhM6CwE#Y#_+-U(O+3Kp= z=&IT1f*Cq0*4oO}T8dVh@|NndmTJ-#Dw5_(5@w2GCi0?2a-xQE!um2I`Z5w$%5oms zDq+UDIS!T`L5{OyT$d#yc^2dmxC>#)5|mjWf?z+w)qq0`k}1F*2dRO@4u}m8C9o`{ zbqW@TMG832qKJUBfGPrzMIc#3Dgr4384l444N0)eA#4Z<4Q;S?2pdH`D6Ou5L@3A> zkeOf`p$w4uU|k?j!8M|o3{{Dw3+gYBUWiJFMvytK%Tw&8Mj2Oms>hnkxM+x*D+=pL z3u#M<=*x;3%S)Om$y%x@+GwfT=xEv*=-L|_IG7qanwvOTm^fM(JD3~UnHt%d8rYZ^ zSQ~>8m| zvN1KbH;0&J>SSr)XsTszsA8inZ>26_rUc6Dx}anxtSc?$pr(;vX;S0mI6D^XJ7mAW z*q~s9_zsDL3L=Lj8XHM5C{ICTQ9=eD1z@WoQqbH7mV$`D&4F+sB&r}JRe&NL%@&Aa z2nq2HSOFrAz+8~?AO^y?p!kJ|KuCxq(0l%S)LnE7@piI~W={o0~gZn>kt<*_s*Jn1G_#&dk)&!YkY_DL<{Kp{#jg`{ac) z=C4`2V%yq{`?l^lwrAhzLkG?tIeO{j(Mu-}oISGd^r0Qc_HNv_b;Y(d^VTe$v|xJk z#P;Hb^2EGU&oEz8CrcwcP^25$m>AibnK@dTIa?b!n`=85D%ogBnJJ4I%M0ttfE^|y z=b@vWZfD&W=CU{m6waW~0Qmy!wG?-R-(kK6i9pzpkc5zMEnqGvG*ZEO;VR(_h)M_v z69lUV*$$00h%6+8A#9jRknNDTLlcK6fJlMe08$BIfJm@o;Ia_6L423$j<6JDi2DkN zND8tzOgr3CP;=U%JwPqWOww8zRBY->iyF&ITd2res%zUDnz~w9xZ0RHSm;?A8rzxK zdOCWB`6d^nw@v9@ykYsyV|&luym0H;z2_faz5D+0!;jCOe}4V?>)W^A-@pC-@#D|W zAAf!!p`YJ=|M>R%``2IJKL7ms;m7B9-# z+ITt`+nMQE8k#y-fE?grrR!jzB!FWX6h2@E z!gpXUSR5rcLBa$Qj1a}hBuG7KKq1RQ*q{(b7!D43q;Q6t3{?h>3y4Mt2^EK#0I?Ai zo7h#Z1StS3hG>VlA8H8LUWkoQ5u_9d)#x-o-lWc3CEP^9QVG)5k~CLRu-4SD(>Hc9 zH+8Z!u{YPVG%|Iv^bGe+DNOH}+Ou)r)(dy8JbLr=?Y9q~e}4V;`}>bSzkdJy^XKoM z-@kwV{`31682m&6KfnM<;3rtguip?IfB*dc`{&1>U*CRz|NQgo+ixEpy?J{6_T`QH zwzf~{PA*LM4EHs2vedIQGPVaf$;ioE!&XnhMpM!pRJ?;4a+XS}5hkYfK2Gx?F^&=r zknjNqAS4_hVFDsyK?Dyy5Cbj{qyhdzkmOJ{|3b@awJm1 z{`KqEpFe;8{rmUl-`^j9etrG*{o~KiPv5^dc<$)D)r+b->H}g!OdTx1B|ONHdJZOP zwmQ=0Dx!vRpcbR4f>My7akZE8B4~_*11HrDlp`Ra0Vl!59g;Xaz`>yh3PxCXBBBx; zj1Uvxg2Z>n+Y}wT2O=3gIowU z8LSwh5+v>pG84oAjg>7=%3IxZp9;neJZLXqbt83s0s(kgWj7;p!odeu+s|uHH zT6yHciKp*geE#|M$Dg0S|Nj2__wVoDzkmMx3C^iMD2Z9DdLhRC_yIEc_ivDO;CTP} z`RCUcpI)E5dUolimAO@gE&=W)_U8IlMy8IIhK^=xwt6xas$xcR!g?~I#_~!5`W7wz zE=!W#P(lWh4MBnIz9PjP97^!uf~tfI!YWO8FoGGN0E9C@dST@@IGRAA4Kf+37i1AA zOG3;gKvAZuGe*F36=O0i>`|H;) z+Ql!{NQVeQqW{c?FmbXHgmQDIorX)#Mx5AUSGyi zRn!<1L&F>zo1oxD4i;$0fTJE1?4VErse!}@B;P^QgJfY*0A<4z zgA743A0!LTTp$Ki92PrZ6F>%nq7@Xs;8=q(z`9aF`5oj2P>F0M6{Iay)a~@lU9HU=t#r)wO&lze3esn z14=C*qrj;J$pov;AVoiYNM4q-R93XsG;_7KcC*(u)6=&xj?GS5xPIxKmk&Pt`1Iq? z&tJcP!NYc-A{W2CKYsiGIr7iXk3T-&d2xU7#ubTqsroj?TBf>IF1F@wHp({Ik`~IK zJgzIP;HPKV5d^KfVFe?E3lCLzz`{kK3|PQI6o7&k9P*&(g9R@r{K0If7Et(uq8`G9 zYFP=&l^`})7On+kEX)d!EZA_6;0maEI2)n`ZYD$=?hKIL6)DaO6ZP_(#7z~1bfm@2 z6=ki|^c_vC-R!hY^)yVh!;+)styz5S(M?b+gX&>WMT|e32ahZ$LH_#rkqB!~ zf)qnUAnKtcD37Os#F5g8&4e)JU?WhMSzkuVQbpB9$J`CnQ&u(5a1HXDFmKAG``6z8 z`1JG7ub;oDAIm>}{sf~RKYqdyNQAnn1rp;w|NQ#!NC_;S%Vftfyw`U}5fR zqi&}sWuYt#9&S|*GPIo#1_~l@t^tKSDA1sVCn(s#Tu|JB86XjG9DziT7!VUs^Aspj zpdv`&4~jk*7pe=KDi8*OErM{N?pXmY^T93zX@{wYr~z96PAA}01QB#ul5AY*DP^l7 zs3j?8A}?p9Zs2HYx0d5E=W5Qw|#0D7xvJRvdCXO&1W;-YufZYQz2ciHH@6Pk$wbN}y z3}l6Mq$MqsRcv+4T&&C;txW9A!&75c@7(b0tv0_xq0@pTB*1|Mlam&+lG-dh`6#>!+VyJ^l3R`KQ+}KfQVN`Q7`kA3uNl z^5e&kf4~2NZpr!kAEf=yzrVl!{QmhHRK$?wAV`e={{8#g?;lS;yjZ_?OH_KCvAwyu zBWT=D%}!6!LRnZxTEswBE5*ifUL11pLV_I>p`ZXq#12ReC_2E|78KfPuwVzpI7AmH ziKx_dS2+GAE6F|14fuz97Kq4SDsqQP`7Qq>*U}Ye~ z?WaYlgcu9!fW|pxtW*LQazyJOrD>#4t`1#{Es1x?* z|DV7A|Nj5~_y3=tzh8cNbK}i}Q_rsKe|UP;%{@~uu5Ld)ujbhFf&+b-`#V$iwj}Ou zO5EL)vbQC3e`mpgzM5mx+fUD%c5(IUn|lsCK6C2Xl^bs!y!`y;_s`!DGyjA70Dpe} z{Ply}BLDZ_Kc9Ynx%TL0$J8DtKQ|*AQ)@RnT?b>(_^Oedu#U8HsIlGDC}=hThbkmm zA<+jWp+4U!ar&Qwgb~ zLAezczaUwN)nJoBQlNwcNs4JU{bAttwv4!`g1nWwv9pDxi;bS8VQ@nD%I#}ke|`5I zmYqqB>t8>A{rUap|KES00kc1UUwwIZ^68beH}*_Cx2*igq{Kaq-Wv<-R;SvoNwr^- zW(P)KMylN!6a*H8DzIIXYPUAcds9K;o`#AelP8>8w&v#Elh3Za`SRgExU~QO_urr2 z;Kl^0&i(oG_wV1|fB$&%?ftqvo5NG0^{k96U2Ke9Eaa`!A;Sv_KDySuq2MS0#~?I> zA(;y-0u5(S1c4(INf0Cj5(jCAXaq+GD8Yc^4jh#bqaY+muNyeef~CM27N!Lpi68~g zT)Z+3%mBwJsNw)wgrn=S$f#1*l3HJD~cG% z$~bFSwgiwnBcND(`ik*-RcxeQ2I%7UY8ZJwW9UJ+`SJ^KK=Oe&%ZyQ z%Kgt@lDiDQet!G?{mzT~T{HU)Y)v#xb*4cuQ4A@EK@kTo zrJ)QE8yY*X>;?&WNRj{*$Y7O_+zpCla2kLxKpJ6OgaUBPFRuV=2dRfJz$8c` zIC2rXAc|9sD?P+a6hy!sD=j+%Yd1ShQ(b!>=fxXWJp1?x)TIKCU=tJ5pka!izd$YL z|Np-H{&wd1)oJHfCGBdkU72jQJkD}OqV1{_=QSCixLcd$x;6_G*K0HJlOVDB(f{cR1DOd_*X{yJ{G!KwCLi08JNyrxJ*X=wCm7f~w>H|33fvcKFHpx?|Jb)@SJ~ z4mDX8ZM!PTc}=<-I4*W4`xKMRtZEIIMJtq?>D^+1V z8DSl1!%{a;bi*SBl-mn5rifViz!I-fszE+oRw)F zaAVY)JkEJ_nhS}BxUK=E7T2|z&TG=mmdEHU46$F8QgL|FfhT9b{rdj@|Nq~=i0!0< zqWjmck3T-2xOO%!Ct2HE-`dSy&%s#AN(Gczbfk^SJ)qGI$$H?xcZWt3B;=vN4pjzE z58${1Wkz_!LE{~q29T72HGt46w)wNW4RnlWDyVX!cnb6w!JP z#y0K_x)uh>g&F7XT>kv?>-Qhup~V*wS^EFq|Ns8~JM-dt&5ss8`J@bXsp<_eP~ zQO>JU$ukzB3+%8o>*evriz9tER7}(cn7T&0g2iAgws- z;=QY%e|;rt2;v90(Ek7b>(6hy?jKLu*=VsW-g0@Y<4RCOIIm748@a4bcU+ZfwlpSY zTlIw(H~;)WM!S2N3UbuA67-RyN8jHN7;MGa(SUA1ib!a$)7 z%2Sa11*@^irLp!a zlATwjxqz}{n)50$5h$WprW!AfOxRv`>E%t(f+ARb3XSd;w-UG4n=FZPfFvq%ErK~U z-EKva`O+BI)tT+b=iPh%^#A|=|Nek_;`k#RRJ;B935xFf*HVfywaxUbT|q5#2@B8~ zHU%F&`)Sdj@CQW}D9j--3nF16@WcRaN`VpsNEVzPAhGEViA|V#Q0#-Mb#PGtW`LC; zG(xHah-nbRo#!Q}gc=L$$%vaPYS`&px!P)&=)~tFU%Y$u%dc<0eiK!3{r~s>@85qr z?jDKQTyC;B(rQ_}^U5@rRq13$F+`)ws&uDSsfG(9;({UU z|NmcobuWHft>MB5r&Xy`vDA5Gn)R{-lf_YyTPkcX+#83YI{rk(WZx`-f ziOWe=H`cavvC**87q?In)|F8SHF2JofE-_-s025UL0JzTgRqtxSQZpHq)#5F7E6pt}3#*OS-J1|@`P znCMu$+pF5@h?^-0>qu*-*|{zSEkpsuBe;eIF~D&IO#`r842>@^7Zhs{22xyu1i=vv z$*>?2tOk+{AjU!rfs@9So+1XaqQ>%amg<)7wpylocHU0APV5J*GypX^@RwVlRa^i6 z|9|%Rb=koQ`U}FWmc+TOf`oIb^GXU)n#;;Go%un*8;h^J2IpuTGZ#O8{`mFt*Z=?j zue`bwy0KVyL9h!nnNjF~RF{>hZmTk^mc;4L4=p>`|Mc@K%z_0o-XV4O#~+{foI2>> z>!N9@Yw2z)XQ?J?Bqw4ZYf|L}iWgYsg+v)BI$$vc5do2q`WmJZqy`qb?km$l3cy^5 zID~|XLySU_bzhlgHz`8OR#jL>TG~>@*x5qg+F0Mlc-hv~@4kN|rq%uX_wWDz|KEQ1 zFnwpU&ioLYWeF}T)0|eMQjS1*b49Awyg;9|x#wSkTHOTuyr91P|Nj?X-tu3cuRTA= zX+^3tl^o-;GR<~bg3kPql%0*Y-ah#M|NkHG!dJ{#hKoS@-S57CT(Nzvp`EF|wXun_ zrL?6AXwlM32I14S}5r{7+bp7XqxJFPVWUx1^)d5YH7foj=}ix=p0 zx}QDOX?Zf`$a#5+xCnQ4g-W)2%v##7z}N^kwC( zH7s0gjBHFp5+lysy7=w)cW{{DBYyw>{qO(3eGg9htj^H~g=doUa_WSq)AD48rHO{~ zL!-A;T*loP!{<2Az$~b#cJ=k0m@QRC^TQmMCQ>D?!O;y$hx+q^y;f)KeQ*NQ%_pM3 z`S$z!*;^Mw5+jUkOf6h(|bg9@J=uGa!aL%}r4BGZ4~|lD1Saa5A%SvUc)wTeo`?Xt4l04cY%l|q&A|*%{DKsNrBI?6ECP{A_gImppYJTJCnI5@q-LvY zH&Ok1 zP-tSqkiz`pi<^{!UD!cRUAu;{#@4tUsdvq%z zHAdgc*uvFD&RQL`58Xz^Zc+p^vQ|MO2*L%&IH&*zM=>a_5s|wJ!iB^;oDES7F$5&# zzC1-U-bz?cM$$rA&%wmR-onhu3RHUk{DLQOLK>Wqest>A2E(}_4oef9s2b6Z%Mu}R zJ>~SW@4rB8Kjc^=kNxlWzb`+(wjP~jxhTqk+OBn4mgulF!C+o+>el+_pI<|w8xl~M zuKn=?w8Hk|kI!p%ZZLDQGO@SNb2I_1LDiGdjJI-MmI97nPy~S^6wXKk$2f!wCRe3- zfFc?Zp_JYb{$32Niww>W=yspWonLe(>Yx&;S4b zfBNyIY+sM|EI+#?aZXE<9G50i7db3Vuv`>9`P7o{KM`^L=Qk*igV%@uA=t%63*`TQ z{(b!MxoUsE#li^cIu&A?)6yjSC2`tw{7d)teERYE|NsAZ`U{}w{`=>}=QmaDb;^3` zHtzO{R+?fa3gTu8mTf_paqS5$s9`Y(;-auUSEhs2APa)>I7o0+y6fU(sb8EyO^lr3p5RqIGBaPd&Zt z59n}!zj!7DfBXQQ|8V-odDkFM9W#AXS1TzC6=6LYIFM<|Lo zC{u%C*<)q8CxpE!&9v4>+(bdlL|)TQ-`dqq-9&rp;+c>kGCUy$Y2<9beZ*oxg!#g7 zr=^JwOX3}AfE<=U;(940U;p|2=k@1z%dTvlczVgSv&$!+U2^itr9Y(X#Q*>2|GTdr z)3(-`%nPA~gB_Q|J1mKJTAE`&~ z%m4lP_Zz%%8;^tGQvd({fBp4c%!V?fxgj)iGsIY@C5gJT147pp-hKNBQgOrGfQ9kr z*R@Bt!c(HP&GanYZDlM~Mf7EqgNDm|Zt2G#pMU@PjjKiR6TB?&`>!9Zho>9N4YFGlXTO++ z$Zm10`TPi284il;J8vIGtS>g08*I5C(s*9T{Bvtbjq7KhUWczMG@chiQjHC7d4U>H&Zlk^aB;bu>1>- zV$5PZ9o9C3rU6*I!wUIT>8^{C)xu3h^kro%R85>MbjtAU|Gl@5gV*Gl z%nhNPvmqusERHvyAMUgye#h;@L}u-{oMatyeN$&k84FcW16j2QGuK7%S{o8~kop(O z24`V#6tBV}4vutCPPgg|matS7HIh@e(=&0fFm<$CvT4PKAD=+G%W=2L{{Q>`=>4G1LyoaNzC5C${>tOTV2 zkjS!B^=Jzb16gSc6(c9m@h?gFX?I>cz%$4312n4q=kLG2lTIu$nG)E}8ycAsv zLqkV%X>-v28jWZRk7cRg(i@a>5f%3;P*j4V6&&wyh9@|Kd#=g=HNU}9wi6;GtyM&g zJ(k?q20v$IKoMstF!=SP~(C9S>&tzP)|fA^iE&WjVw=Y`SQ@pcPi>_I5jcutVlveZ*g zFM?Jz;NArF=(w; zA*?Xt`tI9DQR|D1<^+-v*FVA2AKPvnvX~ccJSUhAX%!sZu{QIgbY}Wg?d$#a^ZWmQ z|8Zp~PqhPHGsU5mz^uPP|0Z&a0TAca+-~Rh2tmZ|S&JMC)5N$^n zWV;~BY)%MrzP|hRVbr=}quGJhAeRJ_+7AEs_uunRuPXNR=*;l3o*zLUkJvAWHkln{ zJwM{Week3wj#)5Bt^Mx%$Ij_}#&%}Ljur~mn! zL75ty20*zU%mBqTsAHaC-ry&qCnI61sA*?lWNYT^?|$&?QP8QLIGSXjqY3`}`~353 z>8^I|X`XiTqv(cg=SP~&4xV%zQHF!!dTo)>EPv~{;ij{K=AT~EbZ7hLpI`s|`3K6vSi0Gu#e83XeLryah)aNnp^b@#tv+N$ zfmx%U=SomshE&JU3=R?j3xXKwo~trI%{EYzECZDBSEhR`PuEJc7SxoKu~Id0wluUc z&8sfD|MJlvyk{W%`~UCogVW}7LXBny+AoN)ogYOXWHUd~bawDWq`1EG_F>f8BEwn! zR&&Em$ck&oisGlAUKH)->dibDo=8;n;&gEFOqJ^W?qEpte}a< z7a_IH9z?DwG@R*YH7Cq?X5jp@t4Yn*p#6hCL4)egKD{d6)2%zr*KAIR{ro6ed&+Kp zwDKgEs=dA6etrK7-bVn9T~zAVFVLxpOE#@EaR5y?Dp_lZ=*h@9syj}P0hi3Mq8k*y zpc))jQ>S}?Vj0|42RG3@SEZZP`ikhuikm3tIhq*TnFqv%oVs=n|Dfpq|Nl=vx$H1M zT7R0a!@L2H>xZDYoRqg3DnBiwRCxq5{&P=l}c$ohmiy=t7l=&Ng$yZRbYN6WPoSH=P+c@z_G} z{581G_2z!$s(i!gepa(Xjb{Xqm#^VD>Hq)#@4mio*gw%^Mu6q)P+EJ; z@%1XUIwE>9a;{p=a}pr&2yUlAq7__UgWBHUNJq5KGpsv8BrHL99B4a$j=}ei@IQS1 zIGzF=GJdl2_F=5JW{`1eQBBQ>BoOAmJ zylvsY{S#h`lMJW(+Rllfwb$(CMwreBFqz@M=gtw(?j$_pDt~?+I)BV7+|Sg(LfgSm z%t&6sQrWsQ*b`JKgQFGHNCTDCpxB4UHK-&9$$G8I(9d-i(U+AnS2lLB(6cm3Ey{fQ z{spe}CqI7v1kbepsoUGHH^tj(R;cZq2%98`qGv zulw&EbDkfgJ;mLYF8;Ee6Ja$gRClU(!~V&C|Ng~QjDu#r{{DIT{zYn0rk;hNv5SSI znUaW}tUbKk`&hEu&kCF-0o8~P&aIiV&q{3jk;@cq~KzrUgNb;PQCgK6HD zvxAMM`;igXphY#{ox=O>f+AXTk{c*H&kd)K*KFrT7*6x{TaxzV!wb+D8n!VF&<>iv ze?I>B+&R6^$j;2d#RfE}qAjf!ZtA%#4HDg+E7QF|-D+_Bf-^fZ*J);qf~StCfvl>n zuDPR?t*7JW1KYp;`i^Ut7HF*O-@nb*_gc>iHJct_J3HKFb{L(J_3TiS>HZUsqQ>>g zT!X1Tma~G5rjZlZkUILno#T#kBDE&D+s+QBr`K#}hnr6Kx1JTc^~U~x|Ni~LHiibN zvcLZNzH#4HTTe%GM=KRu9Z>^0c@G_@8L^<83n{BtLNa)|7bvHL8sT26pb5aLJy^oY2QTSX};DoLu_Y-+0Y$X&k8k}h8owe??yE`xglAxqaAWZnW-X58K&cboQ6+tT3yYA;#1ES`JM9|K~rhVjR>{ z`19++ohzPUz9#k-T6P9v#tKr_DpnofDjQLTgYq?`NQaP~;OI6g^$-P5Vj4Pu?!8JW z%y|6v*`I%Zv5wdM`uX$!-+wPZyiQtMYB0qc93Y`Kv*?YiXN8zd^8@GWAE31N@BbZ8 zT;~}~@v@v5WHi;6)H3}4zyA;4J_%Zqrq=IdJ3Ea2KC+n=YC9`Te~MSiy7Jeb-v0lC z{Xi{HG5+uGqc=~J3(^g3ObngOq|8AlZy1+)K+140Xr_i_?M%2Jh_Nz5C*58|Uslda z-PFm_*v_nNO4qw@AO8KrJ~j)Q{`>p?*!{D<7s}jv%&_%UnbN1{THX4cyJbc zwi1@jETB1vfB)Wo`_M9})7Z|;)X7rLN?k->RyW-qT!v@BYiDS7Myk8r7A31inu{6B zgYIQ;vNEzWTe5M*r=MT`{J}nZ{s(*t*@_FBEoTIqPY<-788RTqYG$zUR9}p^&e5Oj zWicbraEcG9aSd88_vi1AUq9!cUS&Ql&~UPk&CD?Rd(C`$pykYv73Vkq|M&k7wkjKR z0^*-PAAf#XykWVKote3_wW77An6bQiq`BLo6j0`c72}Y&29e;L?zJkzX-1ram#&zx zysC|^nUj^dv-O@62fqCVHOxR&0|x#FzQC;O&@BB)o>nu0t!IP`2(p?HY&^wx;*t6A zaWhc9UY4Uj(bHl&D5mC~TJ`TQ3F}Cqqoe=+{rLTJ;psKzQ~h-(c-YJgv!SmCEoTJl zPx9(IG!t}V6pqtwfBgbo8@Kz!esd=)b0;fR8&LEqdh0pPhy}$gG_FCVx7R8t32vli z*!6)HqKKO+YTFr_*qi%A23)v(`PW~ZL!{YO}@#V+Y|Nr1zhxYT&ud_EVdWHL$*qdwF8;YAKO4+E|^@l^^)DzVF&V;bR zv7ZT!^i0c^KnXKN2{R=<2NOdZla#`Y+t2UgIm`Y3-~ZQM+=^J9Z8F7oKw=qUDUIS9 zQj9^)kzR3Oqv0e^oqkAk(D|QqR3lqcJqHs> zGbM2|Ma$M8aLj^QWncyd;_xtZ3nl|i;nojYigMTfi`5904oN##V&mTW({5BU*@k_tZlAu;%FgnsUcye zs2*(rj%835hUMrCQ0WcJ=E_0F;--p9R$3;G7RL7GtG2HL9W{w#%iG^Se?NcyT)(^5 zXp*PJG=DmH9G4aN;(7|z;u=&l{svwBaOD0epZN*Kle}=bhl=7Bpk(aTxTpWq*Uyln zO(6*nnfwKs$o{rs>l#B_(Cy4h)>;x~iYg%{$oU!+@2f#^?X?Iqa;iUFybLO`O1HMW|MKDQpMO}lm9Ia&`S%y+e*AC0f9yKG&(YV_(AHGdMn~LKQQln#oR`6Iy(-gd zbp{B57-06QOgR@VNL(A*nz{vf?mKfBZ(RR-{_$nvszSpF9+p%51`t_H@im&@KJm~T z~T>m3&+zehwf^K~K4LWc9=>5~qGsD%p?JTGG)6=tt6WkM5 z6}|ZQ^55ToI5&g*`MLMhLC|t>TT?X~U2#)IIae)k84k|HkkWfqhS%y$Z&-aT{qP)rTpu`h#68&C(8ff~R#)6iQPx@03l^>59GwA;<<*%W@zohJ z_MmIv)WC7=73O#B@+n-qw|@Nm@$cWiC+}ZGF3B|Nce9-AH-N}uvaeCU`@}=DG4i#u z*+g%HKG(S?$t=S`Cu;utcl7=##~C5Y?N-)P0<0$c)7hs+{ce$qGoQSF0h(>ZcF!Yd zy7|wqqnA#4NBA4qn5fz4iJL0Q*sFue@Ku@K&<-_>0nOu?;J8*)vjOF6;^W$|-_>%m z&j2Ee$=-(jt`o4twWHZYPlG<^xhI#AQD6TB*U^XWoN$^Rs@P^>HQCn+l%(nHQ^S5& zlHyw378=)}ScYb8aJL$gqajhhIulfeo03q5KmYJDetDi@pR2_rp8-VXlR%lb|KM!I zxY?_Fp$k)Wx*bg?c^@If(Drse*L9dI& zMDGDa<`caPdR_Vt%tDmmukM8|NYUwXFzxry?{Na(5pw~5JMe)lIukhE`SledakXT*~F2LdW8qAe<2Q}B#Y;=w6%p84PiRg2^ z&RAPESmJs=)#Cd9|NrM7U-6q8r`};bP*DuG*Pz!Wb4~f{Pp|R!xpp1{^|_27eJ(R_ zp9>Pvkbc%GSPLASsl8TZD*5P3m?^2+=s?C&HXqnQXe{M@(Z+hi9%qXQUIU2CCwLn4 zIAhD#_NIOA`rVEsmf@gVA+}uIZ86bXf1tf)G11Gg$GLb@!`moVpfS>KUmADx8uU1uP4FD} zxWuS+i3v(Wii3atl!h1+qq>|-{)^%@QjCl`Soq(wzWof zW+sjn&@s|rV=r)ChP2Q?B$(^9DhpJDugX%7G?y?{RIt)8b+*z0&wYIU3A!~8If~(I zh}Dx0%rWe7Hl5%xfMJ3)UxzJB)$MjL>-R7K)z>7n%|MG||NlR6|D4OTFx_s40rZ!} z1TRyN`<vY+x zw_D9TzKEoD`2YX^FF(HKJ|jY>%YL9d1+mw(&rPq}Vfh)z`E1zx5s*2}DT`)kn}O%K ztkfmU6t$CW!7-cV1&Z-3@73Ul2G`vn@l{#+xy}-1N-`Fz#*XItmPW*^RoHvupiz&L zNw2GUzsEo!vwl#e42|nQpgle}U*7SW86nqTsNQO&*l0ZM$h?35LANWy3nlyv&}ru{ zA9&A<*6FYvC{ID`HR*LV?s49G{Sd*m3N4d5QP(QCfOB*fICn!M8XV)Gc=uYJXlioTmmEbvv8)c?=XX>vPxda)ie9@4vr)|9=1V!=9Um z)?L`L@zVD77q?z}e)AXTlyfXgtg%M=|NsA=zI*OJJ6@~JW}rL;vDc{E*=1VDnFkl} z#Py%Qf1ZDQRnk~tU~K|fgCqf3gX9gamqEQPP|pigfP-QgBnWPbTeX4?3714#TX+Zm z+QNVTuRp&XzA)9G%h9aYeV~wOubW<{L*IVTk)D6SYis|2M?ygN)j}}t!vL|z^}qkG zKD`N^m!#QdJy4#4*lW<`5Voy?Bw8vGi!@h6-bVz*-i9X1E zxKQ#Z_L>f&5|jM@@Bf=GZ=x4wXtr4mh>swan)bSCci7~wt9kSB4Y6y6?F=PM!E1*5 zBEao%P+kUC*`O&8Z%FP2lg=|@6+Cstj1|;u^~{~D%$%+8?*;h#`_G?0f4cWh(QdOg z>2@*eaT^e1+U=^>jvm*TF-W|~|9}7AetDm=tWc}XdO&;xvDBp7MXSxaXYaH>fByXa z{RgZ2Aqy3DABXP+&=EIQQ1sS!o*4(q#h@$<%GaQwQBZFSRB?my`|2$BMXBnMW}=3m zJw6uB)<$+_OE<0j^z+Ma9G6K!PJ>^0cB6TZhhe7^#D(-HsUO#W{=fhBF>iIHPMaz8)39y#1`Rv>{eSBIIiDF(x@~rFN79*L z+U=^}?$D1^Ut`Sz#0&oa^Y7#LPn8=Rb=&Ms2dJv7X{CoG_ef!Wl zxy#tj9I|&$)Ie4*!vPebpt3p>)b@tOHK>;js=h%5yZ7oWlX5RnLpf;+RYNCpBU`hy z;w(ISazW?&{r&&;)4RmQIhrk2=G|^)-L7UJ(v4mu$U5_GH?0=yj-3;}{rvXt56`@ws9{lSZ?pc@M-dAdBebDNrz)ce`seTO}>YefRnO|G(Hh1=*AP`0X?B zo?KHSXLD)LiQ;l5<(}Xu26ec=tuk0%mc3w`K z5AJ}Bii6fafJ{fhkVBC6Tt8se<*M6iYu@c<+T}tAWZvy&-s7g;WUf$a;4?jP+TpoR z-#>>8+oHIST+ZJ=e_wojHTUQepXuT9wfgE!=4L%^^z@xhn~iCg>)soOa2&>bvLA72j%fhuN4_;;by`*(uy`( z7OplX_7>eUClEXv=jF%OUem%fnk>z_TunP&XpPLfT#ehE6l)EQJDs}sOuF;>-rs+q z;j&-2kB1^P&OxK%fB*ga|L@N0yPdoGjoKX)>y3;%oXor2Xze@GP8YK-SB+*1?`dJL zKEEM&Hcr>{K4W`xa~Eqx8!cfSY4r#*uN9!xWuUALic?5Q4$st}X1Vw3EFW+JFvxQe zg`Rn3sc&hNR-A?B%q!3e`9FXD{`uRmwOy^j#JJtj3>4SSrk%7xW}PnjZFcfC1_9Hf z_uV+~=Pzh|KE4x0NR4Kwdhi+izd%Ql{{HiO|BXXIGvnlI^z_^8>EJ=5c1N{FljiMR ze{mP!C}&<-8X3D-f)CJ;Gt74d#WFZ5kr?1i4G{!oZD`cnbcRY=D2p2_Xge60+FO9m zvb#X|EV~1@j_9@6Xg6D#bvm1N&^_!nsIM56#zZa}(uVwMFz~RBUxDU2RPrEf=g?3fb&~waNyy#{d6+@!@6k+*IW{ zL$eMiQ<@>Y78~OZr@lRtzkL4!>2>0A5+pRJLV|M8&)@(5{r~#oTmQbP#vM+2EjG0C zqgjWOa;-tkytEe|U*Z}Dhg8;Ietlc8c8RH@rKPK_iXG^TN_jVJ*SU%CScXI{a$F;^ zvwgsWs9JGWBD$c<*UVk5^(+mmJL{i+e1&IoE@)uq|Gz2wXUSG+8@D-_wmZ=RX*F7y zce%|uvgFsFU;qE%K4*X`5sb}{|Ns8~{{4H_kws=5F4~P2wD6;8yOVL7gKU+~lmoN> z{>Rq>2cJFrs=BjY-^$3$)f#j)o34yjyft)a6kJn7VirPz<2@TpLNhglZC>jqq9-e1 zq6j%tIxsGjz?ss&KzF2Gcy!6G*Gr|wz@o#&wB3 z5r8dtetn>AFi;kTu|YL8L>7|$ooB?!IB1CK%PLuGnLAsX zIav{NR{W3OKk7EM$y8{XwL6-%JDRpRPz9N{JLxo9Xg69+J2>a(pP!^&gi6hr2Ac(P zz@MMf4$jqSw9sv|FmHFFiZ@N$96*8A?kHWMS-+|6$8VgQa6w1+fcN1p+p^jWJZ+(5 zttF~2E90QyJOk9*LXJxB)yR1{+Xu#lL^P;twlY&E$yP{9O4eH41bhcXL2b!{SC8?W zrvW-A>i@sv_f8qN*=f`nn}g!o-n5NUWY+Fr*kYqlsnfi*^ZRem1vlXE9$@1CzyIHV zeQ(^{u289M)M{ha?m)RuP223v+a1;GjEvjuPuwHw4v4(!A|qQ<6K6|VD>WevN$q4i zua%(j6L4IEqj(J{7ejJ7IC3Fz4VD76$+NvzgG4ME{6+O;CCrpG?F|fVOMUmzd+Z!+&N*|;i%bQY}#&b(q=~qGH-WKuQM_QrSnt&{$n2#rgJPKTleq( zzoU0fn6%qz)ESw#J5b_NlQuikc6*HmW3vvYqjy1fHi6dBAX^4w|M>Ca*Kg4M$lWvg zO&lyt94!>BHAVDf6uk6Y=Ou%4bQWs4o#g}0yx@EdV#BjJT*PZtre3C_h^~y3xw5{a zsezSoL~0D73l@L;`}^2KmYu?_3R$_ zE;b{5M^h<5193Tdq-LF=ORx922l$S+f}GOx@yF*ms}`C$TbVjp zs@ds@8Ocl8s5(rH0>#>zELe0yA{ta*`+y=H9MK?INP2)2>EL7lX{)cu)Qq(fHIS3F zR5NzA(6=&5Da^S0;vue$x{weBpD6x+%Dx#|4aRzn=B6#SCM`CkBGVRI?K+V5t(Ufg zmVo~LJxFS5h#w&&bQ}Kve_JkY(`ho(t}`avvwDr@It`}N_Rq$@wi#4={`qzH#lyt> zG(AfrBPVn4wa9XsF_xaoGC+khtS$!U-)wL!L!%ok0%n6E7Zml7`WmEbHAtmxSE!_= zikPvyhMj?_qot{n<9>}7?Y zzkmLRWA+vj-~&$n`}^vU+|hqUNCm!zvl znuD@7=&o~19dm<(+>{$n?%U;Gd5|p z*>Yh!{!3#ab`3cB|NsBZ=eHTRSZmjVoK3t>ja#fuT5L4yj4az7_gvdgxT^Z|>-v-1 z3Argc<_4gy7Il2l0if{JlyOAM5Aq2>7+NLdXMfg}k?M1vJWYHg?j%cejH zGbM3j1x-5xb0;eU8r-QHfgpY#i!PWO_r+Fx*gkkfB*Rn8l}Nu3203I z&+iXEJ}+3e#K6|X+{sGQ-T-uQkC~EXV*ohrK;^SHv~32;u&_K1>O_NbH#o*YSr}An zgX(Lr0+1|(;j=p1V^NA~u!)GCtgNM)rMsP$sa`~C43Sy;(VIshQzPX|HOyLUjhn29 zM0)k6TD8U#_e}l!|1Ykt2aX^a7~x;Pe*OLbw{Q0(ty)94GQ!I>Jy1?5&BP!z)oa8SLB zh-FZ`gVaN1LEJUj7Ipp-ri$Vw3Yzu?)^2v{Cfd`N&Uyd+)34w7c2fKX-$%LU>VEwO zQ}rr6(-s@!CQE|hVA5=@TBT#t;e3`*i*#V(6^E^$D<_T{i(H5}D7xX=vpkoisRS8=MqI7bEZl9hP4(PEywBXc@clRF zeqN9pv0=!uV1NJqow9d^LbWgX@kqc)0xk|Nq<1@8zp&rHfVd>P<}= zt&AEh@gV(rGxchNuI>Gh%RAua3~~nOn&6+`JGb?zSL^H7o8j@GQG=yPqqSb0sceZ# z+3H%*E%yKM&Ch~r>c4+petuKa*{H0qZtLl&Y^@DB=G~?}1XS>Xr!&CKFla*y9MPaS zg*Dhftun9zFas2)kXQzDAyqsmJ%F_Ptj_jYnV}VL4O*LIsiN;_YHDv`?rgno_hvkY z(}Ftw;8o@S|Nnpe>2<=KRE1J?{dzOwMoXgx3#`bv!BVHjNT=3l+r=G2q6H35fuN=G z|Nn2juwA>>NT=%(Ua9mu<>4*3ChUOxC2Knq}>ILIEV;n01c`PQfR|TkTP(N28n6GeGz4GTA011sa`jQAT*?tCX=v(*3p{~x}27&l7{~v_oC-BHcV?bs@PTxCg-fX8;t&hip2KA=$C8`nAq8}5}9{c(8 z`|qFEpWKd0k2A0~ws5nRx7H9hQIxh(b(|anjX{*S2FD#Z${@1dpm>DkVn`MSwa~yK zpo9U*+7J?)ufgiQS7#aKxr*q?N?NL@+v!`m+L<_5bWZDg{q@~Xyp1$a)8Z#+TIAA` zD_#=dZi)QEzU9fix=h{3nIncy<%CeiAJ}2Ql}@&!7MQ{lEU~rvId1%}QNN ze;L)AgS=d4CR3!~GcoY$(`)$Gl0p6Y>*uR4?>eUTm^fHix!I}P=}THDi|WZ56}W*$ zzag;{~ft`)Va$;wphuq-qg6>+^EhB4XKsuB+pBK{PyV( zYM4QM{QvL&$8R4e%}rG+*GBUcC~eo98`hi37Ad-Rc^tb9n#sa-_B+HhPzL$^>*J5l z>-TJS@^iCrvN3crm$6b4(Unp5GjyAu42mXjq#(sIq#OpvJ0$Ml85yDyPD0`oq6Q)e zVXw*d1u?)Ttj@OU4hPMa8ptWyf)D%w`a7W@|K(3uVeB+37x2JTW zl0l8BahSuUh(G3JE{+jLTZE`KYzd(xZFve_`kpZ-h6pmw7gukOdHup zhIMAf_2vdOCenpUZr$ETZyf`T@Z-2|3)$*#zrUZmeJM0C(#Y1#!qrB>MpM*4PQpyd zwkr%2(UAOz98-`!7bLPEu?doeBmksr4$%lFS3}|#rU0Y`%my=z@?AxAq$Nxh)ok^w z-RyNO4fAS>@4tSGe@+C_R0EyD{r}&w+s8e7{bUQ3^=gfcK+$aq4;+;;t=e@>U%!8& z;wBDI9{B~jBl9o#gpL3IKr4+wS7zck1|K=ViD&=)^Y_d5uQh8MRLZpA-Z88*HL5q) zt1*%-RQB!gB+)!Cj)(o{oCgtVljELC(J zOe|e&HO+LpX7s=O{FY!B47A-0d@auT2NyynN5~he=v5hka(At%VXdh_t*LT}M)QWY zpFi=fwjw_KfByUl+Bgha-~IpJzrR2J{QUCm%a?Coe*F3Q@84h0p$q^16D&)Kv<@`G z{_{uk##ZGL4Fj-u3~NCJmrk{ze344nl&H&(u7Gm%Z_wf%JZ3^WSzq3E&FI%O)wOi7 z)pInFvQ!b$l2Q#e@m!h)id;~1fZA7}ScAqjGJAx8HB%QW18ZY_8ygBnm=w`^?3bHo$W5aOtRe?Zav>HDYSw@*yoGp&4ORo496togZR zE32mLnRep#$xq)u{r~@;D!V%&1yRe!Hc(vGm>Pm>D9v&`nS8~xd09{2f_5(aCNwz< zn#}qIT6VHx+gbx#6MZWqQx_{4OHfct+o^-v-rz{_U7Z7sJ4mFj$$>^XBp#uHsEGk0 z1=bGL3yE2nVvq?C5ikkjg0=Xr$u_U@5jB((2c3teY2jw8Wvc7w=ep=eRLafkqjDZ0@e7xe(bumD`j?? zWs{v^v6@7#j7+|QOum9du8dN#x@D7H%AB;_m-m9Q`QLv8-HhLA@Zo-+Yt}TV6l)sQ znwr#^tCVQS7b;b(ta|(DEusgJKG}Af#TEk!q=?T%n3i zwQ=jlj+eMMSrBRYzrX+9fB8_lyh^1+Q?JTMwm{LS&TQ8H*iE2Ecc@3lO`2Na2*SO&)zs7-|uU#pR#7#w##==nXzXEiv| zA+oDsF^-5_aKR4cf@FQxK&^9Mn4%VDDylClX`!rXZ(!+at7)Q>n3sC-?$s~9zTrA~ z1I^csLKj+EYr^MtwT+aCS_uupPFVp8`OXkQb6sZPJ4nKI~ zFsMm@cL)@2+V9`Lzx?`k@$S`x+!Rd{9Z)OC-ayhqMO05#EzHbgQ7S0It_H;~D91q~ zD;pFSt07qx6w#ol2c-v4=?zr|Q3;Mduq-t0K#>konFC4>kg^&=LezuvJ4iiP5MfB*iy{rom}VWCv6oJOg(QH_~lwTVHMF;-+)ZDLeos!^&fl`EIO zsQAN|4|rCF;Br4i{QtlI*PdK=?eNsEGVa>k^XSbZ$ksxLEFKb6cl`SK_1Cvc_phZE zWon!2gM!h)NXk-0%t&6z*T8jVJSen5nG_nWATA`{L6HK=s^EA5MI0pdK?wsKYv2+9 z6u&;;To2)bNN^;A7+_^!1HsC`sRF7UQYnDSe~`((t8*MCM#;Hoiy6pCTdC?fn%IDQ zVd*8=m+oEr`s*90KMNie$KzROYx)2GUw?jYyRai`e$LtZ=g5d_(9IN}P1Ap;?wKK% zr=(G$Z3K#CBZDgZ$k?dbM7=~?AzyjM-dTVD{r!vk#vDAp`2X+!*?VWR7v%1^xbx?4 z(59!~KYwGpv;oBoNFDv<*SAaeu4R_z=~^1vxY_GDnn+u!iWR;gY-sQ(v?3x|q^OTH0c^Q8!BV>)LjOFtc?OL6#J-J5Sw9B90f8Kt4`{F%l z)hnb;4)YdH8q(_i^6T4$J6AHwa&;^WY~1bj9ZjXJRK*PCq;1q}J3@UyB`r99K^Zy6 z7gB*jVhWxHV6`wvF({fq>LDt@85W!pK;ocS1H~ptBT_DgazT*`P8Hd(5+1|=B^yu@ zf>hriqbwUhOO-%#+}3LP4kotl4!V|xDTNsq?_T}<^XvB?-$5l6PER8wK!r&$ zxK7A|?Z5y2PTDy|sZd3`OxK{&h$N&{rlVA-I%U^1vX`TS#(_bH^Wa;;f$ZS#KfZtd z`Sr@f8yTfJIu-`D?hg8nrZU!`xoHVgMazZ&P__aUw%~YyiRj*fH~2*8y*@YF@F32?YsH#?L*A8IH??2 z!zyF_N<)&6VYRVTj%@7II7p|P$izvKBjJi6Ir`&|&!?`ROUz3J zlQpaK0mUXbE|8-J6yV@Y4T@h_TtL{MXa%PPP_%+$1R?_Ff+7^G7nE$k20|kpBn$3a zff=B<1~DM!fRY(BC{dY~dyDGHit5YBSgY%TqT61}R4*t#Z0GTP@4kN| zII0E@3i25L|Np=Cp>FQ%pF;1Qvh|vX#=s&;T ze*d`l)WP6{a1CP}D_1)`M^jlFby0me5nUP6axYN4fHN{E$HC$h64anN7ZeweCK-r? zL4CJ^S=(>E@MAer{T(dRDG> zIu6D%mY@cYgsGxg6(mQ4f)|_t!Fdf-BZI;j6y2b}&juH>NYyu}9*1OAkRV7IG+M#h z!CGL60i+DV29?jCBmorxB`{FBLg9j{1CZgMqykD9V3&Y2f>dT(HU>hPYEqUepyi0} z_F86o1~w+`Q@iiHc<}Ytcj}#21P#X@KYu{tde?^E$@L%;z-B`0 z2vD;eWE41Zp{@c&I;aT;=0ek+O-Go#n+{}Gf~qZOtWV#{NW)Youe#{W%?qIFo6zzG zl7bc<*9Wd10JW%d!>D5mau2idJxZfpQ-xQovO$xZ(zhfLd$db~QBaKw~dplffo{3jvS`ATz;s z!PJ8ab`TpZ2$luwf^b2FkI$MMhe^@Oeuj`u+zM72CN7p%F1C7BMxlw3t9Pt_{q^1V z-=IkllHwGPVo<*R_y69@dr32s4Jr(^inUb>)YJ;q5vTx(U8t^x#8ofUP%BW^F3~Zn zG)|eBdhg}^|Ns6|acMee%Hz+^x8Fak-?KS9Ia<%k$kN%y#KlsHs%fwZ;OGD)0FVeM9-&bK zRt$}CkQ7)kxB&o3LZEyNO>dwi0kQ?;5|A<&8zc@g4V>OUSsUbx)j7^H64XM?M08}t zO%!FV)b*T9LH7#Tf-e8+nA&sw@$FAPzyA4${mK$N(TYpz_pjgIe|*3D?B4O4$9G-a zwe;}PzODULE2{Grf*)giHL4Uynw_ZIT~CJfQ$lhAvqVEZBY{gBqf0A zbWkKhOasRgKPmuF2o#AQ3Fm(AoU;yq=gU4@=%o^Lp&Cy=%qV~8On?3%1B$O zs@v&VxY}4a*_b$3M5e{A-L>)g$5-Ef|9}jhLz^yWfkh(sKWID#G!6|J?fmrh)4NY^ zU%h|%?A`Mxub(`A142(;KY9M{`K$M@-hF!a`Riv;ryijV-wIwL-2(B*??1o4|Nim( z03pFkj;TycZB5W9TW1;f8 z?cDnxpML%UtsQ}`${;dK@hbcM>-XP3paIXnfB*i&0ie=<{!(kj0wk6}J7WI)`tak^ z$!lk;I~&ZLK~wxzu69Pw=8Cpjl4eT6TGGgF|m(Ag_cu!HgoC?Y@s4vKV0Aqf%z z=N))Vfr1@Ws)I{iaHPQMSx_{A)PrIiVgj_30P6*@L9*aX3Mw%`mVzP_9#bHb!6g7# z88|9Ij)6A$K=BK*4(3jfN{Dty%0Za#FezFs#8g;EM$|w~#!^kg-oV1m*3{lYMPI`` z#C!6>8CM_P{P5#5s8dGVj0xU&Mo5ANVG1sf%a2 zhj=UNtDD+eSh(40*y)3YN(|+MwWU>qO`ImhfMXLHLEyXzjy_NnfWi}6LV}_O6mj5) z0EvSm0v7(zRud@9!QvnWxRDGBe{dQAiGVEv83I;=WC$o%LgEM|^MaJYOhy<2ifFKD zpp=GU2-s~oP!g=nYiWjIz8j=9B5A22Z>?$IWM=JduVbdKVWJa}8na;ClIxFefB5nF z`!7%*3X(@C3w}E3hQ#uZ-#bvN2bMUnd#}6>DzcX7&w|KSZhjIf)2Zo zFjX|nclBDD4$3BwpoR=(fT9>0T(GDC1uP^0!5wW77nC2Nts^iOtP2!1;9LgJp%4)? z5|X>YaR;i(!F4^z5U@Bj$w91!S_;Y3AS<9r3#<~9W?%|H+96s%4g-4!>IP7ZXWREg zs00{^=zWf}oIxg(pY~5-(s8kQ7oRLbzZOl-(c^0p@~|5SRh73&DWJ7d)E4t$lDbf#VCT z62e6z;pTwT6*P*$Y_P*X27(wMWstH0P2^6Jb3^8)7M|$|NQ*}?*R>XW`{cfv=#U7-|xSFy#M~`&Wi_gS1n8~ z%rJEXdBM!V60|PM!BEy(9URM`Nh_m5H;)A=NYMle98mB=t5+}=9K4_q1LY=ASRvGd z!VnY@n30801J;htg=vI|!;4>pjbJH|;V=d$BZJwX!~6_o-HiE8o`~B>mS*Ui1;YcD#y1?#%lqyKc1DY~H_JS2a4F~lCP|Spw3?d=Qyq0I$ zw1ubynLuOQSV7i8P1D}c$kE)?(bB-$*u>u4Ilw)?wq)7nRmU!!diwswmtWt0{P_tw zvivXTiakiX9iG>)m9pfE!Q=P$Z_uF!e;})le|>xT>CLID=az3-omXAt66j&#U}0cw zZ0cxf>};WFZzyM}4vJ+1IdKyOl>j5_wh-^-nUKhWkPyFv+2BBi1TRVf4i<+69Y{S$ z4N`zZrI3_?q5z2tD!?I84+(7~DUeYhL9hc53~-AM7I7f+k+gug5S8eHU|k@GfgFh_ z&(V~D5(dONuVv|$^#Q6Orl4k8(bCYy#MlnxNCQVxbz41I3pH_L1u;X=?hLgMQ;QluNE-~6 z=21fc8s^|;5;&B=H6S=2fx{LYddON}Y*4g5#*4A6+QkhD|*mFnOaSF+L8 zvNtqwwls6HGIg-jw=yzwvht4bPc6#qn%=+p!1jxGu0DG6?CsYNpMQP@?K1*L{hz;o ze*cCR_>eLm9*yv<3Ks#D$-jR6hUobF2NJp8euGvKy#4zA(VJ%%?_Aw{V0+i}{?wvO zpU41HN02j&?aV|YEKkTQq@ zm=st99y@RbD3BpiU=qZ|6P`qb(B}^?yOzk-#F~ ze*gIT3lyy%e|&!bb$T3xtwNz8F)zxt{HgUBAkF{DG+nO2Kni<=fo7kJ1J6roi1SA(^lr&Yg zPU@VpXy*L2OIB`Mx8>lD-6!_%KXdrtxueIgoIZZ#^ucpS_n$eu`^5e&2Y0O8wr>8~ zB~up7Y?;_m+Ekfbkl`KSZ{}oWVsCD22fC!p*w)O#$r^OijjNT8qp^yuu8gIcq?xj) zft;`w$YF|J`udqpjuWGy^)e!s!4ZVUMT7}hmMAVt=;4k&NUlLm9;miMLKr#Bk=Upj zaXSoT2&%CNLx>f}Vwd~;6pQ)*og`aXM@>;ZQ1%hkmjzYm<|>M|T57g>I`*LB`%IlI z&7G{xK@ntOWNT(<4cZ-}V{QPt>fPSl#>2tc&&@T^(<8*kCnCTnBETcW$2HK?+0V_! z!@h`qnWXjg{c$hphZ(hOK=V~Gqf=^vNbidF)_3; z0W(aE?aV+eEAV-YphlpRg`uOFfs>i8gRz>ep0cgByp@Kuxr&5|B4qAHR9_A>+~=yJ z6=!Wx=kGi%&U*z)1DjB64nWWiT_^dh%yOTXY}XZLnCGeyVXo+#BvB8Rog#b%ndnOZHhc9C8rl$-obL z0Yz4l##Q)(t?*u%10~!Wz!boG9v+67lW-PCX}cd4n$ju>99fK3`~bn zBI3C?&2?siLw}S_TZmbuk71sxPO_b5w54i@sgjR@f`_i0i?*DzmaKz@tb>M}vzDBT zwt|PQl8=FEh^c0@rB1S)VV&yhtC7|gEN}M#RcL;<4-tuG!xSxV@ zVAOY`Auu>YU|7~rgEOc{T{Rj4bO-?|=V&r&Z8EfAo3o%pSdCgieh82k%R{;mH3Gdn zq~mL#96y>)1}fyqws$nj$PT(8p>Z^RheR}y8;44C z3>9}$rFg+u>PgnJs6n26Xy-f{WE1K{E8NS2--U3ejxxv!fss{SkrhN#&Qqgq7!3ik zLtwN(COhaxH4+yB!?{Kn=L_7o3;}#?sNC#ti(? zFu@FyQ4v~(z-X;Ra_KjkNokoNNsbQ69WxrCl!xK4HeqlUl7rJ#1CW1*b=n&ZPx^(x fXhlGFR-#hSjh1QDN$pgM4q_ccXX{8zr3?%JUu%>w 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 de5c723c9..11fdf501e 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 11fdf501e..be6bbc7aa 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 be6bbc7aa..7bc54cda0 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 2ed3ae93b..dc30cb696 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 422e4da84..9623b60dd 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