From 079865397c0f0106184466078f2125f98cfaac07 Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Sat, 25 Jan 2020 11:56:06 +0800 Subject: [PATCH 01/18] update to detect location of openzwave and qt-openzwave --- ozw-admin.pri | 164 ++++++++++++++++++++++++++++++++ ozwadmin-main/mainwindow.cpp | 2 + ozwadmin-main/ozwadmin-main.pro | 8 +- qt-openzwave.pri | 2 - 4 files changed, 168 insertions(+), 8 deletions(-) create mode 100644 ozw-admin.pri delete mode 100644 qt-openzwave.pri diff --git a/ozw-admin.pri b/ozw-admin.pri new file mode 100644 index 0000000..3219804 --- /dev/null +++ b/ozw-admin.pri @@ -0,0 +1,164 @@ +top_srcdir=$$PWD +top_builddir=$$shadowed($$PWD) + +unix { + CONFIG-= no-pkg-config + CONFIG+= link_pkgconfig + !isEmpty(OZW_LIB_PATH) { + !exists($$OZW_LIB_PATH/libopenzwave.so): error("Can't find libopenzwave.so") + !exists($$OZW_INCLUDE_PATH/Manager.h) { + !exists($$OZW_LIB_PATH/cpp/src/Manager.h) { + error("Can't find Manager.h") + } else { + OZW_INCLUDE_PATH=$$OZW_LIB_PATH/cpp/src/ + } + } + !exists($$OZW_DATABASE_PATH/manufacturer_specific.xml) { + !exists($$OZW_LIB_PATH/config/manufacturer_specific.xml) { + error("Can't find manufacturer_specific.xml") + } else { + OZW_DATABASE_PATH=$$absolute_path($$OZW_LIB_PATH/config/) + } + } + OZW_LIB_PATH=$$absolute_path($$OZW_LIB_PATH) + OZW_INCLUDE_PATH=$$absolute_path($$OZW_INCLUDE_PATH) + OZW_LIBS="-L$$OZW_LIB_PATH" + OZW_LIBS+="-lopenzwave" + OZW_DATABASE_PATH=$$absolute_path($$OZW_DATABASE_PATH) + message(" ") + message("OpenZWave Summary:") + message(" OpenZWave Library Path: $$OZW_LIB_PATH") + message(" OpenZWave Include Path: $$OZW_INCLUDE_PATH") + message(" OpenZWave Libs Command: $$OZW_LIBS") + message(" OpenZWave Database Path: $$OZW_DATABASE_PATH") + message(" ") + INCLUDEPATH+=$$OZW_INCLUDE_PATH + LIBS+=$$OZW_LIBS + } else { + exists( $$top_srcdir/../open-zwave/cpp/src/) { + message("Found OZW in $$absolute_path($$top_srcdir/../open-zwave/)") + OZW_LIB_PATH = "$$absolute_path($$top_srcdir/../open-zwave/)" + OZW_INCLUDE_PATH = $$absolute_path($$top_srcdir/../open-zwave/cpp/src/) + OZW_LIBS="-L$$OZW_LIB_PATH" + OZW_LIBS+="-lopenzwave" + OZW_DATABASE_PATH = $$absolute_path($$top_srcdir/../open-zwave/config/) + message(" ") + message("OpenZWave Summary:") + message(" OpenZWave Library Path: $$OZW_LIB_PATH") + message(" OpenZWave Include Path: $$OZW_INCLUDE_PATH") + message(" OpenZWave Libs Command: $$OZW_LIBS") + message(" OpenZWave Database Path: $$OZW_DATABASE_PATH") + message(" ") + INCLUDEPATH+=$$OZW_INCLUDE_PATH + LIBS+=$$OZW_LIBS + } else { + packagesExist("libopenzwave") { + PKGCONFIG += libopenzwave + OZW_DATABASE_PATH=$$system($$PKG_CONFIG --variable=sysconfdir libopenzwave) + message(" ") + message("OpenZWave Summary:") + message(" Using OpenZWave from pkg-config:") + message(" CXXFLAGS: $$system($$PKG_CONFIG --cflags libopenzwave)") + message(" LDFLAGS: $$system($$PKG_CONFIG --libs libopenzwave)") + message(" DATABASE: $$system($$PKG_CONFIG --variable=sysconfdir libopenzwave)") + message(" ") + } else { + error("Can't find OpenZWave Library and Headers"); + } + } + } + + !isEmpty(QTOZW_LIB_PATH) { + !exists($$QTOZW_LIB_PATH/qt-openzwave/libqt-openzwave.so): error("Can't find libqt-openzwave.so") + !exists($$QTOZW_INCLUDE_PATH/qt-openzwave/qtopenzwave.h) { + !exists($$QTOZW_LIB_PATH/qt-openzwave/include/qt-openzwave/qtopenzwave.h) { + error("Can't find Manager.h") + } else { + QTOZW_INCLUDE_PATH=$$QTOZW_LIB_PATH/qt-openzwave/include/ + } + } + QTOZW_LIB_PATH=$$absolute_path($$QTOZW_LIB_PATH) + QTOZW_INCLUDE_PATH=$$absolute_path($$QTOZW_INCLUDE_PATH) + QTOZW_INCLUDE_PATH+=$$absolute_path($$QTOZW_INCLUDE_PATH/../../qt-openzwavedatabase/include/) + QTOZW_LIBS="-L$$absolute_path($$QTOZW_LIB_PATH/qt-openzwave/)" + QTOZW_LIBS+="-lqt-openzwave" + QTOZW_LIBS+="-L$$absolute_path($QTOZW_LIB_PATH/../qt-openzwavedatabase)" + QTOZW_LIBS+="-lqt-openzwavedatabase" + message(" ") + message("QT-OpenZWave Summary:") + message(" QT-OpenZWave Library Path: $$QTOZW_LIB_PATH") + message(" QT-OpenZWave Include Path: $$QTOZW_INCLUDE_PATH") + message(" Qt-OpenZWave Libs Command: $$QTOZW_LIBS") + message(" ") + INCLUDEPATH+=$$QTOZW_INCLUDE_PATH + LIBS+=$$QTOZW_LIBS + } else { + exists($$top_srcdir/../qt-openzwave/qt-openzwave/include/qt-openzwave/qtopenzwave.h) { + message("Found QTOZW in $$absolute_path($$top_srcdir/../qt-openzwave/)") + QTOZW_LIB_PATH = "$$absolute_path($$top_srcdir/../qt-openzwave/qt-openzwave/)" + QTOZW_INCLUDE_PATH = $$absolute_path($$top_srcdir/../qt-openzwave/qt-openzwave/include/) + QTOZW_INCLUDE_PATH += $$absolute_path($$top_srcdir/../qt-openzwave/qt-openzwavedatabase/include/) + QTOZW_LIBS="-L$$QTOZW_LIB_PATH" + QTOZW_LIBS+="-lqt-openzwave" + QTOZW_LIBS+="-L$$absolute_path($$OTOZW_LIB_PATH/../qt-openzwavedatabase/)" + QTZOW_LIBS+="-lqt-openzwavedatabase" + message(" ") + message("QT-OpenZWave Summary:") + message(" QT-OpenZWave Library Path: $$QTOZW_LIB_PATH") + message(" QT-OpenZWave Include Path: $$QTOZW_INCLUDE_PATH") + message(" QT-OpenZWave Libs Command: $$QTOZW_LIBS") + message(" ") + INCLUDEPATH+=$$QTOZW_INCLUDE_PATH + LIBS+=$$QTOZW_LIBS + } else { + packagesExist("libqt-openzwave") { + PKGCONFIG += libqt-openzwave + message(" ") + message("QT-OpenZWave Summary:") + message(" Using QT-OpenZWave from pkg-config:") + message(" CXXFLAGS: $$system($$PKG_CONFIG --cflags libqt-openzwave)") + message(" LDFLAGS: $$system($$PKG_CONFIG --libs libqt-openzwave)") + message(" ") + } else { + error("Can't find QT-OpenZWave Library and Headers"); + } + } + } + + +} + +win32 { + CONFIG(debug, debug|release) { + BUILDTYPE = debug + } else { + BUILDTYPE = release + } + message(Checking for $$BUILDTYPE build of OZW) + exists( $$top_srcdir/../open-zwave/cpp/src/) { + message("Found OZW in $$absolute_path($$top_srcdir/../open-zwave/cpp/src)") + INCLUDEPATH += $$absolute_path($$top_srcdir/../open-zwave/cpp/src/)/ + equals(BUILDTYPE, "release") { + exists( $$absolute_path($$top_srcdir/../open-zwave/cpp/build/windows/vs2010/ReleaseDLL/OpenZWave.dll ) ) { + LIBS += -L$$absolute_path($$top_srcdir/../open-zwave/cpp/build/windows/vs2010/ReleaseDLL) -lopenzwave + OZW_LIB_PATH = $$absolute_path($$top_srcdir/../open-zwave/cpp/build/windows/vs2010/ReleaseDLL) + } else { + error("Can't find a copy of OpenZWave.dll in the ReleaseDLL Directory"); + } + } else { + equals(BUILDTYPE, "debug") { + exists ( $$absolute_path($$top_srcdir/../open-zwave/cpp/build/windows/vs2010/DebugDLL/OpenZWaved.dll )) { + LIBS += -L$$absolute_path($$top_srcdir/../open-zwave/cpp/build/windows/vs2010/DebugDLL) -lOpenZWaved + OZW_LIB_PATH = $$absolute_path($$top_srcdir/../open-zwave/cpp/build/windows/vs2010/ReleaseDLL) + } else { + error("Can't find a copy of OpenZWaved.dll in the DebugDLL Directory"); + } + } + } + isEmpty(OZW_LIB_PATH) { + error("Can't find a copy of OpenZWave with the right builds"); + } + } else { + error("Can't Find a copy of OpenZwave") + } +} diff --git a/ozwadmin-main/mainwindow.cpp b/ozwadmin-main/mainwindow.cpp index 3b9945c..ed0195d 100644 --- a/ozwadmin-main/mainwindow.cpp +++ b/ozwadmin-main/mainwindow.cpp @@ -198,8 +198,10 @@ MainWindow::MainWindow(QWidget *parent) : QFileInfo directory(dir); qDebug() << directory.absoluteFilePath(); if (directory.exists()) { +#if 0 #ifndef _WIN32 copyConfigDatabase(directory.absoluteFilePath().append("/")); +#endif #endif m_configpath.setPath(directory.absoluteFilePath().append("/config/")); m_userpath.setPath(directory.absoluteFilePath().append("/config/")); diff --git a/ozwadmin-main/ozwadmin-main.pro b/ozwadmin-main/ozwadmin-main.pro index 6c9edc7..6bc50bd 100644 --- a/ozwadmin-main/ozwadmin-main.pro +++ b/ozwadmin-main/ozwadmin-main.pro @@ -5,7 +5,7 @@ #------------------------------------------------- QT += core gui widgets xml remoteobjects websockets svg -CONFIG += silent +#CONFIG += silent TARGET = ../ozwadmin TEMPLATE = app @@ -31,12 +31,10 @@ FORMS += mainwindow.ui \ RESOURCES += \ ozwadmin-main.qrc \ +include(../ozw-admin.pri) - -#LIBS += ../devicedb-lib/libdevicedb-lib.a ../ozwadmin-widgets/libozwadmin-widgets.a unix { LIBS += -L../devicedb-lib/ -ldevicedb-lib -L../ozwadmin-widgets/ -lozwadmin-widgets - LIBS += -L../../qt-openzwave/qt-openzwave/ -lqt-openzwave -L../../qt-openzwave/qt-openzwavedatabase -lqt-openzwavedatabase } windows { CONFIG(debug, debug|release) { @@ -51,8 +49,6 @@ windows { INCLUDEPATH += ../devicedb-lib ../ozwadmin-widgets -INCLUDEPATH += ../../qt-openzwave/qt-openzwave/include/ ../../qt-openzwave/qt-openzwavedatabase/include/ - macx: { LIBS += -framework IOKit -framework CoreFoundation diff --git a/qt-openzwave.pri b/qt-openzwave.pri deleted file mode 100644 index b303d71..0000000 --- a/qt-openzwave.pri +++ /dev/null @@ -1,2 +0,0 @@ -top_srcdir=$$PWD -top_builddir=$$shadowed($$PWD) From a1a8c50d15c77288d2940158e586c3af97513d0f Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Sat, 25 Jan 2020 12:25:40 +0800 Subject: [PATCH 02/18] widgets needs the path as well --- ozwadmin-widgets/ozwadmin-widgets.pro | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ozwadmin-widgets/ozwadmin-widgets.pro b/ozwadmin-widgets/ozwadmin-widgets.pro index 4899d2e..afe396f 100644 --- a/ozwadmin-widgets/ozwadmin-widgets.pro +++ b/ozwadmin-widgets/ozwadmin-widgets.pro @@ -44,7 +44,9 @@ FORMS += HelpEditorDlg.ui \ bitsetwidget.ui \ nodeflagswidget.ui -INCLUDEPATH += ../devicedb-lib ../ozwadmin-main ../../qt-openzwave/qt-openzwave/include/ +include(../ozw-admin.pri) + +INCLUDEPATH += ../devicedb-lib ../ozwadmin-main macx: { From f302c743e0df9ea86777c960e32992b7a6c193cd Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Sat, 25 Jan 2020 12:38:14 +0800 Subject: [PATCH 03/18] update this path --- ozw-admin.pri | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ozw-admin.pri b/ozw-admin.pri index 3219804..c1580de 100644 --- a/ozw-admin.pri +++ b/ozw-admin.pri @@ -82,7 +82,7 @@ unix { QTOZW_INCLUDE_PATH+=$$absolute_path($$QTOZW_INCLUDE_PATH/../../qt-openzwavedatabase/include/) QTOZW_LIBS="-L$$absolute_path($$QTOZW_LIB_PATH/qt-openzwave/)" QTOZW_LIBS+="-lqt-openzwave" - QTOZW_LIBS+="-L$$absolute_path($QTOZW_LIB_PATH/../qt-openzwavedatabase)" + QTOZW_LIBS+="-L$$absolute_path($QTOZW_LIB_PATH/qt-openzwavedatabase/)" QTOZW_LIBS+="-lqt-openzwavedatabase" message(" ") message("QT-OpenZWave Summary:") From a51a88f8a7073b6b3b1e409e92151fff26274b44 Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Sat, 25 Jan 2020 12:39:42 +0800 Subject: [PATCH 04/18] typo --- ozw-admin.pri | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ozw-admin.pri b/ozw-admin.pri index c1580de..bf1d17d 100644 --- a/ozw-admin.pri +++ b/ozw-admin.pri @@ -82,7 +82,7 @@ unix { QTOZW_INCLUDE_PATH+=$$absolute_path($$QTOZW_INCLUDE_PATH/../../qt-openzwavedatabase/include/) QTOZW_LIBS="-L$$absolute_path($$QTOZW_LIB_PATH/qt-openzwave/)" QTOZW_LIBS+="-lqt-openzwave" - QTOZW_LIBS+="-L$$absolute_path($QTOZW_LIB_PATH/qt-openzwavedatabase/)" + QTOZW_LIBS+="-L$$absolute_path($$QTOZW_LIB_PATH/qt-openzwavedatabase/)" QTOZW_LIBS+="-lqt-openzwavedatabase" message(" ") message("QT-OpenZWave Summary:") From c6d82b72fdc261065ddee211afea35189ae07cdc Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Sat, 25 Jan 2020 12:46:31 +0800 Subject: [PATCH 05/18] install target --- ozwadmin-main/ozwadmin-main.pro | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ozwadmin-main/ozwadmin-main.pro b/ozwadmin-main/ozwadmin-main.pro index 6bc50bd..86d89b2 100644 --- a/ozwadmin-main/ozwadmin-main.pro +++ b/ozwadmin-main/ozwadmin-main.pro @@ -34,6 +34,8 @@ RESOURCES += \ include(../ozw-admin.pri) unix { + target.path = /usr/local/bin + INSTALLS += target LIBS += -L../devicedb-lib/ -ldevicedb-lib -L../ozwadmin-widgets/ -lozwadmin-widgets } windows { From 9990d41284e2e4df482daf67533226c52f1b9765 Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Mon, 27 Jan 2020 11:32:55 +0800 Subject: [PATCH 06/18] fix up osx compiles --- osxmakebundle.sh | 21 -- ozw-admin.pri | 8 +- ozwadmin-main/ozwadmin-main.pro | 5 +- scripts/macdeployqtfix.py | 395 ++++++++++++++++++++++++++++++++ 4 files changed, 406 insertions(+), 23 deletions(-) delete mode 100755 osxmakebundle.sh create mode 100755 scripts/macdeployqtfix.py diff --git a/osxmakebundle.sh b/osxmakebundle.sh deleted file mode 100755 index e52fea9..0000000 --- a/osxmakebundle.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -if [ $# -lt 3 ]; then - echo "Please Provide the path to the App Bundle and OZW Config Directory and QT Directories" - exit 1 -fi -if [ ! -f $1/Contents/Frameworks/libqt-openzwave.1.dylib ]; then - echo "$1/Contents/Frameworks/libqt-openzwave.1.dylib doens't exist" - exit 1 -fi -if [ ! -f $2/manufacturer_specific.xml ]; then - echo "$2/manufacturer_specific.xml doesn't exist" - exit 1 -fi -if [ ! -f $3/bin/macdeployqt ]; then - echo "$3/bin/macdeployqt does't exist" - exit 1 -fi -PATH=`otool -L $1/Contents/Frameworks/libqt-openzwave.1.dylib | grep libopenzwave | awk '{print $1}'` -/usr/bin/install_name_tool -change $PATH "@rpath/${PATH##*/}" $1/Contents/Frameworks/libqt-openzwave.1.dylib -/bin/cp -r $2 $1/Contents/Resources/config/ -echo "To Finish Please Run '$3/bin/macdeployqt $1 -verbose=2 -always-overwrite'" diff --git a/ozw-admin.pri b/ozw-admin.pri index bf1d17d..de6b153 100644 --- a/ozw-admin.pri +++ b/ozw-admin.pri @@ -1,5 +1,11 @@ top_srcdir=$$PWD top_builddir=$$shadowed($$PWD) +unix { + macx { + QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.14 + } + !macx: QMAKE_CXXFLAGS += -Wno-deprecated-copy +} unix { CONFIG-= no-pkg-config @@ -100,7 +106,7 @@ unix { QTOZW_INCLUDE_PATH += $$absolute_path($$top_srcdir/../qt-openzwave/qt-openzwavedatabase/include/) QTOZW_LIBS="-L$$QTOZW_LIB_PATH" QTOZW_LIBS+="-lqt-openzwave" - QTOZW_LIBS+="-L$$absolute_path($$OTOZW_LIB_PATH/../qt-openzwavedatabase/)" + QTOZW_LIBS+="-L$$absolute_path($$QTOZW_LIB_PATH/../qt-openzwavedatabase/)" QTZOW_LIBS+="-lqt-openzwavedatabase" message(" ") message("QT-OpenZWave Summary:") diff --git a/ozwadmin-main/ozwadmin-main.pro b/ozwadmin-main/ozwadmin-main.pro index 86d89b2..369ffd4 100644 --- a/ozwadmin-main/ozwadmin-main.pro +++ b/ozwadmin-main/ozwadmin-main.pro @@ -54,9 +54,12 @@ INCLUDEPATH += ../devicedb-lib ../ozwadmin-widgets macx: { LIBS += -framework IOKit -framework CoreFoundation - BUNDLE.files = ../../qt-openzwave/qt-openzwave/libqt-openzwave.1.dylib ../../open-zwave/libopenzwave-1.6.dylib ../../qt-openzwave/qt-openzwavedatabase/libqt-openzwavedatabase.1.dylib + BUNDLE.files = $$OZW_LIB_PATH/libopenzwave-1.6.dylib $$QTOZW_LIB_PATH/libqt-openzwave.1.dylib $$QTOZW_LIB_PATH/../qt-openzwavedatabase/libqt-openzwavedatabase.1.dylib BUNDLE.path = Contents/Frameworks/ QMAKE_BUNDLE_DATA += BUNDLE + MakeBundle.commands = $$[QT_HOST_BINS]/macdeployqt ../ozwadmin.app && $$top_srcdir/scripts/macdeployqtfix.py ../ozwadmin.app/Contents/MacOS/ozwadmin $$[QT_INSTALL_PREFIX] + QMAKE_EXTRA_TARGETS += MakeBundle + QMAKE_POST_LINK += $$MakeBundle.commands ICON = res/ozw_logo.icns } diff --git a/scripts/macdeployqtfix.py b/scripts/macdeployqtfix.py new file mode 100755 index 0000000..1189c43 --- /dev/null +++ b/scripts/macdeployqtfix.py @@ -0,0 +1,395 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +finish the job started by macdeployqtfix +""" + +from subprocess import Popen, PIPE +from string import Template +import os +import sys +import logging +import argparse +import re +from collections import namedtuple + + +QTLIB_NAME_REGEX = r'^(?:@executable_path)?/.*/(Qt[a-zA-Z]*).framework/(?:Versions/\d/)?\1$' +QTLIB_NORMALIZED = r'$prefix/Frameworks/$qtlib.framework/Versions/$qtversion/$qtlib' + +QTPLUGIN_NAME_REGEX = r'^(?:@executable_path)?/.*/[pP]lug[iI]ns/(.*)/(.*).dylib$' +QTPLUGIN_NORMALIZED = r'$prefix/PlugIns/$plugintype/$pluginname.dylib' + +#BREWLIB_REGEX = r'^//usr/local/.*//(.*)' +BREWLIB_REGEX =r'(libqt-.*)' +BREWLIB_NORMALIZED = r'$prefix/Frameworks/$brewlib' + + +class GlobalConfig(object): + logger = None + qtpath = None + exepath = None + + +def run_and_get_output(popen_args): + """Run process and get all output""" + process_output = namedtuple('ProcessOutput', ['stdout', 'stderr', 'retcode']) + try: + GlobalConfig.logger.debug('run_and_get_output({0})'.format(repr(popen_args))) + + proc = Popen(popen_args, stdin=PIPE, stdout=PIPE, stderr=PIPE) + stdout, stderr = proc.communicate(b'') + proc_out = process_output(stdout, stderr, proc.returncode) + + GlobalConfig.logger.debug('\tprocess_output: {0}'.format(proc_out)) + return proc_out + except Exception as exc: + GlobalConfig.logger.error('\texception: {0}'.format(exc)) + return process_output('', exc.message, -1) + + +def get_dependencies(filename): + """ + input: filename must be an absolute path + Should call `otool` and returns the list of dependencies, unsorted, + unmodified, just the raw list so then we could eventually re-use in other + more specialized functions + """ + GlobalConfig.logger.debug('get_dependencies({0})'.format(filename)) + popen_args = ['otool', '-L', filename] + proc_out = run_and_get_output(popen_args) + deps = [] + if proc_out.retcode == 0: + # some string splitting + deps = [s.strip().split(' ')[0] for s in proc_out.stdout.splitlines()[1:] if s] + # prevent infinite recursion when a binary depends on itself (seen with QtWidgets)... + deps = [s for s in deps if os.path.basename(filename) not in s] + return deps + + +def is_qt_plugin(filename): + """ + Checks if a given file is a qt plugin. + Accepts absolute path as well as path containing @executable_path + """ + qtlib_name_rgx = re.compile(QTPLUGIN_NAME_REGEX) + return qtlib_name_rgx.match(filename) is not None + + +def is_qt_lib(filename): + """ + Checks if a given file is a qt library. + Accepts absolute path as well as path containing @executable_path + """ + qtlib_name_rgx = re.compile(QTLIB_NAME_REGEX) + return qtlib_name_rgx.match(filename) is not None + + +def is_brew_lib(filename): + """ + Checks if a given file is a brew library + Accepts absolute path as well as path containing @executable_path + """ + qtlib_name_rgx = re.compile(BREWLIB_REGEX) + return qtlib_name_rgx.match(filename) is not None + + +def normalize_qtplugin_name(filename): + """ + input: a path to a qt plugin, as returned by otool, that can have this form : + - an absolute path /../plugins/PLUGINTYPE/PLUGINNAME.dylib + - @executable_path/../plugins/PLUGINTYPE/PLUGINNAME.dylib + output: + a tuple (qtlib, abspath, rpath) where: + - qtname is the name of the plugin (libqcocoa.dylib, etc.) + - abspath is the absolute path of the qt lib inside the app bundle of exepath + - relpath is the correct rpath to a qt lib inside the app bundle + """ + + GlobalConfig.logger.debug('normalize_plugin_name({0})'.format(filename)) + + qtplugin_name_rgx = re.compile(QTPLUGIN_NAME_REGEX) + rgxret = qtplugin_name_rgx.match(filename) + if not rgxret: + msg = 'couldn\'t normalize a non-qt plugin filename: {0}'.format(filename) + GlobalConfig.logger.critical(msg) + raise Exception(msg) + + # qtplugin normalization settings + qtplugintype = rgxret.groups()[0] + qtpluginname = rgxret.groups()[1] + + templ = Template(QTPLUGIN_NORMALIZED) + + # from qtlib, forge 2 path : + # - absolute path of qt lib in bundle, + abspath = os.path.normpath(templ.safe_substitute( + prefix=os.path.dirname(GlobalConfig.exepath) + '/..', + plugintype=qtplugintype, + pluginname=qtpluginname)) + + # - and rpath containing @executable_path, relative to exepath + rpath = templ.safe_substitute( + prefix='@executable_path/..', + plugintype=qtplugintype, + pluginname=qtpluginname) + + GlobalConfig.logger.debug('\treturns({0})'.format((qtpluginname, abspath, rpath))) + return qtpluginname, abspath, rpath + + +def normalize_qtlib_name(filename): + """ + input: a path to a qt library, as returned by otool, that can have this form : + - an absolute path /lib/xxx/yyy + - @executable_path/../Frameworks/QtSerialPort.framework/Versions/5/QtSerialPort + output: + a tuple (qtlib, abspath, rpath) where: + - qtlib is the name of the qtlib (QtCore, QtWidgets, etc.) + - abspath is the absolute path of the qt lib inside the app bundle of exepath + - relpath is the correct rpath to a qt lib inside the app bundle + """ + GlobalConfig.logger.debug('normalize_qtlib_name({0})'.format(filename)) + + qtlib_name_rgx = re.compile(QTLIB_NAME_REGEX) + rgxret = qtlib_name_rgx.match(filename) + if not rgxret: + msg = 'couldn\'t normalize a non-qt lib filename: {0}'.format(filename) + GlobalConfig.logger.critical(msg) + raise Exception(msg) + + # qtlib normalization settings + qtlib = rgxret.groups()[0] + qtversion = 5 + + templ = Template(QTLIB_NORMALIZED) + + # from qtlib, forge 2 path : + # - absolute path of qt lib in bundle, + abspath = os.path.normpath(templ.safe_substitute( + prefix=os.path.dirname(GlobalConfig.exepath) + '/..', + qtlib=qtlib, + qtversion=qtversion)) + + # - and rpath containing @executable_path, relative to exepath + rpath = templ.safe_substitute( + prefix='@executable_path/..', + qtlib=qtlib, + qtversion=qtversion) + + GlobalConfig.logger.debug('\treturns({0})'.format((qtlib, abspath, rpath))) + return qtlib, abspath, rpath + + +def normalize_brew_name(filename): + """ + input: a path to a brew library, as returned by otool, that can have this form : + - an absolute path /usr/local/lib/yyy + output: + a tuple (brewlib, abspath, rpath) where: + - brewlib is the name of the brew lib + - abspath is the absolute path of the qt lib inside the app bundle of exepath + - relpath is the correct rpath to a qt lib inside the app bundle + """ + GlobalConfig.logger.debug('normalize_brew_name({0})'.format(filename)) + + brewlib_name_rgx = re.compile(BREWLIB_REGEX) + rgxret = brewlib_name_rgx.match(filename) + if not rgxret: + msg = 'couldn\'t normalize a brew lib filename: {0}'.format(filename) + GlobalConfig.logger.critical(msg) + raise Exception(msg) + + # brewlib normalization settings + brewlib = rgxret.groups()[0] + templ = Template(BREWLIB_NORMALIZED) + + # from brewlib, forge 2 path : + # - absolute path of qt lib in bundle, + abspath = os.path.normpath(templ.safe_substitute( + prefix=os.path.dirname(GlobalConfig.exepath) + '/..', + brewlib=brewlib)) + + # - and rpath containing @executable_path, relative to exepath + rpath = templ.safe_substitute( + prefix='@executable_path/..', + brewlib=brewlib) + + GlobalConfig.logger.debug('\treturns({0})'.format((brewlib, abspath, rpath))) + return brewlib, abspath, rpath + + +def fix_dependency(binary, dep): + """ + fix 'dep' dependency of 'binary'. 'dep' is a qt library + """ + if is_qt_lib(dep): + qtname, dep_abspath, dep_rpath = normalize_qtlib_name(dep) + elif is_qt_plugin(dep): + qtname, dep_abspath, dep_rpath = normalize_qtplugin_name(dep) + elif is_brew_lib(dep): + qtname, dep_abspath, dep_rpath = normalize_brew_name(dep) + else: + return True + + dep_ok = True + # check that rpath of 'dep' inside binary has been correctly set + # (ie: relative to exepath using '@executable_path' syntax) + if dep != dep_rpath: + # dep rpath is not ok + GlobalConfig.logger.info('changing rpath \'{0}\' in binary {1}'.format(dep, binary)) + + # call install_name_tool -change on binary + popen_args = ['install_name_tool', '-change', dep, dep_rpath, binary] + proc_out = run_and_get_output(popen_args) + if proc_out.retcode != 0: + GlobalConfig.logger.error(proc_out.stderr) + dep_ok = False + else: + # call install_name_tool -id on binary + popen_args = ['install_name_tool', '-id', dep_rpath, binary] + proc_out = run_and_get_output(popen_args) + if proc_out.retcode != 0: + GlobalConfig.logger.error(proc_out.stderr) + dep_ok = False + + # now ensure that 'dep' exists at the specified path, relative to bundle + if dep_ok and not os.path.exists(dep_abspath): + + # ensure destination directory exists + GlobalConfig.logger.info('ensuring directory \'{0}\' exists: {0}'. + format(os.path.dirname(dep_abspath))) + popen_args = ['mkdir', '-p', os.path.dirname(dep_abspath)] + proc_out = run_and_get_output(popen_args) + if proc_out.retcode != 0: + GlobalConfig.logger.info(proc_out.stderr) + dep_ok = False + else: + # copy missing dependency into bundle + qtnamesrc = os.path.join(GlobalConfig.qtpath, 'lib', '{0}.framework'. + format(qtname), qtname) + GlobalConfig.logger.info('copying missing dependency in bundle: {0}'. + format(qtname)) + popen_args = ['cp', qtnamesrc, dep_abspath] + proc_out = run_and_get_output(popen_args) + if proc_out.retcode != 0: + GlobalConfig.logger.info(proc_out.stderr) + dep_ok = False + else: + # ensure permissions are correct if we ever have to change its rpath + GlobalConfig.logger.info('ensuring 755 perm to {0}'.format(dep_abspath)) + popen_args = ['chmod', '755', dep_abspath] + proc_out = run_and_get_output(popen_args) + if proc_out.retcode != 0: + GlobalConfig.logger.info(proc_out.stderr) + dep_ok = False + else: + GlobalConfig.logger.debug('{0} is at correct location in bundle'.format(qtname)) + + if dep_ok: + return fix_binary(dep_abspath) + return False + + +def fix_binary(binary): + """ + input: + binary: relative or absolute path (no @executable_path syntax) + process: + - first fix the rpath for the qt libs on which 'binary' depend + - copy into the bundle of exepath the eventual libraries that are missing + - (create the soft links) needed ? + - do the same for all qt dependencies of binary (recursive) + """ + GlobalConfig.logger.debug('fix_binary({0})'.format(binary)) + + # loop on 'binary' dependencies + for dep in get_dependencies(binary): + if not fix_dependency(binary, dep): + GlobalConfig.logger.error('quitting early: couldn\'t fix dependency {0} of {1}'.format(dep, binary)) + return False + return True + + +def fix_main_binaries(): + """ + list the main binaries of the app bundle and fix them + """ + # deduce bundle path + bundlepath = os.path.sep.join(GlobalConfig.exepath.split(os.path.sep)[0:-3]) + + # fix main binary + GlobalConfig.logger.info('fixing executable \'{0}\''.format(GlobalConfig.exepath)) + if fix_binary(GlobalConfig.exepath): + GlobalConfig.logger.info('fixing plugins') + for root, dummy, files in os.walk(bundlepath): + for name in [f for f in files if os.path.splitext(f)[1] == '.dylib']: + GlobalConfig.logger.info('fixing plugin {0}'.format(name)) + if not fix_binary(os.path.join(root, name)): + return False + return True + + +def main(): + descr = """finish the job started by macdeployqt! + - find dependencies/rpaths with otool + - copy missed dependencies with cp and mkdir + - fix missed rpaths with install_name_tool + + exit codes: + - 0 : success + - 1 : error + """ + + parser = argparse.ArgumentParser(description=descr, + formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument('exepath', + help='path to the binary depending on Qt') + parser.add_argument('qtpath', + help='path of Qt libraries used to build the Qt application') + parser.add_argument('-q', '--quiet', action='store_true', default=False, + help='do not create log on standard output') + parser.add_argument('-nl', '--no-log-file', action='store_true', default=False, + help='do not create log file \'./macdeployqtfix.log\'') + parser.add_argument('-v', '--verbose', action='store_true', default=False, + help='produce more log messages(debug log)') + args = parser.parse_args() + + # globals + GlobalConfig.qtpath = os.path.normpath(args.qtpath) + GlobalConfig.exepath = args.exepath + GlobalConfig.logger = logging.getLogger() + + # configure logging + ################### + + # create formatter + formatter = logging.Formatter('%(levelname)s | %(message)s') + # create console GlobalConfig.logger + if not args.quiet: + chdlr = logging.StreamHandler(sys.stdout) + chdlr.setFormatter(formatter) + GlobalConfig.logger.addHandler(chdlr) + + # create file GlobalConfig.logger + if not args.no_log_file: + fhdlr = logging.FileHandler('./macdeployqtfix.log', mode='w') + fhdlr.setFormatter(formatter) + GlobalConfig.logger.addHandler(fhdlr) + + if args.no_log_file and args.quiet: + GlobalConfig.logger.addHandler(logging.NullHandler()) + else: + GlobalConfig.logger.setLevel(logging.DEBUG if args.verbose else logging.INFO) + + if fix_main_binaries(): + GlobalConfig.logger.info('macdeployqtfix terminated with success') + ret = 0 + else: + GlobalConfig.logger.error('macdeployqtfix terminated with error') + ret = 1 + sys.exit(ret) + + +if __name__ == "__main__": + main() From bb2e7b989a6d35def9d5fb01df394ac4ce2d201a Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Mon, 27 Jan 2020 17:44:41 +0800 Subject: [PATCH 07/18] Update Python executable name --- scripts/macdeployqtfix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/macdeployqtfix.py b/scripts/macdeployqtfix.py index 1189c43..a37be84 100755 --- a/scripts/macdeployqtfix.py +++ b/scripts/macdeployqtfix.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python # -*- coding: utf-8 -*- """ finish the job started by macdeployqtfix From 2d9785809e3432d1b53f850e3f5f8d93348da563 Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Mon, 27 Jan 2020 19:31:54 +0800 Subject: [PATCH 08/18] Fix Windows Builds --- ozw-admin.pri | 2 ++ scripts/win32build.bat | 1 + 2 files changed, 3 insertions(+) diff --git a/ozw-admin.pri b/ozw-admin.pri index de6b153..3dbc738 100644 --- a/ozw-admin.pri +++ b/ozw-admin.pri @@ -144,6 +144,8 @@ win32 { exists( $$top_srcdir/../open-zwave/cpp/src/) { message("Found OZW in $$absolute_path($$top_srcdir/../open-zwave/cpp/src)") INCLUDEPATH += $$absolute_path($$top_srcdir/../open-zwave/cpp/src/)/ + INCLUDEPATH += $$absolute_path($$top_srcdir/../qt-openzwave/qt-openzwave/include/) + INCLUDEPATH += $$absolute_path($$top_srcdir/../qt-openzwave/qt-openzwavedatabase/include/) equals(BUILDTYPE, "release") { exists( $$absolute_path($$top_srcdir/../open-zwave/cpp/build/windows/vs2010/ReleaseDLL/OpenZWave.dll ) ) { LIBS += -L$$absolute_path($$top_srcdir/../open-zwave/cpp/build/windows/vs2010/ReleaseDLL) -lopenzwave diff --git a/scripts/win32build.bat b/scripts/win32build.bat index 9a9fd89..3049fb1 100644 --- a/scripts/win32build.bat +++ b/scripts/win32build.bat @@ -1,6 +1,7 @@ @echo off cd ..\open-zwave msbuild /p:Configuration=ReleaseDLL /p:Platform=Win32 cpp\build\windows\vs2010\OpenZWave.sln +msbuild /p:Configuration=DebugDLL /p:Platform=Win32 cpp\build\windows\vs2010\OpenZWave.sln cd ..\qt-openzwave qmake -r -tp vc msbuild /p:Configuration=Release /p:Platform=Win32 qt-openzwave.sln From d640f88673ac11f85b7c1abf3ba233929d0d84c1 Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Tue, 28 Jan 2020 15:27:07 +0800 Subject: [PATCH 09/18] AppImage Docker Builds --- scripts/Dockerfile | 11 +++++++++++ scripts/build.sh | 16 ++++++++++++++++ scripts/ozwadmin.desktop | 8 ++++++++ scripts/ozwadmin.png | Bin 0 -> 31219 bytes 4 files changed, 35 insertions(+) create mode 100644 scripts/Dockerfile create mode 100755 scripts/build.sh create mode 100644 scripts/ozwadmin.desktop create mode 100644 scripts/ozwadmin.png diff --git a/scripts/Dockerfile b/scripts/Dockerfile new file mode 100644 index 0000000..829b075 --- /dev/null +++ b/scripts/Dockerfile @@ -0,0 +1,11 @@ +#DockerFile to build a image capabile of Building AppImages. See build.sh for details +FROM ubuntu:xenial +WORKDIR /opt + +RUN apt update && apt-get install -y software-properties-common && add-apt-repository ppa:beineri/opt-qt-5.12.6-xenial && \ +apt update && apt-get install -y qt512-meta-minimal qt512remoteobjects rapidjson-dev git g++ cmake make pkgconf bash python wget joe mc libunwind-dev libcurl4-openssl-dev qt512svg qt512websockets mesa-common-dev libgl1-mesa-dev fuse appstream && \ +wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage && \ +wget https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage && \ +chmod +x linuxdeploy*.AppImage +VOLUME /opt/buildfiles +ENTRYPOINT /opt/buildfiles/build.sh \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..4906acd --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,16 @@ +#!/bin/bash +#Build File for Docker Image. +#Run docker in this directory with: docker run -it --rm --device /dev/fuse --privileged -v`pwd`:/opt/buildfiles -e DOCKERUSERID=`id -u` -e DOCKERGROUPID=`id -g` +cd /opt +git clone https://github.com/OpenZWave/open-zwave.git && cd open-zwave && make -j4 && make install +cd /opt +git clone https://github.com/OpenZWave/qt-openzwave.git && cd qt-openzwave && /opt/qt512/bin/qmake -r && make -j4 && make install +cd /opt +git clone https://github.com/OpenZWave/ozw-admin.git && cd ozw-admin && git checkout buildfixes && /opt/qt512/bin/qmake -r && make -j4 && make install +cd /opt +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib64 +export QMAKE=/opt/qt512/bin/qmake +export VERSION=0.1 +cd ozw-admin && ../linuxdeploy-x86_64.AppImage --appdir AppDir -e ozwadmin --plugin qt --output appimage -d scripts/ozwadmin.desktop -i scripts/ozwadmin.png +cp /opt/ozw-admin/OZWAdmin-*.AppImage /opt/buildfiles/ +chown $DOCKERUSERID:$DOCKERGROUPID /opt/buildfiles/OZWAdmin-*.AppImage diff --git a/scripts/ozwadmin.desktop b/scripts/ozwadmin.desktop new file mode 100644 index 0000000..8e70a72 --- /dev/null +++ b/scripts/ozwadmin.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=OZWAdmin +Comment=OpenZWave Administration Gui +Exec=ozwadmin +Type=Application +Icon=ozwadmin +Categories=System; +MimeType=application/x-iso9660-appimage; \ No newline at end of file diff --git a/scripts/ozwadmin.png b/scripts/ozwadmin.png new file mode 100644 index 0000000000000000000000000000000000000000..c5c5450f9455ca18085ee566fc7cbb928740bda0 GIT binary patch literal 31219 zcmV+CKp4M?P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 arJ3jd{|O@}r_I0 Date: Tue, 28 Jan 2020 18:17:10 +0800 Subject: [PATCH 10/18] Add dmgbuild script --- scripts/dmgbuild.py | 205 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 scripts/dmgbuild.py diff --git a/scripts/dmgbuild.py b/scripts/dmgbuild.py new file mode 100644 index 0000000..cc75434 --- /dev/null +++ b/scripts/dmgbuild.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import biplist +import os.path + +# +# Example settings file for dmgbuild +# + +# Use like this: dmgbuild -s settings.py "Test Volume" test.dmg + +# You can actually use this file for your own application (not just TextEdit) +# by doing e.g. +# +# dmgbuild -s settings.py -D app=/path/to/My.app "My Application" MyApp.dmg + +# .. Useful stuff .............................................................. + +application = defines.get('app', '../ozwadmin.app') +appname = os.path.basename(application) + +def icon_from_app(app_path): + plist_path = os.path.join(app_path, 'Contents', 'Info.plist') + plist = biplist.readPlist(plist_path) + icon_name = plist['CFBundleIconFile'] + icon_root,icon_ext = os.path.splitext(icon_name) + if not icon_ext: + icon_ext = '.icns' + icon_name = icon_root + icon_ext + return os.path.join(app_path, 'Contents', 'Resources', icon_name) + +# .. Basics .................................................................... + +# Uncomment to override the output filename +# filename = 'test.dmg' + +# Uncomment to override the output volume name +# volume_name = 'Test' + +# Volume format (see hdiutil create -help) +format = defines.get('format', 'UDBZ') + +# Compression level (if relevant) +# compression_level = 9 + +# Volume size +size = defines.get('size', None) + +# Files to include +files = [ application ] + +# Symlinks to create +symlinks = { 'Applications': '/Applications' } + +# Volume icon +# +# You can either define icon, in which case that icon file will be copied to the +# image, *or* you can define badge_icon, in which case the icon file you specify +# will be used to badge the system's Removable Disk icon. Badge icons require +# pyobjc-framework-Quartz. +# +#icon = '/path/to/icon.icns' +badge_icon = icon_from_app(application) + +# Where to put the icons +icon_locations = { + appname: (140, 120), + 'Applications': (500, 120) + } + +# .. Window configuration ...................................................... + +# Background +# +# This is a STRING containing any of the following: +# +# #3344ff - web-style RGB color +# #34f - web-style RGB color, short form (#34f == #3344ff) +# rgb(1,0,0) - RGB color, each value is between 0 and 1 +# hsl(120,1,.5) - HSL (hue saturation lightness) color +# hwb(300,0,0) - HWB (hue whiteness blackness) color +# cmyk(0,1,0,0) - CMYK color +# goldenrod - X11/SVG named color +# builtin-arrow - A simple built-in background with a blue arrow +# /foo/bar/baz.png - The path to an image file +# +# The hue component in hsl() and hwb() may include a unit; it defaults to +# degrees ('deg'), but also supports radians ('rad') and gradians ('grad' +# or 'gon'). +# +# Other color components may be expressed either in the range 0 to 1, or +# as percentages (e.g. 60% is equivalent to 0.6). +background = 'builtin-arrow' + +show_status_bar = False +show_tab_view = False +show_toolbar = False +show_pathbar = False +show_sidebar = False +sidebar_width = 180 + +# Window position in ((x, y), (w, h)) format +window_rect = ((100, 100), (640, 280)) + +# Select the default view; must be one of +# +# 'icon-view' +# 'list-view' +# 'column-view' +# 'coverflow' +# +default_view = 'icon-view' + +# General view configuration +show_icon_preview = False + +# Set these to True to force inclusion of icon/list view settings (otherwise +# we only include settings for the default view) +include_icon_view_settings = 'auto' +include_list_view_settings = 'auto' + +# .. Icon view configuration ................................................... + +arrange_by = None +grid_offset = (0, 0) +grid_spacing = 100 +scroll_position = (0, 0) +label_pos = 'bottom' # or 'right' +text_size = 16 +icon_size = 128 + +# .. List view configuration ................................................... + +# Column names are as follows: +# +# name +# date-modified +# date-created +# date-added +# date-last-opened +# size +# kind +# label +# version +# comments +# +list_icon_size = 16 +list_text_size = 12 +list_scroll_position = (0, 0) +list_sort_by = 'name' +list_use_relative_dates = True +list_calculate_all_sizes = False, +list_columns = ('name', 'date-modified', 'size', 'kind', 'date-added') +list_column_widths = { + 'name': 300, + 'date-modified': 181, + 'date-created': 181, + 'date-added': 181, + 'date-last-opened': 181, + 'size': 97, + 'kind': 115, + 'label': 100, + 'version': 75, + 'comments': 300, + } +list_column_sort_directions = { + 'name': 'ascending', + 'date-modified': 'descending', + 'date-created': 'descending', + 'date-added': 'descending', + 'date-last-opened': 'descending', + 'size': 'descending', + 'kind': 'ascending', + 'label': 'ascending', + 'version': 'ascending', + 'comments': 'ascending', + } + +# .. License configuration ..................................................... + +# Text in the license configuration is stored in the resources, which means +# it gets stored in a legacy Mac encoding according to the language. dmgbuild +# will *try* to convert Unicode strings to the appropriate encoding, *but* +# you should be aware that Python doesn't support all of the necessary encodings; +# in many cases you will need to encode the text yourself and use byte strings +# instead here. + +# Recognized language names are: +# +# af_ZA, ar, be_BY, bg_BG, bn, bo, br, ca_ES, cs_CZ, cy, da_DK, de_AT, de_CH, +# de_DE, dz_BT, el_CY, el_GR, en_AU, en_CA, en_GB, en_IE, en_SG, en_US, eo, +# es_419, es_ES, et_EE, fa_IR, fi_FI, fo_FO, fr_001, fr_BE, fr_CA, fr_CH, +# fr_FR, ga-Latg_IE, ga_IE, gd, grc, gu_IN, gv, he_IL, hi_IN, hr_HR, hu_HU, +# hy_AM, is_IS, it_CH, it_IT, iu_CA, ja_JP, ka_GE, kl, ko_KR, lt_LT, lv_LV, +# mk_MK, mr_IN, mt_MT, nb_NO, ne_NP, nl_BE, nl_NL, nn_NO, pa, pl_PL, pt_BR, +# pt_PT, ro_RO, ru_RU, se, sk_SK, sl_SI, sr_RS, sv_SE, th_TH, to_TO, tr_TR, +# uk_UA, ur_IN, ur_PK, uz_UZ, vi_VN, zh_CN, zh_TW + +license = { + 'default-language': 'en_US', + 'licenses': { + 'en_US': '../LICENSE' + }, + } From ec2f3b7ea9af7285070438ae95b7997c992cb53e Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Tue, 28 Jan 2020 20:18:49 +0800 Subject: [PATCH 11/18] fix up rpath of binary --- scripts/macdeployqtfix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/macdeployqtfix.py b/scripts/macdeployqtfix.py index a37be84..ea67b38 100755 --- a/scripts/macdeployqtfix.py +++ b/scripts/macdeployqtfix.py @@ -21,7 +21,7 @@ QTPLUGIN_NAME_REGEX = r'^(?:@executable_path)?/.*/[pP]lug[iI]ns/(.*)/(.*).dylib$ QTPLUGIN_NORMALIZED = r'$prefix/PlugIns/$plugintype/$pluginname.dylib' #BREWLIB_REGEX = r'^//usr/local/.*//(.*)' -BREWLIB_REGEX =r'(libqt-.*)' +BREWLIB_REGEX =r'.*(.*openzwave.*dylib)' BREWLIB_NORMALIZED = r'$prefix/Frameworks/$brewlib' @@ -254,7 +254,7 @@ def fix_dependency(binary, dep): dep_ok = False # now ensure that 'dep' exists at the specified path, relative to bundle - if dep_ok and not os.path.exists(dep_abspath): + if False and dep_ok and not os.path.exists(dep_abspath): # ensure destination directory exists GlobalConfig.logger.info('ensuring directory \'{0}\' exists: {0}'. From 1e9d170b6eaad4381e9fbf7c1c30a4ac95175fcc Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Tue, 28 Jan 2020 21:48:56 +0800 Subject: [PATCH 12/18] really fix rpath setup now --- scripts/macdeployqtfix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/macdeployqtfix.py b/scripts/macdeployqtfix.py index ea67b38..29d3810 100755 --- a/scripts/macdeployqtfix.py +++ b/scripts/macdeployqtfix.py @@ -21,7 +21,7 @@ QTPLUGIN_NAME_REGEX = r'^(?:@executable_path)?/.*/[pP]lug[iI]ns/(.*)/(.*).dylib$ QTPLUGIN_NORMALIZED = r'$prefix/PlugIns/$plugintype/$pluginname.dylib' #BREWLIB_REGEX = r'^//usr/local/.*//(.*)' -BREWLIB_REGEX =r'.*(.*openzwave.*dylib)' +BREWLIB_REGEX =r'.*(lib.*openzwave.*dylib)' BREWLIB_NORMALIZED = r'$prefix/Frameworks/$brewlib' From f0e48bacdf329ddcc4da2a01b370d9fa25057422 Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Tue, 28 Jan 2020 22:25:22 +0800 Subject: [PATCH 13/18] Link in openzwavedatabase --- ozw-admin.pri | 2 +- ozwadmin-main/mainwindow.cpp | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ozw-admin.pri b/ozw-admin.pri index 3dbc738..83d3ed7 100644 --- a/ozw-admin.pri +++ b/ozw-admin.pri @@ -107,7 +107,7 @@ unix { QTOZW_LIBS="-L$$QTOZW_LIB_PATH" QTOZW_LIBS+="-lqt-openzwave" QTOZW_LIBS+="-L$$absolute_path($$QTOZW_LIB_PATH/../qt-openzwavedatabase/)" - QTZOW_LIBS+="-lqt-openzwavedatabase" + QTOZW_LIBS+="-lqt-openzwavedatabase" message(" ") message("QT-OpenZWave Summary:") message(" QT-OpenZWave Library Path: $$QTOZW_LIB_PATH") diff --git a/ozwadmin-main/mainwindow.cpp b/ozwadmin-main/mainwindow.cpp index ed0195d..561930f 100644 --- a/ozwadmin-main/mainwindow.cpp +++ b/ozwadmin-main/mainwindow.cpp @@ -198,6 +198,9 @@ MainWindow::MainWindow(QWidget *parent) : QFileInfo directory(dir); qDebug() << directory.absoluteFilePath(); if (directory.exists()) { + QStringList dirs; + dirs << directory.absoluteFilePath().append("/"); + initConfigDatabase(dirs); #if 0 #ifndef _WIN32 copyConfigDatabase(directory.absoluteFilePath().append("/")); From 58982ce99ad7601cc5ba70dc74da280373ffd402 Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Tue, 28 Jan 2020 22:33:17 +0800 Subject: [PATCH 14/18] Fix Windows Build --- ozwadmin-main/ozwadmin-main.pro | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ozwadmin-main/ozwadmin-main.pro b/ozwadmin-main/ozwadmin-main.pro index 369ffd4..10d0e21 100644 --- a/ozwadmin-main/ozwadmin-main.pro +++ b/ozwadmin-main/ozwadmin-main.pro @@ -40,11 +40,11 @@ unix { } windows { CONFIG(debug, debug|release) { - LIBS += -L..\devicedb-lib\debug\ -L..\ozwadmin-widgets\debug\ -L..\..\qt-openzwave\qt-openzwave\debug\ + LIBS += -L..\devicedb-lib\debug\ -L..\ozwadmin-widgets\debug\ -L..\..\qt-openzwave\qt-openzwave\debug\ -L..\..\qt-openzwave\qt-openzwavedatabase\debug\ } else { - LIBS += -L..\devicedb-lib\release\ -L..\ozwadmin-widgets\release\ -L..\..\qt-openzwave\qt-openzwave\release\ + LIBS += -L..\devicedb-lib\release\ -L..\ozwadmin-widgets\release\ -L..\..\qt-openzwave\qt-openzwave\release\ -L..\..\qt-openzwave\qt-openzwavedatabase\release\ } - LIBS += -ldevicedb-lib -lozwadmin-widgets -lqt-openzwave1 + LIBS += -ldevicedb-lib -lozwadmin-widgets -lqt-openzwave1 -lqt-openzwavedatabase1 message($$LIBS) message($$PWD) } From 9f2f4fd2e7efec3443985f26821f064e6baa4ac6 Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Tue, 28 Jan 2020 22:41:04 +0800 Subject: [PATCH 15/18] We don't want the openzwavedatabase on Windows just yet --- ozwadmin-main/mainwindow.cpp | 2 +- ozwadmin-main/ozwadmin-main.pro | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ozwadmin-main/mainwindow.cpp b/ozwadmin-main/mainwindow.cpp index 561930f..58c0709 100644 --- a/ozwadmin-main/mainwindow.cpp +++ b/ozwadmin-main/mainwindow.cpp @@ -198,11 +198,11 @@ MainWindow::MainWindow(QWidget *parent) : QFileInfo directory(dir); qDebug() << directory.absoluteFilePath(); if (directory.exists()) { +#ifndef _WIN32 QStringList dirs; dirs << directory.absoluteFilePath().append("/"); initConfigDatabase(dirs); #if 0 -#ifndef _WIN32 copyConfigDatabase(directory.absoluteFilePath().append("/")); #endif #endif diff --git a/ozwadmin-main/ozwadmin-main.pro b/ozwadmin-main/ozwadmin-main.pro index 10d0e21..8c02adf 100644 --- a/ozwadmin-main/ozwadmin-main.pro +++ b/ozwadmin-main/ozwadmin-main.pro @@ -40,11 +40,11 @@ unix { } windows { CONFIG(debug, debug|release) { - LIBS += -L..\devicedb-lib\debug\ -L..\ozwadmin-widgets\debug\ -L..\..\qt-openzwave\qt-openzwave\debug\ -L..\..\qt-openzwave\qt-openzwavedatabase\debug\ + LIBS += -L..\devicedb-lib\debug\ -L..\ozwadmin-widgets\debug\ -L..\..\qt-openzwave\qt-openzwave\debug\ } else { - LIBS += -L..\devicedb-lib\release\ -L..\ozwadmin-widgets\release\ -L..\..\qt-openzwave\qt-openzwave\release\ -L..\..\qt-openzwave\qt-openzwavedatabase\release\ + LIBS += -L..\devicedb-lib\release\ -L..\ozwadmin-widgets\release\ -L..\..\qt-openzwave\qt-openzwave\release\ } - LIBS += -ldevicedb-lib -lozwadmin-widgets -lqt-openzwave1 -lqt-openzwavedatabase1 + LIBS += -ldevicedb-lib -lozwadmin-widgets -lqt-openzwave1 message($$LIBS) message($$PWD) } From b51a1fc1470e61703166ff011ac9062c2f6a6940 Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Tue, 28 Jan 2020 22:46:07 +0800 Subject: [PATCH 16/18] Update Path to License --- scripts/dmgbuild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/dmgbuild.py b/scripts/dmgbuild.py index cc75434..d1af1ca 100644 --- a/scripts/dmgbuild.py +++ b/scripts/dmgbuild.py @@ -200,6 +200,6 @@ list_column_sort_directions = { license = { 'default-language': 'en_US', 'licenses': { - 'en_US': '../LICENSE' + 'en_US': './LICENSE' }, } From 001c8002cdc9603186cf1d4ce860c9ca8249f1cb Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Tue, 28 Jan 2020 23:30:00 +0800 Subject: [PATCH 17/18] Enable Remote Connection --- ozwadmin-main/mainwindow.cpp | 28 ++++++++++++++++++++++++++++ ozwadmin-main/mainwindow.h | 2 ++ ozwadmin-main/mainwindow.ui | 20 +++++++++++++++++++- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/ozwadmin-main/mainwindow.cpp b/ozwadmin-main/mainwindow.cpp index 58c0709..d8fdfbc 100644 --- a/ozwadmin-main/mainwindow.cpp +++ b/ozwadmin-main/mainwindow.cpp @@ -55,6 +55,8 @@ MainWindow::MainWindow(QWidget *parent) : connect(ui->actionOpen_Log_Window, SIGNAL(triggered()), this, SLOT(openLogWindow())); connect(ui->actionOpen_Serial_Port, SIGNAL(triggered()), this, SLOT(OpenSerialPort())); + connect(ui->actionOpen_Remote, SIGNAL(triggered()), this, SLOT(OpenRemote())); + connect(ui->action_Close, SIGNAL(triggered()), this, SLOT(CloseConnection())); connect(ui->actionDevice_Database, SIGNAL(triggered()), this, SLOT(OpenDeviceDB())); connect(ui->md_helpwindow, &QPushButton::clicked, this, &MainWindow::openMetaDataWindow); connect(ui->action_Configuration, SIGNAL(triggered()), this, SLOT(openConfigWindow())); @@ -325,10 +327,36 @@ void MainWindow::QTOZW_Ready() { this->m_statTimer.start(1000); } +void MainWindow::OpenRemote() { + bool ok; + + QString text = QInputDialog::getText(this, tr("Enter Remote Host"), + tr("Hostname/IP Address:"), QLineEdit::Normal, + "localhost", &ok); + if (!ok) + return; + + QUrl server; + server.setHost(text); + server.setPort(1983); + server.setScheme("tcp"); + qDebug() << "Connecting to " << server; + this->m_qtozwmanager->initilizeReplica(server); + +} +void MainWindow::CloseConnection() { + +} + void MainWindow::OpenSerialPort() { bool ok; + + QMessageBox::warning(this, tr("OZW-Admin"), + tr("This is Work In Progress!"), + QMessageBox::Ok); + QString text = QInputDialog::getText(this, tr("Select Serial Port"), tr("Serial Port:"), QLineEdit::Normal, this->m_serialport, &ok); diff --git a/ozwadmin-main/mainwindow.h b/ozwadmin-main/mainwindow.h index 88f4016..4733eac 100644 --- a/ozwadmin-main/mainwindow.h +++ b/ozwadmin-main/mainwindow.h @@ -42,6 +42,8 @@ public: Q_PROPERTY(QString SerialPort MEMBER m_serialport) public slots: void OpenSerialPort(); + void OpenRemote(); + void CloseConnection(); void resizeColumns(); void NodeSelected(QModelIndex,QModelIndex); void openLogWindow(); diff --git a/ozwadmin-main/mainwindow.ui b/ozwadmin-main/mainwindow.ui index 222005a..bf97c32 100644 --- a/ozwadmin-main/mainwindow.ui +++ b/ozwadmin-main/mainwindow.ui @@ -907,7 +907,7 @@ 0 0 - 98 + 85 71 @@ -1086,6 +1086,8 @@ false + + @@ -1120,6 +1122,22 @@ &Device Database + + + Open &Remote + + + Open Remote + + + + + &Close + + + Close Local or Remote Connection + + From 30ac4d88364fff560802571d067006dc529bbb9e Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Wed, 29 Jan 2020 00:22:49 +0800 Subject: [PATCH 18/18] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index f8e1028..4aa31b5 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,9 @@ This is a Gui for OpenZWave, intended to replace the open-zwave-control-panel eventually. Work in Progress. Don't expect much right now :) + +If you are using the ozwdaemon docker image, you need to allow remote network access to the Docker Image: + +```docker run -it --security-opt seccomp=unconfined --device=/dev/ttyUSB0 -v /tmp/ozw:/opt/ozw/config -e MQTT_SERVER="10.100.200.102" -e USBPATH=/dev/ttyUSB0 --network host openzwave/ozwdaemon:latest``` + +Then in the OZWAdmin gui, when opening a "Remote Connection" specify the IP address of the Host the Docker Image is running on.