diff --git a/3rdparty/macdeployqt/CMakeLists.txt b/3rdparty/macdeployqt/CMakeLists.txt new file mode 100644 index 00000000..d091e077 --- /dev/null +++ b/3rdparty/macdeployqt/CMakeLists.txt @@ -0,0 +1,7 @@ +add_executable(macdeployqt main.cpp shared.cpp) +target_link_libraries(macdeployqt PRIVATE + "-framework AppKit" + ${QtCore_LIBRARIES} +) + +#execute_process(COMMAND cp ${CMAKE_CURRENT_BINARY_DIR}/macdeployqt ${CMAKE_BINARY_DIR}) diff --git a/3rdparty/macdeployqt/main.cpp b/3rdparty/macdeployqt/main.cpp new file mode 100644 index 00000000..b7fd985a --- /dev/null +++ b/3rdparty/macdeployqt/main.cpp @@ -0,0 +1,291 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the tools applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#undef QT_NO_DEBUG_OUTPUT +#undef QT_NO_WARNING_OUTPUT +#undef QT_NO_INFO_OUTPUT + +#include +#include +#include + +#include "shared.h" + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + + QString appBundlePath; + if (argc > 1) + appBundlePath = QString::fromLocal8Bit(argv[1]); + + if (argc < 2 || appBundlePath.startsWith("-")) { + qDebug() << "Usage: macdeployqt app-bundle [options]"; + qDebug() << ""; + qDebug() << "Options:"; + qDebug() << " -verbose=<0-3> : 0 = no output, 1 = error/warning (default), 2 = normal, 3 = debug"; + qDebug() << " -no-plugins : Skip plugin deployment"; + qDebug() << " -dmg : Create a .dmg disk image"; + qDebug() << " -no-strip : Don't run 'strip' on the binaries"; + qDebug() << " -use-debug-libs : Deploy with debug versions of frameworks and plugins (implies -no-strip)"; + qDebug() << " -executable= : Let the given executable use the deployed frameworks too"; + qDebug() << " -qmldir= : Scan for QML imports in the given path"; + qDebug() << " -qmlimport= : Add the given path to the QML module search locations"; + qDebug() << " -always-overwrite : Copy files even if the target file exists"; + qDebug() << " -codesign= : Run codesign with the given identity on all executables"; + qDebug() << " -hardened-runtime : Enable Hardened Runtime when code signing"; + qDebug() << " -timestamp : Include a secure timestamp when code signing (requires internet connection)"; + qDebug() << " -sign-for-notarization=: Activate the necessary options for notarization (requires internet connection)"; + qDebug() << " -appstore-compliant : Skip deployment of components that use private API"; + qDebug() << " -libpath= : Add the given path to the library search path"; + qDebug() << " -fs= : Set the filesystem used for the .dmg disk image (defaults to HFS+)"; + qDebug() << " -plugins-dir= : Set plugins directory"; + qDebug() << ""; + qDebug() << "macdeployqt takes an application bundle as input and makes it"; + qDebug() << "self-contained by copying in the Qt frameworks and plugins that"; + qDebug() << "the application uses."; + qDebug() << ""; + qDebug() << "Plugins related to a framework are copied in with the"; + qDebug() << "framework. The accessibility, image formats, and text codec"; + qDebug() << "plugins are always copied, unless \"-no-plugins\" is specified."; + qDebug() << ""; + qDebug() << "Qt plugins may use private API and will cause the app to be"; + qDebug() << "rejected from the Mac App store. MacDeployQt will print a warning"; + qDebug() << "when known incompatible plugins are deployed. Use -appstore-compliant "; + qDebug() << "to skip these plugins. Currently two SQL plugins are known to"; + qDebug() << "be incompatible: qsqlodbc and qsqlpsql."; + qDebug() << ""; + qDebug() << "See the \"Deploying Applications on OS X\" topic in the"; + qDebug() << "documentation for more information about deployment on OS X."; + + return 1; + } + + appBundlePath = QDir::cleanPath(appBundlePath); + + if (QDir().exists(appBundlePath) == false) { + qDebug() << "Error: Could not find app bundle" << appBundlePath; + return 1; + } + + bool plugins = true; + bool dmg = false; + QByteArray filesystem("HFS+"); + bool useDebugLibs = false; + extern bool runStripEnabled; + extern bool alwaysOwerwriteEnabled; + extern QStringList librarySearchPath; + QStringList additionalExecutables; + bool qmldirArgumentUsed = false; + QStringList qmlDirs; + QStringList qmlImportPaths; + extern bool runCodesign; + extern QString codesignIdentiy; + extern bool hardenedRuntime; + extern bool appstoreCompliant; + extern bool deployFramework; + extern bool secureTimestamp; + QString plugin_dir; + + for (int i = 2; i < argc; ++i) { + QByteArray argument = QByteArray(argv[i]); + if (argument == QByteArray("-no-plugins")) { + LogDebug() << "Argument found:" << argument; + plugins = false; + } else if (argument == QByteArray("-dmg")) { + LogDebug() << "Argument found:" << argument; + dmg = true; + } else if (argument == QByteArray("-no-strip")) { + LogDebug() << "Argument found:" << argument; + runStripEnabled = false; + } else if (argument == QByteArray("-use-debug-libs")) { + LogDebug() << "Argument found:" << argument; + useDebugLibs = true; + runStripEnabled = false; + } else if (argument.startsWith(QByteArray("-verbose"))) { + LogDebug() << "Argument found:" << argument; + int index = argument.indexOf("="); + bool ok = false; + int number = argument.mid(index+1).toInt(&ok); + if (!ok) + LogError() << "Could not parse verbose level"; + else + logLevel = number; + } else if (argument.startsWith(QByteArray("-executable"))) { + LogDebug() << "Argument found:" << argument; + int index = argument.indexOf('='); + if (index == -1) + LogError() << "Missing executable path"; + else + additionalExecutables << argument.mid(index+1); + } else if (argument.startsWith(QByteArray("-qmldir"))) { + LogDebug() << "Argument found:" << argument; + qmldirArgumentUsed = true; + int index = argument.indexOf('='); + if (index == -1) + LogError() << "Missing qml directory path"; + else + qmlDirs << argument.mid(index+1); + } else if (argument.startsWith(QByteArray("-qmlimport"))) { + LogDebug() << "Argument found:" << argument; + int index = argument.indexOf('='); + if (index == -1) + LogError() << "Missing qml import path"; + else + qmlImportPaths << argument.mid(index+1); + } else if (argument.startsWith(QByteArray("-libpath"))) { + LogDebug() << "Argument found:" << argument; + int index = argument.indexOf('='); + if (index == -1) + LogError() << "Missing library search path"; + else + librarySearchPath << argument.mid(index+1); + } else if (argument == QByteArray("-always-overwrite")) { + LogDebug() << "Argument found:" << argument; + alwaysOwerwriteEnabled = true; + } else if (argument.startsWith(QByteArray("-codesign"))) { + LogDebug() << "Argument found:" << argument; + int index = argument.indexOf("="); + if (index < 0 || index >= argument.size()) { + LogError() << "Missing code signing identity"; + } else { + runCodesign = true; + codesignIdentiy = argument.mid(index+1); + } + } else if (argument.startsWith(QByteArray("-sign-for-notarization"))) { + LogDebug() << "Argument found:" << argument; + int index = argument.indexOf("="); + if (index < 0 || index >= argument.size()) { + LogError() << "Missing code signing identity"; + } else { + runCodesign = true; + hardenedRuntime = true; + secureTimestamp = true; + codesignIdentiy = argument.mid(index+1); + } + } else if (argument.startsWith(QByteArray("-hardened-runtime"))) { + LogDebug() << "Argument found:" << argument; + hardenedRuntime = true; + } else if (argument.startsWith(QByteArray("-timestamp"))) { + LogDebug() << "Argument found:" << argument; + secureTimestamp = true; + } else if (argument == QByteArray("-appstore-compliant")) { + LogDebug() << "Argument found:" << argument; + appstoreCompliant = true; + + // Undocumented option, may not work as intented + } else if (argument == QByteArray("-deploy-framework")) { + LogDebug() << "Argument found:" << argument; + deployFramework = true; + + } else if (argument.startsWith(QByteArray("-fs"))) { + LogDebug() << "Argument found:" << argument; + int index = argument.indexOf('='); + if (index == -1) + LogError() << "Missing filesystem type"; + else + filesystem = argument.mid(index+1); + } else if (argument.startsWith(QByteArray("-plugins-dir"))) { + LogDebug() << "Argument found:" << argument; + int index = argument.indexOf('='); + if (index == -1) + LogError() << "Missing filesystem type"; + else + plugin_dir = argument.mid(index+1); + } else if (argument.startsWith("-")) { + LogError() << "Unknown argument" << argument << "\n"; + return 1; + } + } + + DeploymentInfo deploymentInfo = deployQtFrameworks(appBundlePath, additionalExecutables, useDebugLibs); + + if (deploymentInfo.isDebug) + useDebugLibs = true; + + if (deployFramework && deploymentInfo.isFramework) + fixupFramework(appBundlePath); + + // Convenience: Look for .qml files in the current directoty if no -qmldir specified. + if (qmlDirs.isEmpty()) { + QDir dir; + if (!dir.entryList(QStringList() << QStringLiteral("*.qml")).isEmpty()) { + qmlDirs += QStringLiteral("."); + } + } + + if (!qmlDirs.isEmpty()) { + bool ok = deployQmlImports(appBundlePath, deploymentInfo, qmlDirs, qmlImportPaths); + if (!ok && qmldirArgumentUsed) + return 1; // exit if the user explicitly asked for qml import deployment + + // Update deploymentInfo.deployedFrameworks - the QML imports + // may have brought in extra frameworks as dependencies. + deploymentInfo.deployedFrameworks += findAppFrameworkNames(appBundlePath); + deploymentInfo.deployedFrameworks = + QSet(deploymentInfo.deployedFrameworks.begin(), + deploymentInfo.deployedFrameworks.end()).values(); + } + + if (plugins) { + if (plugin_dir.isEmpty()) { + deploymentInfo.pluginPath = QLibraryInfo::path(QLibraryInfo::PluginsPath); + } + else { + deploymentInfo.pluginPath = plugin_dir; + } + if (deploymentInfo.pluginPath.isEmpty()) { + LogError() << "Missing Qt plugins path\n"; + return 1; + } + if (!QDir(deploymentInfo.pluginPath).exists()) { + LogError() << "Plugins path does not exist\n" << deploymentInfo.pluginPath; + return 1; + } + Q_ASSERT(!deploymentInfo.pluginPath.isEmpty()); + if (!deploymentInfo.pluginPath.isEmpty()) { + LogNormal(); + deployPlugins(appBundlePath, deploymentInfo, useDebugLibs); + createQtConf(appBundlePath); + } + } + + if (runStripEnabled) + stripAppBinary(appBundlePath); + + if (runCodesign) + codesign(codesignIdentiy, appBundlePath); + + if (dmg) { + LogNormal(); + createDiskImage(appBundlePath, filesystem); + } + + return 0; +} + diff --git a/3rdparty/macdeployqt/shared.cpp b/3rdparty/macdeployqt/shared.cpp new file mode 100644 index 00000000..8d50ee75 --- /dev/null +++ b/3rdparty/macdeployqt/shared.cpp @@ -0,0 +1,1681 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the tools applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#undef QT_NO_DEBUG_OUTPUT +#undef QT_NO_WARNING_OUTPUT +#undef QT_NO_INFO_OUTPUT + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "shared.h" + +#ifdef Q_OS_DARWIN +#include +#endif + +bool runStripEnabled = true; +bool alwaysOwerwriteEnabled = false; +bool runCodesign = false; +QStringList librarySearchPath; +QString codesignIdentiy; +QString extraEntitlements; +bool hardenedRuntime = false; +bool secureTimestamp = false; +bool appstoreCompliant = false; +int logLevel = 1; +bool deployFramework = false; + +using std::cout; +using std::endl; + +bool operator==(const FrameworkInfo &a, const FrameworkInfo &b) +{ + return ((a.frameworkPath == b.frameworkPath) && (a.binaryPath == b.binaryPath)); +} + +QDebug operator<<(QDebug debug, const FrameworkInfo &info) +{ + debug << "Framework name" << info.frameworkName << "\n"; + debug << "Framework directory" << info.frameworkDirectory << "\n"; + debug << "Framework path" << info.frameworkPath << "\n"; + debug << "Binary directory" << info.binaryDirectory << "\n"; + debug << "Binary name" << info.binaryName << "\n"; + debug << "Binary path" << info.binaryPath << "\n"; + debug << "Version" << info.version << "\n"; + debug << "Install name" << info.installName << "\n"; + debug << "Deployed install name" << info.deployedInstallName << "\n"; + debug << "Source file Path" << info.sourceFilePath << "\n"; + debug << "Framework Destination Directory (relative to bundle)" << info.frameworkDestinationDirectory << "\n"; + debug << "Binary Destination Directory (relative to bundle)" << info.binaryDestinationDirectory << "\n"; + + return debug; +} + +const QString bundleFrameworkDirectory = "Contents/Frameworks"; + +inline QDebug operator<<(QDebug debug, const ApplicationBundleInfo &info) +{ + debug << "Application bundle path" << info.path << "\n"; + debug << "Binary path" << info.binaryPath << "\n"; + debug << "Additional libraries" << info.libraryPaths << "\n"; + return debug; +} + +bool copyFilePrintStatus(const QString &from, const QString &to) +{ + if (QFile(to).exists()) { + if (alwaysOwerwriteEnabled) { + QFile(to).remove(); + } else { + qDebug() << "File exists, skip copy:" << to; + return false; + } + } + + if (QFile::copy(from, to)) { + QFile dest(to); + dest.setPermissions(dest.permissions() | QFile::WriteOwner | QFile::WriteUser); + LogNormal() << " copied:" << from; + LogNormal() << " to" << to; + + // The source file might not have write permissions set. Set the + // write permission on the target file to make sure we can use + // install_name_tool on it later. + QFile toFile(to); + if (toFile.permissions() & QFile::WriteOwner) + return true; + + if (!toFile.setPermissions(toFile.permissions() | QFile::WriteOwner)) { + LogError() << "Failed to set u+w permissions on target file: " << to; + return false; + } + + return true; + } else { + LogError() << "file copy failed from" << from; + LogError() << " to" << to; + return false; + } +} + +bool linkFilePrintStatus(const QString &file, const QString &link) +{ + if (QFile(link).exists()) { + if (QFile(link).symLinkTarget().isEmpty()) + LogError() << link << "exists but it's a file."; + else + LogNormal() << "Symlink exists, skipping:" << link; + return false; + } else if (QFile::link(file, link)) { + LogNormal() << " symlink" << link; + LogNormal() << " points to" << file; + return true; + } else { + LogError() << "failed to symlink" << link; + LogError() << " to" << file; + return false; + } +} + +void patch_debugInInfoPlist(const QString &infoPlistPath) +{ + // Older versions of qmake may have the "_debug" binary as + // the value for CFBundleExecutable. Remove it. + QFile infoPlist(infoPlistPath); + infoPlist.open(QIODevice::ReadOnly); + QByteArray contents = infoPlist.readAll(); + infoPlist.close(); + infoPlist.open(QIODevice::WriteOnly | QIODevice::Truncate); + contents.replace("_debug", ""); // surely there are no legit uses of "_debug" in an Info.plist + infoPlist.write(contents); +} + +OtoolInfo findDependencyInfo(const QString &binaryPath) +{ + OtoolInfo info; + info.binaryPath = binaryPath; + + LogDebug() << "Using otool:"; + LogDebug() << " inspecting" << binaryPath; + QProcess otool; + otool.start("otool", QStringList() << "-L" << binaryPath); + otool.waitForFinished(); + + if (otool.exitStatus() != QProcess::NormalExit || otool.exitCode() != 0) { + LogError() << otool.readAllStandardError(); + return info; + } + + static const QRegularExpression regexp(QStringLiteral( + "^\\t(.+) \\(compatibility version (\\d+\\.\\d+\\.\\d+), " + "current version (\\d+\\.\\d+\\.\\d+)(, weak)?\\)$")); + + QString output = otool.readAllStandardOutput(); + QStringList outputLines = output.split("\n", Qt::SkipEmptyParts); + if (outputLines.size() < 2) { + LogError() << "Could not parse otool output:" << output; + return info; + } + + outputLines.removeFirst(); // remove line containing the binary path + if (binaryPath.contains(".framework/") || binaryPath.endsWith(".dylib")) { + const auto match = regexp.match(outputLines.first()); + if (match.hasMatch()) { + info.installName = match.captured(1); + info.compatibilityVersion = QVersionNumber::fromString(match.captured(2)); + info.currentVersion = QVersionNumber::fromString(match.captured(3)); + } else { + LogError() << "Could not parse otool output line:" << outputLines.first(); + } + //outputLines.removeFirst(); + } + + for (const QString &outputLine : outputLines) { + const auto match = regexp.match(outputLine); + if (match.hasMatch()) { + DylibInfo dylib; + dylib.binaryPath = match.captured(1); + dylib.compatibilityVersion = QVersionNumber::fromString(match.captured(2)); + dylib.currentVersion = QVersionNumber::fromString(match.captured(3)); + info.dependencies << dylib; + } else { + LogError() << "Could not parse otool output line:" << outputLine; + } + } + + return info; +} + +FrameworkInfo parseOtoolLibraryLine(const QString &line, const QString &appBundlePath, const QSet &rpaths, bool useDebugLibs) +{ + FrameworkInfo info; + QString trimmed = line.trimmed(); + + if (trimmed.isEmpty()) + return info; + + // Don't deploy system libraries. + if (trimmed.startsWith("/System/Library/") || + (trimmed.startsWith("/usr/lib/") && trimmed.contains("libQt") == false) // exception for libQtuitools and libQtlucene + || trimmed.startsWith("@executable_path") || trimmed.startsWith("@loader_path")) + return info; + + // Resolve rpath relative libraries. + if (trimmed.startsWith("@rpath/")) { + QString rpathRelativePath = trimmed.mid(QStringLiteral("@rpath/").length()); + bool foundInsideBundle = false; + foreach (const QString &rpath, rpaths) { + QString path = QDir::cleanPath(rpath + "/" + rpathRelativePath); + // Skip paths already inside the bundle. + if (!appBundlePath.isEmpty()) { + if (QDir::isAbsolutePath(appBundlePath)) { + if (path.startsWith(QDir::cleanPath(appBundlePath) + "/")) { + foundInsideBundle = true; + continue; + } + } else { + if (path.startsWith(QDir::cleanPath(QDir::currentPath() + "/" + appBundlePath) + "/")) { + foundInsideBundle = true; + continue; + } + } + } + // Try again with substituted rpath. + FrameworkInfo resolvedInfo = parseOtoolLibraryLine(path, appBundlePath, rpaths, useDebugLibs); + if (!resolvedInfo.frameworkName.isEmpty() && QFile::exists(resolvedInfo.frameworkPath)) { + resolvedInfo.rpathUsed = rpath; + resolvedInfo.installName = trimmed; + return resolvedInfo; + } + } + if (!rpaths.isEmpty() && !foundInsideBundle) { + LogError() << "Cannot resolve rpath" << trimmed; + LogError() << " using" << rpaths; + } + return info; + } + + enum State {QtPath, FrameworkName, DylibName, Version, FrameworkBinary, End}; + State state = QtPath; + int part = 0; + QString name; + QString qtPath; + QString suffix = useDebugLibs ? "_debug" : ""; + + // Split the line into [Qt-path]/lib/qt[Module].framework/Versions/[Version]/ + QStringList parts = trimmed.split("/"); + while (part < parts.count()) { + const QString currentPart = parts.at(part).simplified() ; + ++part; + if (currentPart == "") + continue; + + if (state == QtPath) { + // Check for library name part + if (part < parts.count() && parts.at(part).contains(".dylib")) { + info.frameworkDirectory += "/" + (qtPath + currentPart + "/").simplified(); + state = DylibName; + continue; + } else if (part < parts.count() && parts.at(part).endsWith(".framework")) { + info.frameworkDirectory += "/" + (qtPath + "lib/").simplified(); + state = FrameworkName; + continue; + } else if (trimmed.startsWith("/") == false) { // If the line does not contain a full path, the app is using a binary Qt package. + QStringList partsCopy = parts; + partsCopy.removeLast(); + foreach (QString path, librarySearchPath) { + if (!path.endsWith("/")) + path += '/'; + QString nameInPath = path + parts.join(QLatin1Char('/')); + if (QFile::exists(nameInPath)) { + info.frameworkDirectory = path + partsCopy.join(QLatin1Char('/')); + break; + } + } + if (currentPart.contains(".framework")) { + if (info.frameworkDirectory.isEmpty()) + info.frameworkDirectory = "/Library/Frameworks/" + partsCopy.join(QLatin1Char('/')); + if (!info.frameworkDirectory.endsWith("/")) + info.frameworkDirectory += "/"; + state = FrameworkName; + --part; + continue; + } else if (currentPart.contains(".dylib")) { + if (info.frameworkDirectory.isEmpty()) + info.frameworkDirectory = "/usr/lib/" + partsCopy.join(QLatin1Char('/')); + if (!info.frameworkDirectory.endsWith("/")) + info.frameworkDirectory += "/"; + state = DylibName; + --part; + continue; + } + } + qtPath += (currentPart + "/"); + + } if (state == FrameworkName) { + // remove ".framework" + name = currentPart; + name.chop(QString(".framework").length()); + info.isDylib = false; + info.frameworkName = currentPart; + state = Version; + ++part; + continue; + } if (state == DylibName) { + name = currentPart; + info.isDylib = true; + info.frameworkName = name; + info.binaryName = name.contains(suffix) ? name : name.left(name.indexOf('.')) + suffix + name.mid(name.indexOf('.')); + info.deployedInstallName = "@executable_path/../Frameworks/" + info.binaryName; + info.frameworkPath = info.frameworkDirectory + info.binaryName; + info.sourceFilePath = info.frameworkPath; + info.frameworkDestinationDirectory = bundleFrameworkDirectory + "/"; + info.binaryDestinationDirectory = info.frameworkDestinationDirectory; + info.binaryDirectory = info.frameworkDirectory; + info.binaryPath = info.frameworkPath; + state = End; + ++part; + continue; + } else if (state == Version) { + info.version = currentPart; + info.binaryDirectory = "Versions/" + info.version; + info.frameworkPath = info.frameworkDirectory + info.frameworkName; + info.frameworkDestinationDirectory = bundleFrameworkDirectory + "/" + info.frameworkName; + info.binaryDestinationDirectory = info.frameworkDestinationDirectory + "/" + info.binaryDirectory; + state = FrameworkBinary; + } else if (state == FrameworkBinary) { + info.binaryName = currentPart.contains(suffix) ? currentPart : currentPart + suffix; + info.binaryPath = "/" + info.binaryDirectory + "/" + info.binaryName; + info.deployedInstallName = "@executable_path/../Frameworks/" + info.frameworkName + info.binaryPath; + info.sourceFilePath = info.frameworkPath + info.binaryPath; + state = End; + } else if (state == End) { + break; + } + } + + if (!info.sourceFilePath.isEmpty() && QFile::exists(info.sourceFilePath)) { + info.installName = findDependencyInfo(info.sourceFilePath).installName; + if (info.installName.startsWith("@rpath/")) + info.deployedInstallName = info.installName; + } + + return info; +} + +QString findAppBinary(const QString &appBundlePath) +{ + QString binaryPath; + +#ifdef Q_OS_DARWIN + CFStringRef bundlePath = appBundlePath.toCFString(); + CFURLRef bundleURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, bundlePath, + kCFURLPOSIXPathStyle, true); + CFRelease(bundlePath); + CFBundleRef bundle = CFBundleCreate(kCFAllocatorDefault, bundleURL); + if (bundle) { + CFURLRef executableURL = CFBundleCopyExecutableURL(bundle); + if (executableURL) { + CFURLRef absoluteExecutableURL = CFURLCopyAbsoluteURL(executableURL); + if (absoluteExecutableURL) { + CFStringRef executablePath = CFURLCopyFileSystemPath(absoluteExecutableURL, + kCFURLPOSIXPathStyle); + if (executablePath) { + binaryPath = QString::fromCFString(executablePath); + CFRelease(executablePath); + } + CFRelease(absoluteExecutableURL); + } + CFRelease(executableURL); + } + CFRelease(bundle); + } + CFRelease(bundleURL); +#endif + + if (QFile::exists(binaryPath)) + return binaryPath; + LogError() << "Could not find bundle binary for" << appBundlePath; + return QString(); +} + +QStringList findAppFrameworkNames(const QString &appBundlePath) +{ + QStringList frameworks; + + // populate the frameworks list with QtFoo.framework etc, + // as found in /Contents/Frameworks/ + QString searchPath = appBundlePath + "/Contents/Frameworks/"; + QDirIterator iter(searchPath, QStringList() << QString::fromLatin1("*.framework"), + QDir::Dirs | QDir::NoSymLinks); + while (iter.hasNext()) { + iter.next(); + frameworks << iter.fileInfo().fileName(); + } + + return frameworks; +} + +QStringList findAppFrameworkPaths(const QString &appBundlePath) +{ + QStringList frameworks; + QString searchPath = appBundlePath + "/Contents/Frameworks/"; + QDirIterator iter(searchPath, QStringList() << QString::fromLatin1("*.framework"), + QDir::Dirs | QDir::NoSymLinks); + while (iter.hasNext()) { + iter.next(); + frameworks << iter.fileInfo().filePath(); + } + + return frameworks; +} + +QStringList findAppLibraries(const QString &appBundlePath) +{ + QStringList result; + // dylibs + QDirIterator iter(appBundlePath, QStringList() << QString::fromLatin1("*.dylib"), + QDir::Files | QDir::NoSymLinks, QDirIterator::Subdirectories); + while (iter.hasNext()) { + iter.next(); + result << iter.fileInfo().filePath(); + } + return result; +} + +QStringList findAppBundleFiles(const QString &appBundlePath, bool absolutePath = false) +{ + QStringList result; + + QDirIterator iter(appBundlePath, QStringList() << QString::fromLatin1("*"), + QDir::Files, QDirIterator::Subdirectories); + + while (iter.hasNext()) { + iter.next(); + if (iter.fileInfo().isSymLink()) + continue; + result << (absolutePath ? iter.fileInfo().absoluteFilePath() : iter.fileInfo().filePath()); + } + + return result; +} + +QString findEntitlementsFile(const QString& path) +{ + QDirIterator iter(path, QStringList() << QString::fromLatin1("*.entitlements"), + QDir::Files, QDirIterator::Subdirectories); + + while (iter.hasNext()) { + iter.next(); + if (iter.fileInfo().isSymLink()) + continue; + + //return the first entitlements file - only one is used for signing anyway + return iter.fileInfo().absoluteFilePath(); + } + + return QString(); +} + +QList getQtFrameworks(const QList &dependencies, const QString &appBundlePath, const QSet &rpaths, bool useDebugLibs) +{ + QList libraries; + for (const DylibInfo &dylibInfo : dependencies) { + FrameworkInfo info = parseOtoolLibraryLine(dylibInfo.binaryPath, appBundlePath, rpaths, useDebugLibs); + if (info.frameworkName.isEmpty() == false) { + LogDebug() << "Adding framework:"; + LogDebug() << info; + libraries.append(info); + } + } + return libraries; +} + +QString resolveDyldPrefix(const QString &path, const QString &loaderPath, const QString &executablePath) +{ + if (path.startsWith("@")) { + if (path.startsWith(QStringLiteral("@executable_path/"))) { + // path relative to bundle executable dir + if (QDir::isAbsolutePath(executablePath)) { + return QDir::cleanPath(QFileInfo(executablePath).path() + path.mid(QStringLiteral("@executable_path").length())); + } else { + return QDir::cleanPath(QDir::currentPath() + "/" + + QFileInfo(executablePath).path() + path.mid(QStringLiteral("@executable_path").length())); + } + } else if (path.startsWith(QStringLiteral("@loader_path"))) { + // path relative to loader dir + if (QDir::isAbsolutePath(loaderPath)) { + return QDir::cleanPath(QFileInfo(loaderPath).path() + path.mid(QStringLiteral("@loader_path").length())); + } else { + return QDir::cleanPath(QDir::currentPath() + "/" + + QFileInfo(loaderPath).path() + path.mid(QStringLiteral("@loader_path").length())); + } + } else { + LogError() << "Unexpected prefix" << path; + } + } + return path; +} + +QSet getBinaryRPaths(const QString &path, bool resolve = true, QString executablePath = QString()) +{ + QSet rpaths; + + QProcess otool; + otool.start("otool", QStringList() << "-l" << path); + otool.waitForFinished(); + + if (otool.exitCode() != 0) { + LogError() << otool.readAllStandardError(); + } + + if (resolve && executablePath.isEmpty()) { + executablePath = path; + } + + QString output = otool.readAllStandardOutput(); + QStringList outputLines = output.split("\n"); + + for (auto i = outputLines.cbegin(), end = outputLines.cend(); i != end; ++i) { + if (i->contains("cmd LC_RPATH") && ++i != end && + i->contains("cmdsize") && ++i != end) { + const QString &rpathCmd = *i; + int pathStart = rpathCmd.indexOf("path "); + int pathEnd = rpathCmd.indexOf(" ("); + if (pathStart >= 0 && pathEnd >= 0 && pathStart < pathEnd) { + QString rpath = rpathCmd.mid(pathStart + 5, pathEnd - pathStart - 5); + if (resolve) { + rpaths << resolveDyldPrefix(rpath, path, executablePath); + } else { + rpaths << rpath; + } + } + } + } + + return rpaths; +} + +QList getQtFrameworks(const QString &path, const QString &appBundlePath, const QSet &rpaths, bool useDebugLibs) +{ + const OtoolInfo info = findDependencyInfo(path); + return getQtFrameworks(info.dependencies, appBundlePath, rpaths + getBinaryRPaths(path), useDebugLibs); +} + +QList getQtFrameworksForPaths(const QStringList &paths, const QString &appBundlePath, const QSet &rpaths, bool useDebugLibs) +{ + QList result; + QSet existing; + foreach (const QString &path, paths) { + foreach (const FrameworkInfo &info, getQtFrameworks(path, appBundlePath, rpaths, useDebugLibs)) { + if (!existing.contains(info.frameworkPath)) { // avoid duplicates + existing.insert(info.frameworkPath); + result << info; + } + } + } + return result; +} + +QStringList getBinaryDependencies(const QString executablePath, + const QString &path, + const QList &additionalBinariesContainingRpaths) +{ + QStringList binaries; + + const auto dependencies = findDependencyInfo(path).dependencies; + + bool rpathsLoaded = false; + QSet rpaths; + + // return bundle-local dependencies. (those starting with @executable_path) + foreach (const DylibInfo &info, dependencies) { + QString trimmedLine = info.binaryPath; + if (trimmedLine.startsWith("@executable_path/")) { + QString binary = QDir::cleanPath(executablePath + trimmedLine.mid(QStringLiteral("@executable_path/").length())); + if (binary != path) + binaries.append(binary); + } else if (trimmedLine.startsWith("@rpath/")) { + if (!rpathsLoaded) { + rpaths = getBinaryRPaths(path, true, executablePath); + foreach (const QString &binaryPath, additionalBinariesContainingRpaths) { + QSet binaryRpaths = getBinaryRPaths(binaryPath, true); + rpaths += binaryRpaths; + } + rpathsLoaded = true; + } + bool resolved = false; + foreach (const QString &rpath, rpaths) { + QString binary = QDir::cleanPath(rpath + "/" + trimmedLine.mid(QStringLiteral("@rpath/").length())); + LogDebug() << "Checking for" << binary; + if (QFile::exists(binary)) { + binaries.append(binary); + resolved = true; + break; + } + } + if (!resolved && !rpaths.isEmpty()) { + LogError() << "Cannot resolve rpath" << trimmedLine; + LogError() << " using" << rpaths; + } + } + } + + return binaries; +} + +// copies everything _inside_ sourcePath to destinationPath +bool recursiveCopy(const QString &sourcePath, const QString &destinationPath) +{ + if (!QDir(sourcePath).exists()) + return false; + QDir().mkpath(destinationPath); + + LogNormal() << "copy:" << sourcePath << destinationPath; + + QStringList files = QDir(sourcePath).entryList(QStringList() << "*", QDir::Files | QDir::NoDotAndDotDot); + foreach (QString file, files) { + const QString fileSourcePath = sourcePath + "/" + file; + const QString fileDestinationPath = destinationPath + "/" + file; + copyFilePrintStatus(fileSourcePath, fileDestinationPath); + } + + QStringList subdirs = QDir(sourcePath).entryList(QStringList() << "*", QDir::Dirs | QDir::NoDotAndDotDot); + foreach (QString dir, subdirs) { + recursiveCopy(sourcePath + "/" + dir, destinationPath + "/" + dir); + } + return true; +} + +void recursiveCopyAndDeploy(const QString &appBundlePath, const QSet &rpaths, const QString &sourcePath, const QString &destinationPath) +{ + QDir().mkpath(destinationPath); + + LogNormal() << "copy:" << sourcePath << destinationPath; + const bool isDwarfPath = sourcePath.endsWith("DWARF"); + + QStringList files = QDir(sourcePath).entryList(QStringList() << QStringLiteral("*"), QDir::Files | QDir::NoDotAndDotDot); + foreach (QString file, files) { + const QString fileSourcePath = sourcePath + QLatin1Char('/') + file; + + if (file.endsWith("_debug.dylib")) { + continue; // Skip debug versions + } else if (!isDwarfPath && file.endsWith(QStringLiteral(".dylib"))) { + // App store code signing rules forbids code binaries in Contents/Resources/, + // which poses a problem for deploying mixed .qml/.dylib Qt Quick imports. + // Solve this by placing the dylibs in Contents/PlugIns/quick, and then + // creting a symlink to there from the Qt Quick import in Contents/Resources/. + // + // Example: + // MyApp.app/Contents/Resources/qml/QtQuick/Controls/libqtquickcontrolsplugin.dylib -> + // ../../../../PlugIns/quick/libqtquickcontrolsplugin.dylib + // + + // The .dylib destination path: + QString fileDestinationDir = appBundlePath + QStringLiteral("/Contents/PlugIns/quick/"); + QDir().mkpath(fileDestinationDir); + QString fileDestinationPath = fileDestinationDir + file; + + // The .dylib symlink destination path: + QString linkDestinationPath = destinationPath + QLatin1Char('/') + file; + + // The (relative) link; with a correct number of "../"'s. + QString linkPath = QStringLiteral("PlugIns/quick/") + file; + int cdupCount = linkDestinationPath.count(QStringLiteral("/")) - appBundlePath.count(QStringLiteral("/")); + for (int i = 0; i < cdupCount - 2; ++i) + linkPath.prepend("../"); + + if (copyFilePrintStatus(fileSourcePath, fileDestinationPath)) { + linkFilePrintStatus(linkPath, linkDestinationPath); + + runStrip(fileDestinationPath); + bool useDebugLibs = false; + bool useLoaderPath = false; + QList frameworks = getQtFrameworks(fileDestinationPath, appBundlePath, rpaths, useDebugLibs); + deployQtFrameworks(frameworks, appBundlePath, QStringList(fileDestinationPath), useDebugLibs, useLoaderPath); + } + } else { + QString fileDestinationPath = destinationPath + QLatin1Char('/') + file; + copyFilePrintStatus(fileSourcePath, fileDestinationPath); + } + } + + QStringList subdirs = QDir(sourcePath).entryList(QStringList() << QStringLiteral("*"), QDir::Dirs | QDir::NoDotAndDotDot); + foreach (QString dir, subdirs) { + recursiveCopyAndDeploy(appBundlePath, rpaths, sourcePath + QLatin1Char('/') + dir, destinationPath + QLatin1Char('/') + dir); + } +} + +QString copyDylib(const FrameworkInfo &framework, const QString path) +{ + if (!QFile::exists(framework.sourceFilePath)) { + LogError() << "no file at" << framework.sourceFilePath; + return QString(); + } + + // Construct destination paths. The full path typically looks like + // MyApp.app/Contents/Frameworks/libfoo.dylib + QString dylibDestinationDirectory = path + QLatin1Char('/') + framework.frameworkDestinationDirectory; + QString dylibDestinationBinaryPath = dylibDestinationDirectory + QLatin1Char('/') + framework.binaryName; + + // Create destination directory + if (!QDir().mkpath(dylibDestinationDirectory)) { + LogError() << "could not create destination directory" << dylibDestinationDirectory; + return QString(); + } + + // Retrun if the dylib has aleardy been deployed + if (QFileInfo(dylibDestinationBinaryPath).exists() && !alwaysOwerwriteEnabled) + return dylibDestinationBinaryPath; + + // Copy dylib binary + copyFilePrintStatus(framework.sourceFilePath, dylibDestinationBinaryPath); + return dylibDestinationBinaryPath; +} + +QString copyFramework(const FrameworkInfo &framework, const QString path) +{ + if (!QFile::exists(framework.sourceFilePath)) { + LogError() << "no file at" << framework.sourceFilePath; + return QString(); + } + + // Construct destination paths. The full path typically looks like + // MyApp.app/Contents/Frameworks/Foo.framework/Versions/5/QtFoo + QString frameworkDestinationDirectory = path + QLatin1Char('/') + framework.frameworkDestinationDirectory; + QString frameworkBinaryDestinationDirectory = frameworkDestinationDirectory + QLatin1Char('/') + framework.binaryDirectory; + QString frameworkDestinationBinaryPath = frameworkBinaryDestinationDirectory + QLatin1Char('/') + framework.binaryName; + + // Return if the framework has aleardy been deployed + if (QDir(frameworkDestinationDirectory).exists() && !alwaysOwerwriteEnabled) + return QString(); + + // Create destination directory + if (!QDir().mkpath(frameworkBinaryDestinationDirectory)) { + LogError() << "could not create destination directory" << frameworkBinaryDestinationDirectory; + return QString(); + } + + // Now copy the framework. Some parts should be left out (headers/, .prl files). + // Some parts should be included (Resources/, symlink structure). We want this + // function to make as few assumtions about the framework as possible while at + // the same time producing a codesign-compatible framework. + + // Copy framework binary + copyFilePrintStatus(framework.sourceFilePath, frameworkDestinationBinaryPath); + + // Copy Resouces/, Libraries/ and Helpers/ + const QString resourcesSourcePath = framework.frameworkPath + "/Resources"; + const QString resourcesDestianationPath = frameworkDestinationDirectory + "/Versions/" + framework.version + "/Resources"; + recursiveCopy(resourcesSourcePath, resourcesDestianationPath); + const QString librariesSourcePath = framework.frameworkPath + "/Libraries"; + const QString librariesDestianationPath = frameworkDestinationDirectory + "/Versions/" + framework.version + "/Libraries"; + bool createdLibraries = recursiveCopy(librariesSourcePath, librariesDestianationPath); + const QString helpersSourcePath = framework.frameworkPath + "/Helpers"; + const QString helpersDestianationPath = frameworkDestinationDirectory + "/Versions/" + framework.version + "/Helpers"; + bool createdHelpers = recursiveCopy(helpersSourcePath, helpersDestianationPath); + + // Create symlink structure. Links at the framework root point to Versions/Current/ + // which again points to the actual version: + // QtFoo.framework/QtFoo -> Versions/Current/QtFoo + // QtFoo.framework/Resources -> Versions/Current/Resources + // QtFoo.framework/Versions/Current -> 5 + linkFilePrintStatus("Versions/Current/" + framework.binaryName, frameworkDestinationDirectory + "/" + framework.binaryName); + linkFilePrintStatus("Versions/Current/Resources", frameworkDestinationDirectory + "/Resources"); + if (createdLibraries) + linkFilePrintStatus("Versions/Current/Libraries", frameworkDestinationDirectory + "/Libraries"); + if (createdHelpers) + linkFilePrintStatus("Versions/Current/Helpers", frameworkDestinationDirectory + "/Helpers"); + linkFilePrintStatus(framework.version, frameworkDestinationDirectory + "/Versions/Current"); + + // Correct Info.plist location for frameworks produced by older versions of qmake + // Contents/Info.plist should be Versions/5/Resources/Info.plist + const QString legacyInfoPlistPath = framework.frameworkPath + "/Contents/Info.plist"; + const QString correctInfoPlistPath = frameworkDestinationDirectory + "/Resources/Info.plist"; + if (QFile(legacyInfoPlistPath).exists()) { + copyFilePrintStatus(legacyInfoPlistPath, correctInfoPlistPath); + patch_debugInInfoPlist(correctInfoPlistPath); + } + return frameworkDestinationBinaryPath; +} + +void runInstallNameTool(QStringList options) +{ + QProcess installNametool; + qDebug() << "install_name_tool " + options.join(" "); + installNametool.start("install_name_tool", options); + installNametool.waitForFinished(); + if (installNametool.exitCode() != 0) { + LogError() << installNametool.readAllStandardError(); + LogError() << installNametool.readAllStandardOutput(); + } +} + +void changeIdentification(const QString &id, const QString &binaryPath) +{ + LogDebug() << "Using install_name_tool:"; + LogDebug() << " change identification in" << binaryPath; + LogDebug() << " to" << id; + runInstallNameTool(QStringList() << "-id" << id << binaryPath); +} + +void changeInstallName(const QString &bundlePath, const FrameworkInfo &framework, const QStringList &binaryPaths, bool useLoaderPath) +{ + const QString absBundlePath = QFileInfo(bundlePath).absoluteFilePath(); + foreach (const QString &binary, binaryPaths) { + QString deployedInstallName; + if (useLoaderPath) { + deployedInstallName = QLatin1String("@loader_path/") + + QFileInfo(binary).absoluteDir().relativeFilePath(absBundlePath + QLatin1Char('/') + framework.binaryDestinationDirectory + QLatin1Char('/') + framework.binaryName); + } else { + deployedInstallName = framework.deployedInstallName; + } + changeInstallName(framework.installName, deployedInstallName, binary); + // Workaround for the case when the library ID name is a symlink, while the dependencies + // specified using the canonical path to the library (QTBUG-56814) + QString canonicalInstallName = QFileInfo(framework.installName).canonicalFilePath(); + if (!canonicalInstallName.isEmpty() && canonicalInstallName != framework.installName) { + changeInstallName(canonicalInstallName, deployedInstallName, binary); + } + } +} + +void addRPath(const QString &rpath, const QString &binaryPath) +{ + runInstallNameTool(QStringList() << "-add_rpath" << rpath << binaryPath); +} + +void deployRPaths(const QString &bundlePath, const QSet &rpaths, const QString &binaryPath, bool useLoaderPath) +{ + const QString absFrameworksPath = QFileInfo(bundlePath).absoluteFilePath() + + QLatin1String("/Contents/Frameworks"); + const QString relativeFrameworkPath = QFileInfo(binaryPath).absoluteDir().relativeFilePath(absFrameworksPath); + const QString loaderPathToFrameworks = QLatin1String("@loader_path/") + relativeFrameworkPath; + bool rpathToFrameworksFound = false; + QStringList args; + foreach (const QString &rpath, getBinaryRPaths(binaryPath, false)) { + if (rpath == "@executable_path/../Frameworks" || + rpath == loaderPathToFrameworks) { + rpathToFrameworksFound = true; + continue; + } + if (rpaths.contains(resolveDyldPrefix(rpath, binaryPath, binaryPath))) { + args << "-delete_rpath" << rpath; + } + } + if (!args.length()) { + return; + } + if (!rpathToFrameworksFound) { + if (!useLoaderPath) { + args << "-add_rpath" << "@executable_path/../Frameworks"; + } else { + args << "-add_rpath" << loaderPathToFrameworks; + } + } + LogDebug() << "Using install_name_tool:"; + LogDebug() << " change rpaths in" << binaryPath; + LogDebug() << " using" << args; + runInstallNameTool(QStringList() << args << binaryPath); +} + +void deployRPaths(const QString &bundlePath, const QSet &rpaths, const QStringList &binaryPaths, bool useLoaderPath) +{ + foreach (const QString &binary, binaryPaths) { + deployRPaths(bundlePath, rpaths, binary, useLoaderPath); + } +} + +void changeInstallName(const QString &oldName, const QString &newName, const QString &binaryPath) +{ + LogDebug() << "Using install_name_tool:"; + LogDebug() << " in" << binaryPath; + LogDebug() << " change reference" << oldName; + LogDebug() << " to" << newName; + runInstallNameTool(QStringList() << "-change" << oldName << newName << binaryPath); +} + +void runStrip(const QString &binaryPath) +{ + if (runStripEnabled == false) + return; + + LogDebug() << "Using strip:"; + LogDebug() << " stripped" << binaryPath; + QProcess strip; + strip.start("strip", QStringList() << "-x" << binaryPath); + strip.waitForFinished(); + if (strip.exitCode() != 0) { + LogError() << strip.readAllStandardError(); + LogError() << strip.readAllStandardOutput(); + } +} + +void stripAppBinary(const QString &bundlePath) +{ + runStrip(findAppBinary(bundlePath)); +} + +bool DeploymentInfo::containsModule(const QString &module, const QString &libInFix) const +{ + // Check for framework first + if (deployedFrameworks.contains(QLatin1String("Qt") + module + libInFix + + QLatin1String(".framework"))) { + return true; + } + // Check for dylib + const QRegularExpression dylibRegExp(QLatin1String("libQt[0-9]+") + module + + libInFix + QLatin1String(".[0-9]+.dylib")); + return deployedFrameworks.filter(dylibRegExp).size() > 0; +} + +/* + Deploys the the listed frameworks listed into an app bundle. + The frameworks are searched for dependencies, which are also deployed. + (deploying Qt3Support will also deploy QtNetwork and QtSql for example.) + Returns a DeploymentInfo structure containing the Qt path used and a + a list of actually deployed frameworks. +*/ +DeploymentInfo deployQtFrameworks(QList frameworks, + const QString &bundlePath, const QStringList &binaryPaths, bool useDebugLibs, + bool useLoaderPath) +{ + LogNormal(); + LogNormal() << "Deploying Qt frameworks found inside:" << binaryPaths; + QStringList copiedFrameworks; + DeploymentInfo deploymentInfo; + deploymentInfo.useLoaderPath = useLoaderPath; + deploymentInfo.isFramework = bundlePath.contains(".framework"); + deploymentInfo.isDebug = false; + QSet rpathsUsed; + + while (frameworks.isEmpty() == false) { + const FrameworkInfo framework = frameworks.takeFirst(); + copiedFrameworks.append(framework.frameworkName); + + // If a single dependency has the _debug suffix, we treat that as + // the whole deployment being a debug deployment, including deploying + // the debug version of plugins. + if (framework.isDebugLibrary()) + deploymentInfo.isDebug = true; + + if (deploymentInfo.qtPath.isNull()) + deploymentInfo.qtPath = QLibraryInfo::path(QLibraryInfo::PrefixPath); + + if (framework.frameworkDirectory.startsWith(bundlePath)) { + LogError() << framework.frameworkName << "already deployed, skipping."; + continue; + } + + if (!framework.rpathUsed.isEmpty()) + rpathsUsed << framework.rpathUsed; + + // Copy the framework/dylib to the app bundle. + const QString deployedBinaryPath = framework.isDylib ? copyDylib(framework, bundlePath) + : copyFramework(framework, bundlePath); + + // Install_name_tool the new id into the binaries + changeInstallName(bundlePath, framework, binaryPaths, useLoaderPath); + + // Skip the rest if already was deployed. + if (deployedBinaryPath.isNull()) + continue; + + runStrip(deployedBinaryPath); + + // Install_name_tool it a new id. + if (!framework.rpathUsed.length()) { + changeIdentification(framework.deployedInstallName, deployedBinaryPath); + } + + // Check for framework dependencies + QList dependencies = getQtFrameworks(deployedBinaryPath, bundlePath, rpathsUsed, useDebugLibs); + + foreach (FrameworkInfo dependency, dependencies) { + if (dependency.rpathUsed.isEmpty()) { + changeInstallName(bundlePath, dependency, QStringList() << deployedBinaryPath, useLoaderPath); + } else { + rpathsUsed << dependency.rpathUsed; + } + + // Deploy framework if necessary. + if (copiedFrameworks.contains(dependency.frameworkName) == false && frameworks.contains(dependency) == false) { + frameworks.append(dependency); + } + } + } + deploymentInfo.deployedFrameworks = copiedFrameworks; + deployRPaths(bundlePath, rpathsUsed, binaryPaths, useLoaderPath); + deploymentInfo.rpathsUsed += rpathsUsed; + return deploymentInfo; +} + +DeploymentInfo deployQtFrameworks(const QString &appBundlePath, const QStringList &additionalExecutables, bool useDebugLibs) +{ + ApplicationBundleInfo applicationBundle; + applicationBundle.path = appBundlePath; + applicationBundle.binaryPath = findAppBinary(appBundlePath); + applicationBundle.libraryPaths = findAppLibraries(appBundlePath); + QStringList allBinaryPaths = QStringList() << applicationBundle.binaryPath << applicationBundle.libraryPaths + << additionalExecutables; + QSet allLibraryPaths = getBinaryRPaths(applicationBundle.binaryPath, true); + allLibraryPaths.insert(QLibraryInfo::path(QLibraryInfo::LibrariesPath)); + QList frameworks = getQtFrameworksForPaths(allBinaryPaths, appBundlePath, allLibraryPaths, useDebugLibs); + if (frameworks.isEmpty() && !alwaysOwerwriteEnabled) { + LogWarning(); + LogWarning() << "Could not find any external Qt frameworks to deploy in" << appBundlePath; + LogWarning() << "Perhaps macdeployqt was already used on" << appBundlePath << "?"; + LogWarning() << "If so, you will need to rebuild" << appBundlePath << "before trying again."; + return DeploymentInfo(); + } else { + return deployQtFrameworks(frameworks, applicationBundle.path, allBinaryPaths, useDebugLibs, !additionalExecutables.isEmpty()); + } +} + +QString getLibInfix(const QStringList &deployedFrameworks) +{ + QString libInfix; + foreach (const QString &framework, deployedFrameworks) { + if (framework.startsWith(QStringLiteral("QtCore")) && framework.endsWith(QStringLiteral(".framework"))) { + Q_ASSERT(framework.length() >= 16); + // 16 == "QtCore" + ".framework" + const int lengthOfLibInfix = framework.length() - 16; + if (lengthOfLibInfix) + libInfix = framework.mid(6, lengthOfLibInfix); + break; + } + } + return libInfix; +} + +void deployPlugins(const ApplicationBundleInfo &appBundleInfo, const QString &pluginSourcePath, + const QString pluginDestinationPath, DeploymentInfo deploymentInfo, bool useDebugLibs) +{ + LogNormal() << "Deploying plugins from" << pluginSourcePath; + + if (!pluginSourcePath.contains(deploymentInfo.pluginPath)) + return; + + // Plugin white list: + QStringList pluginList; + + const auto addPlugins = [&pluginSourcePath,&pluginList,useDebugLibs](const QString &subDirectory, + const std::function &predicate = std::function()) { + const QStringList libs = QDir(pluginSourcePath + QLatin1Char('/') + subDirectory) + .entryList({QStringLiteral("*.dylib")}); + for (const QString &lib : libs) { + if (lib.endsWith(QStringLiteral("_debug.dylib")) != useDebugLibs) + continue; + if (!predicate || predicate(lib)) + pluginList.append(subDirectory + QLatin1Char('/') + lib); + } + }; + + // Platform plugin: + addPlugins(QStringLiteral("platforms"), [](const QString &lib) { + // Ignore minimal and offscreen platform plugins + if (!lib.contains(QStringLiteral("cocoa"))) + return false; + return true; + }); + + // Cocoa print support + addPlugins(QStringLiteral("printsupport")); + + // Styles + addPlugins(QStringLiteral("styles")); + + // Check if Qt was configured with -libinfix + const QString libInfix = getLibInfix(deploymentInfo.deployedFrameworks); + + // Network + if (deploymentInfo.containsModule("Network", libInfix)) + addPlugins(QStringLiteral("bearer")); + + // All image formats (svg if QtSvg is used) + const bool usesSvg = deploymentInfo.containsModule("Svg", libInfix); + addPlugins(QStringLiteral("imageformats"), [usesSvg](const QString &lib) { + if (lib.contains(QStringLiteral("qsvg")) && !usesSvg) + return false; + return true; + }); + + addPlugins(QStringLiteral("iconengines")); + + // Platforminputcontext plugins if QtGui is in use + if (deploymentInfo.containsModule("Gui", libInfix)) { + addPlugins(QStringLiteral("platforminputcontexts"), [&addPlugins](const QString &lib) { + // Deploy the virtual keyboard plugins if we have deployed virtualkeyboard + if (lib.startsWith(QStringLiteral("libqtvirtualkeyboard"))) + addPlugins(QStringLiteral("virtualkeyboard")); + return true; + }); + } + + // Sql plugins if QtSql is in use + if (deploymentInfo.containsModule("Sql", libInfix)) { + addPlugins(QStringLiteral("sqldrivers"), [](const QString &lib) { + if (lib.startsWith(QStringLiteral("libqsqlodbc")) || lib.startsWith(QStringLiteral("libqsqlpsql"))) { + LogWarning() << "Plugin" << lib << "uses private API and is not Mac App store compliant."; + if (appstoreCompliant) { + LogWarning() << "Skip plugin" << lib; + return false; + } + } + return true; + }); + } + + // WebView plugins if QtWebView is in use + if (deploymentInfo.containsModule("WebView", libInfix)) { + addPlugins(QStringLiteral("webview"), [](const QString &lib) { + if (lib.startsWith(QStringLiteral("libqtwebview_webengine"))) { + LogWarning() << "Plugin" << lib << "uses QtWebEngine and is not Mac App store compliant."; + if (appstoreCompliant) { + LogWarning() << "Skip plugin" << lib; + return false; + } + } + return true; + }); + } + + static const std::map> map { + {QStringLiteral("Multimedia"), {QStringLiteral("mediaservice"), QStringLiteral("audio")}}, + {QStringLiteral("3DRender"), {QStringLiteral("sceneparsers"), QStringLiteral("geometryloaders"), QStringLiteral("renderers")}}, + {QStringLiteral("3DQuickRender"), {QStringLiteral("renderplugins")}}, + {QStringLiteral("Positioning"), {QStringLiteral("position")}}, + {QStringLiteral("Location"), {QStringLiteral("geoservices")}}, + {QStringLiteral("TextToSpeech"), {QStringLiteral("texttospeech")}} + }; + + for (const auto &it : map) { + if (deploymentInfo.containsModule(it.first, libInfix)) { + for (const auto &pluginType : it.second) { + addPlugins(pluginType); + } + } + } + + foreach (const QString &plugin, pluginList) { + QString sourcePath = pluginSourcePath + "/" + plugin; + const QString destinationPath = pluginDestinationPath + "/" + plugin; + QDir dir; + dir.mkpath(QFileInfo(destinationPath).path()); + + if (copyFilePrintStatus(sourcePath, destinationPath)) { + runStrip(destinationPath); + QList frameworks = getQtFrameworks(destinationPath, appBundleInfo.path, deploymentInfo.rpathsUsed, useDebugLibs); + deployQtFrameworks(frameworks, appBundleInfo.path, QStringList() << destinationPath, useDebugLibs, deploymentInfo.useLoaderPath); + } + } + + // GIO modules + { + const QString sourcePath = "/usr/local/lib/gio/modules/libgiognutls.so"; + const QString destinationPath = appBundleInfo.path + "/Contents/PlugIns/gio-modules/libgiognutls.so"; + QDir dir; + if (dir.mkpath(QFileInfo(destinationPath).path()) && copyFilePrintStatus(sourcePath, destinationPath)) { + runStrip(destinationPath); + QList frameworks = getQtFrameworks(destinationPath, appBundleInfo.path, deploymentInfo.rpathsUsed, useDebugLibs); + deployQtFrameworks(frameworks, appBundleInfo.path, QStringList() << destinationPath, useDebugLibs, deploymentInfo.useLoaderPath); + } + } + + // gst-plugin-scanner + { + const QString sourcePath = "/usr/local/opt/gstreamer/libexec/gstreamer-1.0/gst-plugin-scanner"; + const QString destinationPath = appBundleInfo.path + "/" + "Contents/PlugIns/gst-plugin-scanner"; + QDir dir; + if (dir.mkpath(QFileInfo(destinationPath).path()) && copyFilePrintStatus(sourcePath, destinationPath)) { + runStrip(destinationPath); + QList frameworks = getQtFrameworks(destinationPath, appBundleInfo.path, deploymentInfo.rpathsUsed, useDebugLibs); + deployQtFrameworks(frameworks, appBundleInfo.path, QStringList() << destinationPath, useDebugLibs, deploymentInfo.useLoaderPath); + } + } + + // GStreamer plugins. + QStringList gstreamer_plugins = QStringList() << "libgstapetag.dylib" + << "libgstapp.dylib" + << "libgstaudioconvert.dylib" + << "libgstaudiofx.dylib" + << "libgstaudiomixer.dylib" + << "libgstaudioparsers.dylib" + << "libgstaudiorate.dylib" + << "libgstaudioresample.dylib" + << "libgstaudiotestsrc.dylib" + << "libgstaudiovisualizers.dylib" + << "libgstauparse.dylib" + << "libgstautoconvert.dylib" + << "libgstautodetect.dylib" + << "libgstcoreelements.dylib" + << "libgstequalizer.dylib" + << "libgstgio.dylib" + << "libgsticydemux.dylib" + << "libgstid3demux.dylib" + << "libgstlevel.dylib" + << "libgstosxaudio.dylib" + << "libgstplayback.dylib" + << "libgstrawparse.dylib" + << "libgstreplaygain.dylib" + << "libgstsoup.dylib" + << "libgstspectrum.dylib" + << "libgsttypefindfunctions.dylib" + << "libgstvolume.dylib" + << "libgstxingmux.dylib" + << "libgsttcp.dylib" + << "libgstudp.dylib" + << "libgstpbtypes.dylib" + << "libgstrtp.dylib" + << "libgstrtsp.dylib" + << "libgstflac.dylib" + << "libgstwavparse.dylib" + << "libgstfaac.dylib" + << "libgstfaad.dylib" + << "libgstogg.dylib" + << "libgstopus.dylib" + << "libgstopusparse.dylib" + << "libgstasf.dylib" + << "libgstspeex.dylib" + << "libgsttaglib.dylib" + << "libgstvorbis.dylib" + << "libgstisomp4.dylib" + << "libgstlibav.dylib" + << "libgstaiff.dylib" + << "libgstlame.dylib" + << "libgstmusepack.dylib"; + + QString gstreamer_plugins_dir = "/usr/local/lib/gstreamer-1.0"; + for (const QString &plugin : gstreamer_plugins) { + const QString sourcePath = gstreamer_plugins_dir + "/" + plugin; + const QString destinationPath = appBundleInfo.path + "/Contents/PlugIns/gstreamer/" + plugin; + QDir dir; + if (dir.mkpath(QFileInfo(destinationPath).path()) && copyFilePrintStatus(sourcePath, destinationPath)) { + runStrip(destinationPath); + QList frameworks = getQtFrameworks(destinationPath, appBundleInfo.path, deploymentInfo.rpathsUsed, useDebugLibs); + deployQtFrameworks(frameworks, appBundleInfo.path, QStringList() << destinationPath, useDebugLibs, deploymentInfo.useLoaderPath); + } + } + +} + +void createQtConf(const QString &appBundlePath) +{ + // Set Plugins and imports paths. These are relative to App.app/Contents. + QByteArray contents = "[Paths]\n" + "Plugins = PlugIns\n" + "Imports = Resources/qml\n" + "Qml2Imports = Resources/qml\n"; + + QString filePath = appBundlePath + "/Contents/Resources/"; + QString fileName = filePath + "qt.conf"; + + QDir().mkpath(filePath); + + QFile qtconf(fileName); + if (qtconf.exists() && !alwaysOwerwriteEnabled) { + LogWarning(); + LogWarning() << fileName << "already exists, will not overwrite."; + LogWarning() << "To make sure the plugins are loaded from the correct location,"; + LogWarning() << "please make sure qt.conf contains the following lines:"; + LogWarning() << "[Paths]"; + LogWarning() << " Plugins = PlugIns"; + return; + } + + qtconf.open(QIODevice::WriteOnly); + if (qtconf.write(contents) != -1) { + LogNormal() << "Created configuration file:" << fileName; + LogNormal() << "This file sets the plugin search path to" << appBundlePath + "/Contents/PlugIns"; + } +} + +void deployPlugins(const QString &appBundlePath, DeploymentInfo deploymentInfo, bool useDebugLibs) +{ + ApplicationBundleInfo applicationBundle; + applicationBundle.path = appBundlePath; + applicationBundle.binaryPath = findAppBinary(appBundlePath); + + const QString pluginDestinationPath = appBundlePath + "/" + "Contents/PlugIns"; + deployPlugins(applicationBundle, deploymentInfo.pluginPath, pluginDestinationPath, deploymentInfo, useDebugLibs); +} + +void deployQmlImport(const QString &appBundlePath, const QSet &rpaths, const QString &importSourcePath, const QString &importName) +{ + QString importDestinationPath = appBundlePath + "/Contents/Resources/qml/" + importName; + + // Skip already deployed imports. This can happen in cases like "QtQuick.Controls.Styles", + // where deploying QtQuick.Controls will also deploy the "Styles" sub-import. + if (QDir().exists(importDestinationPath)) + return; + + recursiveCopyAndDeploy(appBundlePath, rpaths, importSourcePath, importDestinationPath); +} + +static bool importLessThan(const QVariant &v1, const QVariant &v2) +{ + QVariantMap import1 = v1.toMap(); + QVariantMap import2 = v2.toMap(); + QString path1 = import1["path"].toString(); + QString path2 = import2["path"].toString(); + return path1 < path2; +} + +// Scan qml files in qmldirs for import statements, deploy used imports from Qml2ImportsPath to Contents/Resources/qml. +bool deployQmlImports(const QString &appBundlePath, DeploymentInfo deploymentInfo, QStringList &qmlDirs, QStringList &qmlImportPaths) +{ + LogNormal() << ""; + LogNormal() << "Deploying QML imports "; + LogNormal() << "Application QML file path(s) is" << qmlDirs; + LogNormal() << "QML module search path(s) is" << qmlImportPaths; + + // Use qmlimportscanner from QLibraryInfo::BinariesPath + QString qmlImportScannerPath = QDir::cleanPath(QLibraryInfo::path(QLibraryInfo::BinariesPath) + "/qmlimportscanner"); + + // Fallback: Look relative to the macdeployqt binary + if (!QFile(qmlImportScannerPath).exists()) + qmlImportScannerPath = QCoreApplication::applicationDirPath() + "/qmlimportscanner"; + + // Verify that we found a qmlimportscanner binary + if (!QFile(qmlImportScannerPath).exists()) { + LogError() << "qmlimportscanner not found at" << qmlImportScannerPath; + LogError() << "Rebuild qtdeclarative/tools/qmlimportscanner"; + return false; + } + + // build argument list for qmlimportsanner: "-rootPath foo/ -rootPath bar/ -importPath path/to/qt/qml" + // ("rootPath" points to a directory containing app qml, "importPath" is where the Qt imports are installed) + QStringList argumentList; + foreach (const QString &qmlDir, qmlDirs) { + argumentList.append("-rootPath"); + argumentList.append(qmlDir); + } + for (const QString &importPath : qmlImportPaths) + argumentList << "-importPath" << importPath; + QString qmlImportsPath = QLibraryInfo::path(QLibraryInfo::Qml2ImportsPath); + argumentList.append( "-importPath"); + argumentList.append(qmlImportsPath); + + // run qmlimportscanner + QProcess qmlImportScanner; + qmlImportScanner.start(qmlImportScannerPath, argumentList); + if (!qmlImportScanner.waitForStarted()) { + LogError() << "Could not start qmlimpoortscanner. Process error is" << qmlImportScanner.errorString(); + return false; + } + qmlImportScanner.waitForFinished(); + + // log qmlimportscanner errors + qmlImportScanner.setReadChannel(QProcess::StandardError); + QByteArray errors = qmlImportScanner.readAll(); + if (!errors.isEmpty()) { + LogWarning() << "QML file parse error (deployment will continue):"; + LogWarning() << errors; + } + + // parse qmlimportscanner json + qmlImportScanner.setReadChannel(QProcess::StandardOutput); + QByteArray json = qmlImportScanner.readAll(); + QJsonDocument doc = QJsonDocument::fromJson(json); + if (!doc.isArray()) { + LogError() << "qmlimportscanner output error. Expected json array, got:"; + LogError() << json; + return false; + } + + // sort imports to deploy a module before its sub-modules (otherwise + // deployQmlImports can consider the module deployed if it has already + // deployed one of its sub-module) + QVariantList array = doc.array().toVariantList(); + std::sort(array.begin(), array.end(), importLessThan); + + // deploy each import + foreach (const QVariant &importValue, array) { + QVariantMap import = importValue.toMap(); + QString name = import["name"].toString(); + QString path = import["path"].toString(); + QString type = import["type"].toString(); + + LogNormal() << "Deploying QML import" << name; + + // Skip imports with missing info - path will be empty if the import is not found. + if (name.isEmpty() || path.isEmpty()) { + LogNormal() << " Skip import: name or path is empty"; + LogNormal() << ""; + continue; + } + + // Deploy module imports only, skip directory (local/remote) and js imports. These + // should be deployed as a part of the application build. + if (type != QStringLiteral("module")) { + LogNormal() << " Skip non-module import"; + LogNormal() << ""; + continue; + } + + // Create the destination path from the name + // and version (grabbed from the source path) + // ### let qmlimportscanner provide this. + name.replace(QLatin1Char('.'), QLatin1Char('/')); + int secondTolast = path.length() - 2; + QString version = path.mid(secondTolast); + if (version.startsWith(QLatin1Char('.'))) + name.append(version); + + deployQmlImport(appBundlePath, deploymentInfo.rpathsUsed, path, name); + LogNormal() << ""; + } + return true; +} + +void codesignFile(const QString &identity, const QString &filePath) +{ + if (!runCodesign) + return; + + QString codeSignLogMessage = "codesign"; + if (hardenedRuntime) + codeSignLogMessage += ", enable hardened runtime"; + if (secureTimestamp) + codeSignLogMessage += ", include secure timestamp"; + LogNormal() << codeSignLogMessage << filePath; + + QStringList codeSignOptions = { "--preserve-metadata=identifier,entitlements", "--force", "-s", + identity, filePath }; + if (hardenedRuntime) + codeSignOptions << "-o" << "runtime"; + + if (secureTimestamp) + codeSignOptions << "--timestamp"; + + if (!extraEntitlements.isEmpty()) + codeSignOptions << "--entitlements" << extraEntitlements; + + QProcess codesign; + codesign.start("codesign", codeSignOptions); + codesign.waitForFinished(-1); + + QByteArray err = codesign.readAllStandardError(); + if (codesign.exitCode() > 0) { + LogError() << "Codesign signing error:"; + LogError() << err; + } else if (!err.isEmpty()) { + LogDebug() << err; + } +} + +QSet codesignBundle(const QString &identity, + const QString &appBundlePath, + QList additionalBinariesContainingRpaths) +{ + // Code sign all binaries in the app bundle. This needs to + // be done inside-out, e.g sign framework dependencies + // before the main app binary. The codesign tool itself has + // a "--deep" option to do this, but usage when signing is + // not recommended: "Signing with --deep is for emergency + // repairs and temporary adjustments only." + + LogNormal() << ""; + LogNormal() << "Signing" << appBundlePath << "with identity" << identity; + + QStack pendingBinaries; + QSet pendingBinariesSet; + QSet signedBinaries; + + // Create the root code-binary set. This set consists of the application + // executable(s) and the plugins. + QString appBundleAbsolutePath = QFileInfo(appBundlePath).absoluteFilePath(); + QString rootBinariesPath = appBundleAbsolutePath + "/Contents/MacOS/"; + QStringList foundRootBinaries = QDir(rootBinariesPath).entryList(QStringList() << "*", QDir::Files); + foreach (const QString &binary, foundRootBinaries) { + QString binaryPath = rootBinariesPath + binary; + pendingBinaries.push(binaryPath); + pendingBinariesSet.insert(binaryPath); + additionalBinariesContainingRpaths.append(binaryPath); + } + + bool getAbsoltuePath = true; + QStringList foundPluginBinaries = findAppBundleFiles(appBundlePath + "/Contents/PlugIns/", getAbsoltuePath); + foreach (const QString &binary, foundPluginBinaries) { + pendingBinaries.push(binary); + pendingBinariesSet.insert(binary); + } + + // Add frameworks for processing. + QStringList frameworkPaths = findAppFrameworkPaths(appBundlePath); + foreach (const QString &frameworkPath, frameworkPaths) { + + // Prioritise first to sign any additional inner bundles found in the Helpers folder (e.g + // used by QtWebEngine). + QDirIterator helpersIterator(frameworkPath, QStringList() << QString::fromLatin1("Helpers"), QDir::Dirs | QDir::NoSymLinks, QDirIterator::Subdirectories); + while (helpersIterator.hasNext()) { + helpersIterator.next(); + QString helpersPath = helpersIterator.filePath(); + QStringList innerBundleNames = QDir(helpersPath).entryList(QStringList() << "*.app", QDir::Dirs); + foreach (const QString &innerBundleName, innerBundleNames) + signedBinaries += codesignBundle(identity, + helpersPath + "/" + innerBundleName, + additionalBinariesContainingRpaths); + } + + // Also make sure to sign any libraries that will not be found by otool because they + // are not linked and won't be seen as a dependency. + QDirIterator librariesIterator(frameworkPath, QStringList() << QString::fromLatin1("Libraries"), QDir::Dirs | QDir::NoSymLinks, QDirIterator::Subdirectories); + while (librariesIterator.hasNext()) { + librariesIterator.next(); + QString librariesPath = librariesIterator.filePath(); + QStringList bundleFiles = findAppBundleFiles(librariesPath, getAbsoltuePath); + foreach (const QString &binary, bundleFiles) { + pendingBinaries.push(binary); + pendingBinariesSet.insert(binary); + } + } + } + + // Sign all binaries; use otool to find and sign dependencies first. + while (!pendingBinaries.isEmpty()) { + QString binary = pendingBinaries.pop(); + if (signedBinaries.contains(binary)) + continue; + + // Check if there are unsigned dependencies, sign these first. + QStringList dependencies = getBinaryDependencies(rootBinariesPath, binary, + additionalBinariesContainingRpaths); + dependencies = QSet(dependencies.begin(), dependencies.end()) + .subtract(signedBinaries) + .subtract(pendingBinariesSet) + .values(); + + if (!dependencies.isEmpty()) { + pendingBinaries.push(binary); + pendingBinariesSet.insert(binary); + int dependenciesSkipped = 0; + foreach (const QString &dependency, dependencies) { + // Skip dependencies that are outside the current app bundle, because this might + // cause a codesign error if the current bundle is part of the dependency (e.g. + // a bundle is part of a framework helper, and depends on that framework). + // The dependencies will be taken care of after the current bundle is signed. + if (!dependency.startsWith(appBundleAbsolutePath)) { + ++dependenciesSkipped; + LogNormal() << "Skipping outside dependency: " << dependency; + continue; + } + pendingBinaries.push(dependency); + pendingBinariesSet.insert(dependency); + } + + // If all dependencies were skipped, make sure the binary is actually signed, instead + // of going into an infinite loop. + if (dependenciesSkipped == dependencies.size()) { + pendingBinaries.pop(); + } else { + continue; + } + } + + // Look for an entitlements file in the bundle to include when signing + extraEntitlements = findEntitlementsFile(appBundleAbsolutePath + "/Contents/Resources/"); + + // All dependencies are signed, now sign this binary. + codesignFile(identity, binary); + signedBinaries.insert(binary); + pendingBinariesSet.remove(binary); + } + + LogNormal() << "Finished codesigning " << appBundlePath << "with identity" << identity; + + // Verify code signature + QProcess codesign; + codesign.start("codesign", QStringList() << "--deep" << "-v" << appBundlePath); + codesign.waitForFinished(-1); + QByteArray err = codesign.readAllStandardError(); + if (codesign.exitCode() > 0) { + LogError() << "codesign verification error:"; + LogError() << err; + } else if (!err.isEmpty()) { + LogDebug() << err; + } + + return signedBinaries; +} + +void codesign(const QString &identity, const QString &appBundlePath) { + codesignBundle(identity, appBundlePath, QList()); +} + +void createDiskImage(const QString &appBundlePath, const QString &filesystemType) +{ + QString appBaseName = appBundlePath; + appBaseName.chop(4); // remove ".app" from end + + QString dmgName = appBaseName + ".dmg"; + + QFile dmg(dmgName); + + if (dmg.exists() && alwaysOwerwriteEnabled) + dmg.remove(); + + if (dmg.exists()) { + LogNormal() << "Disk image already exists, skipping .dmg creation for" << dmg.fileName(); + } else { + LogNormal() << "Creating disk image (.dmg) for" << appBundlePath; + } + + LogNormal() << "Image will use" << filesystemType; + + // More dmg options can be found in the hdiutil man page. + QStringList options = QStringList() + << "create" << dmgName + << "-srcfolder" << appBundlePath + << "-format" << "UDZO" + << "-fs" << filesystemType + << "-volname" << appBaseName; + + QProcess hdutil; + hdutil.start("hdiutil", options); + hdutil.waitForFinished(-1); + if (hdutil.exitCode() != 0) { + LogError() << "Bundle creation error:" << hdutil.readAllStandardError(); + } +} + +void fixupFramework(const QString &frameworkName) +{ + // Expected framework name looks like "Foo.framework" + QStringList parts = frameworkName.split("."); + if (parts.count() < 2) { + LogError() << "fixupFramework: Unexpected framework name" << frameworkName; + return; + } + + // Assume framework binary path is Foo.framework/Foo + QString frameworkBinary = frameworkName + QStringLiteral("/") + parts[0]; + + // Xcode expects to find Foo.framework/Versions/A when code + // signing, while qmake typically generates numeric versions. + // Create symlink to the actual version in the framework. + linkFilePrintStatus("Current", frameworkName + "/Versions/A"); + + // Set up @rpath structure. + changeIdentification("@rpath/" + frameworkBinary, frameworkBinary); + addRPath("@loader_path/../../Contents/Frameworks/", frameworkBinary); +} diff --git a/3rdparty/macdeployqt/shared.h b/3rdparty/macdeployqt/shared.h new file mode 100644 index 00000000..15ff0843 --- /dev/null +++ b/3rdparty/macdeployqt/shared.h @@ -0,0 +1,141 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the tools applications of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +#ifndef MAC_DEPLOMYMENT_SHARED_H +#define MAC_DEPLOMYMENT_SHARED_H + +#include +#include +#include +#include +#include + +extern int logLevel; +#define LogError() if (logLevel < 0) {} else qDebug() << "ERROR:" +#define LogWarning() if (logLevel < 1) {} else qDebug() << "WARNING:" +#define LogNormal() if (logLevel < 2) {} else qDebug() << "Log:" +#define LogDebug() if (logLevel < 3) {} else qDebug() << "Log:" + +extern bool runStripEnabled; + +class FrameworkInfo +{ +public: + bool isDylib; + QString frameworkDirectory; + QString frameworkName; + QString frameworkPath; + QString binaryDirectory; + QString binaryName; + QString binaryPath; + QString rpathUsed; + QString version; + QString installName; + QString deployedInstallName; + QString sourceFilePath; + QString frameworkDestinationDirectory; + QString binaryDestinationDirectory; + + bool isDebugLibrary() const + { + return binaryName.contains(QLatin1String("_debug")); + } +}; + +class DylibInfo +{ +public: + QString binaryPath; + QVersionNumber currentVersion; + QVersionNumber compatibilityVersion; +}; + +class OtoolInfo +{ +public: + QString installName; + QString binaryPath; + QVersionNumber currentVersion; + QVersionNumber compatibilityVersion; + QList dependencies; +}; + +bool operator==(const FrameworkInfo &a, const FrameworkInfo &b); +QDebug operator<<(QDebug debug, const FrameworkInfo &info); + +class ApplicationBundleInfo +{ + public: + QString path; + QString binaryPath; + QStringList libraryPaths; +}; + +class DeploymentInfo +{ +public: + QString qtPath; + QString pluginPath; + QStringList deployedFrameworks; + QSet rpathsUsed; + bool useLoaderPath; + bool isFramework; + bool isDebug; + + bool containsModule(const QString &module, const QString &libInFix) const; +}; + +inline QDebug operator<<(QDebug debug, const ApplicationBundleInfo &info); + +OtoolInfo findDependencyInfo(const QString &binaryPath); +FrameworkInfo parseOtoolLibraryLine(const QString &line, const QString &appBundlePath, const QSet &rpaths, bool useDebugLibs); +QString findAppBinary(const QString &appBundlePath); +QList getQtFrameworks(const QString &path, const QString &appBundlePath, const QSet &rpaths, bool useDebugLibs); +QList getQtFrameworks(const QStringList &otoolLines, const QString &appBundlePath, const QSet &rpaths, bool useDebugLibs); +QString copyFramework(const FrameworkInfo &framework, const QString path); +DeploymentInfo deployQtFrameworks(const QString &appBundlePath, const QStringList &additionalExecutables, bool useDebugLibs); +DeploymentInfo deployQtFrameworks(QList frameworks,const QString &bundlePath, const QStringList &binaryPaths, bool useDebugLibs, bool useLoaderPath); +void createQtConf(const QString &appBundlePath); +void deployPlugins(const QString &appBundlePath, DeploymentInfo deploymentInfo, bool useDebugLibs); +bool deployQmlImports(const QString &appBundlePath, DeploymentInfo deploymentInfo, QStringList &qmlDirs, QStringList &qmlImportPaths); +void changeIdentification(const QString &id, const QString &binaryPath); +void changeInstallName(const QString &oldName, const QString &newName, const QString &binaryPath); +void runStrip(const QString &binaryPath); +void stripAppBinary(const QString &bundlePath); +QString findAppBinary(const QString &appBundlePath); +QStringList findAppFrameworkNames(const QString &appBundlePath); +QStringList findAppFrameworkPaths(const QString &appBundlePath); +void codesignFile(const QString &identity, const QString &filePath); +QSet codesignBundle(const QString &identity, + const QString &appBundlePath, + QList additionalBinariesContainingRpaths); +void codesign(const QString &identity, const QString &appBundlePath); +void createDiskImage(const QString &appBundlePath, const QString &filesystemType); +void fixupFramework(const QString &appBundlePath); + + +#endif diff --git a/CMakeLists.txt b/CMakeLists.txt index 839f22d8..725f6a39 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -252,6 +252,7 @@ set(SINGLECOREAPPLICATION_LIBRARIES singlecoreapplication) if(APPLE) find_library(SPARKLE Sparkle) + add_subdirectory(3rdparty/macdeployqt) add_subdirectory(3rdparty/SPMediaKeyTap) set(SPMEDIAKEYTAP_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/SPMediaKeyTap) set(SPMEDIAKEYTAP_LIBRARIES SPMediaKeyTap) diff --git a/cmake/Dmg.cmake b/cmake/Dmg.cmake index a243ecd1..2cc75c77 100644 --- a/cmake/Dmg.cmake +++ b/cmake/Dmg.cmake @@ -1,9 +1,11 @@ -find_program(MACDEPLOYQT_EXECUTABLE NAMES macdeployqt PATHS /usr/local/opt/qt6/bin /usr/local/opt/qt5/bin /usr/local/bin REQUIRED) -if(MACDEPLOYQT_EXECUTABLE) - message(STATUS "Found macdeployqt: ${MACDEPLOYQT_EXECUTABLE}") -else() - message(WARNING "Missing macdeployqt executable.") -endif() +#find_program(MACDEPLOYQT_EXECUTABLE NAMES macdeployqt PATHS /usr/local/opt/qt6/bin /usr/local/opt/qt5/bin /usr/local/bin REQUIRED) +#if(MACDEPLOYQT_EXECUTABLE) +# message(STATUS "Found macdeployqt: ${MACDEPLOYQT_EXECUTABLE}") +#else() +# message(WARNING "Missing macdeployqt executable.") +#endif() + +set(MACDEPLOYQT_EXECUTABLE "${CMAKE_BINARY_DIR}/3rdparty/macdeployqt/macdeployqt") find_program(CREATEDMG_EXECUTABLE NAMES create-dmg REQUIRED) if(CREATEDMG_EXECUTABLE) @@ -20,13 +22,11 @@ endif() if(MACDEPLOYQT_EXECUTABLE AND CREATEDMG_EXECUTABLE AND MACOS_VERSION_PACKAGE) add_custom_target(dmg COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/strawberry-tagreader -verbose=3 - COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macdeploy.py strawberry.app COMMAND ${CREATEDMG_EXECUTABLE} --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 strawberry-${STRAWBERRY_VERSION_PACKAGE}-${MACOS_VERSION_PACKAGE}-${CMAKE_HOST_SYSTEM_PROCESSOR}.dmg strawberry.app WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) add_custom_target(dmg2 COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/strawberry-tagreader -verbose=3 - COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macdeploy.py strawberry.app COMMAND ${CREATEDMG_EXECUTABLE} --skip-jenkins --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 strawberry-${STRAWBERRY_VERSION_PACKAGE}-${MACOS_VERSION_PACKAGE}-${CMAKE_HOST_SYSTEM_PROCESSOR}.dmg strawberry.app WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) diff --git a/dist/macos/macdeploy.py b/dist/macos/macdeploy.py deleted file mode 100755 index f1c8dd75..00000000 --- a/dist/macos/macdeploy.py +++ /dev/null @@ -1,513 +0,0 @@ -#!/usr/bin/python - -# Strawberry Music Player -# This file was part of Clementine. -# -# Strawberry 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 3 of the License, or -# (at your option) any later version. -# -# Strawberry 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 Strawberry. If not, see . - -from distutils import spawn -import logging -import os -import re -import subprocess -import sys -import traceback - -LOGGER = logging.getLogger('macdeploy') - -LIBRARY_SEARCH_PATH = ['/usr/local/lib', '/usr/local/opt/icu4c/lib'] - -FRAMEWORK_SEARCH_PATH = [ - '/Library/Frameworks', - os.path.join(os.environ['HOME'], 'Library/Frameworks'), - '/Library/Frameworks/Sparkle.framework/Versions' -] - -QT_PLUGINS = [ - 'platforms/libqcocoa.dylib', - 'styles/libqmacstyle.dylib', - 'sqldrivers/libqsqlite.dylib', - 'iconengines/libqsvgicon.dylib', - 'imageformats/libqgif.dylib', - 'imageformats/libqicns.dylib', - 'imageformats/libqico.dylib', - 'imageformats/libqjpeg.dylib', - 'imageformats/libqjp2.dylib', - 'imageformats/libqsvg.dylib', - 'imageformats/libqtiff.dylib', -] - -QT_PLUGINS_SEARCH_PATH = [ - '/usr/local/opt/qt6/share/qt/plugins', -] - -GSTREAMER_SEARCH_PATH = [ - '/usr/local/lib/gstreamer-1.0', - '/usr/local/Cellar/gstreamer', -] - -GSTREAMER_PLUGINS = [ - - 'libgstapetag.dylib', - 'libgstapp.dylib', - 'libgstaudioconvert.dylib', - 'libgstaudiofx.dylib', - 'libgstaudiomixer.dylib', - 'libgstaudioparsers.dylib', - 'libgstaudiorate.dylib', - 'libgstaudioresample.dylib', - 'libgstaudiotestsrc.dylib', - 'libgstaudiovisualizers.dylib', - 'libgstauparse.dylib', - 'libgstautoconvert.dylib', - 'libgstautodetect.dylib', - 'libgstcoreelements.dylib', - 'libgstequalizer.dylib', - 'libgstgio.dylib', - 'libgsticydemux.dylib', - 'libgstid3demux.dylib', - 'libgstlevel.dylib', - 'libgstosxaudio.dylib', - 'libgstplayback.dylib', - 'libgstrawparse.dylib', - 'libgstreplaygain.dylib', - 'libgstsoup.dylib', - 'libgstspectrum.dylib', - 'libgsttypefindfunctions.dylib', - 'libgstvolume.dylib', - 'libgstxingmux.dylib', - 'libgsttcp.dylib', - 'libgstudp.dylib', - 'libgstpbtypes.dylib', - 'libgstrtp.dylib', - 'libgstrtsp.dylib', - - 'libgstflac.dylib', - 'libgstwavparse.dylib', - 'libgstfaac.dylib', - 'libgstfaad.dylib', - 'libgstogg.dylib', - 'libgstopus.dylib', - 'libgstopusparse.dylib', - 'libgstasf.dylib', - 'libgstspeex.dylib', - 'libgsttaglib.dylib', - 'libgstvorbis.dylib', - 'libgstisomp4.dylib', - 'libgstlibav.dylib', - 'libgstaiff.dylib', - 'libgstlame.dylib', - 'libgstmusepack.dylib', - -] - -GIO_MODULES_SEARCH_PATH = ['/usr/local/lib/gio/modules',] - -INSTALL_NAME_TOOL_APPLE = 'install_name_tool' -INSTALL_NAME_TOOL_CROSS = 'x86_64-apple-darwin-%s' % INSTALL_NAME_TOOL_APPLE -INSTALL_NAME_TOOL = INSTALL_NAME_TOOL_CROSS if spawn.find_executable(INSTALL_NAME_TOOL_CROSS) else INSTALL_NAME_TOOL_APPLE - -OTOOL_APPLE = 'otool' -OTOOL_CROSS = 'x86_64-apple-darwin-%s' % OTOOL_APPLE -OTOOL = OTOOL_CROSS if spawn.find_executable(OTOOL_CROSS) else OTOOL_APPLE - - -class Error(Exception): - pass - - -class CouldNotFindFrameworkError(Error): - pass - -class CouldNotFindGioModuleError(Error): - pass - -class CouldNotFindQtPluginError(Error): - pass - -class CouldNotParseFrameworkNameError(Error): - pass - -class InstallNameToolError(Error): - pass - - -class CouldNotFindGstreamerPluginError(Error): - pass - -if len(sys.argv) < 2: - print('Usage: %s ' % sys.argv[0]) - -bundle_dir = sys.argv[1] - -bundle_name = os.path.basename(bundle_dir).split('.')[0] - -commands = [] - -frameworks_dir = os.path.join(bundle_dir, 'Contents', 'Frameworks') -commands.append(['mkdir', '-p', frameworks_dir]) -resources_dir = os.path.join(bundle_dir, 'Contents', 'Resources') -commands.append(['mkdir', '-p', resources_dir]) -plugins_dir = os.path.join(bundle_dir, 'Contents', 'PlugIns') -binary = os.path.join(bundle_dir, 'Contents', 'MacOS', bundle_name) -tagreader_binary = os.path.join(plugins_dir, bundle_name + "-tagreader") - -fixed_libraries = set() -fixed_frameworks = set() - - -def GetBrokenLibraries(binary): - #print("Checking libs for binary: %s" % binary) - output = subprocess.Popen([OTOOL, '-L', binary], stdout=subprocess.PIPE).communicate()[0].decode('utf-8') - broken_libs = {'frameworks': [], 'libs': []} - for line in [x.split(' ')[0].lstrip() for x in output.split('\n')[1:]]: - #print("Checking line: %s" % line) - if not line: # skip empty lines - continue - if os.path.basename(binary) == os.path.basename(line): - #print("mnope %s-%s" % (os.path.basename(binary), os.path.basename(line))) - continue - if re.match(r'^\s*/System/', line): - #print("system framework: %s" % line) - continue # System framework - elif re.match(r'^\s*/usr/lib/', line): - #print("unix style system lib: %s" % line) - continue # unix style system library - elif re.match(r'^\s*@executable_path', line) or re.match(r'^\s*@rpath', line) or re.match(r'^\s*@loader_path', line): - # Potentially already fixed library - if line.count('/') == 1: - relative_path = os.path.join(*line.split('/')[1:]) - if not os.path.exists(os.path.join(frameworks_dir, relative_path)): - broken_libs['libs'].append(relative_path) - elif line.count('/') == 2: - relative_path = os.path.join(*line.split('/')[2:]) - if not os.path.exists(os.path.join(frameworks_dir, relative_path)): - broken_libs['libs'].append(relative_path) - elif line.count('/') >= 3: - relative_path = os.path.join(*line.split('/')[3:]) - if not os.path.exists(os.path.join(frameworks_dir, relative_path)): - broken_libs['frameworks'].append(relative_path) - else: - print("GetBrokenLibraries Error: %s" % line) - elif re.search(r'\w+\.framework', line): - #print("framework: %s" % line) - broken_libs['frameworks'].append(line) - else: - broken_libs['libs'].append(line) - - return broken_libs - - -def FindFramework(path): - for search_path in FRAMEWORK_SEARCH_PATH: - abs_path = os.path.join(search_path, path) - if os.path.exists(abs_path): - LOGGER.debug("Found framework '%s' in '%s'", path, search_path) - return abs_path - - raise CouldNotFindFrameworkError(path) - - -def FindLibrary(path): - if os.path.exists(path): - return path - for search_path in LIBRARY_SEARCH_PATH: - abs_path = os.path.join(search_path, path) - if os.path.exists(abs_path): - LOGGER.debug("Found library '%s' in '%s'", path, search_path) - return abs_path - else: # try harder---look for lib name in library folders - newpath = os.path.join(search_path,os.path.basename(path)) - if os.path.exists(newpath): - return newpath - - raise CouldNotFindFrameworkError(path) - - -def FixAllLibraries(broken_libs): - for framework in broken_libs['frameworks']: - FixFramework(framework) - for lib in broken_libs['libs']: - FixLibrary(lib) - - -def FixFramework(path): - if path in fixed_frameworks: - return - else: - fixed_frameworks.add(path) - abs_path = FindFramework(path) - broken_libs = GetBrokenLibraries(abs_path) - FixAllLibraries(broken_libs) - - new_path = CopyFramework(abs_path) - id = os.sep.join(new_path.split(os.sep)[3:]) - FixFrameworkId(new_path, id) - for framework in broken_libs['frameworks']: - FixFrameworkInstallPath(framework, new_path) - for library in broken_libs['libs']: - FixLibraryInstallPath(library, new_path) - - -def FixLibrary(path): - - if path in fixed_libraries: - return - - # Always bundle libraries provided by homebrew (/usr/local). - if not re.match(r'^\s*/usr/local', path) and FindSystemLibrary(os.path.basename(path)) is not None: - return - - fixed_libraries.add(path) - - abs_path = FindLibrary(path) - if abs_path == "": - print("Could not resolve %s, not fixing!" % path) - return - - broken_libs = GetBrokenLibraries(abs_path) - FixAllLibraries(broken_libs) - - new_path = CopyLibrary(abs_path) - FixLibraryId(new_path) - for framework in broken_libs['frameworks']: - FixFrameworkInstallPath(framework, new_path) - for library in broken_libs['libs']: - FixLibraryInstallPath(library, new_path) - - -def FixPlugin(abs_path, subdir): - broken_libs = GetBrokenLibraries(abs_path) - FixAllLibraries(broken_libs) - - new_path = CopyPlugin(abs_path, subdir) - for framework in broken_libs['frameworks']: - FixFrameworkInstallPath(framework, new_path) - for library in broken_libs['libs']: - FixLibraryInstallPath(library, new_path) - - -def FixBinary(path): - broken_libs = GetBrokenLibraries(path) - FixAllLibraries(broken_libs) - for framework in broken_libs['frameworks']: - FixFrameworkInstallPath(framework, path) - for library in broken_libs['libs']: - FixLibraryInstallPath(library, path) - - -def CopyLibrary(path): - new_path = os.path.join(frameworks_dir, os.path.basename(path)) - #args = ['cp', path, new_path] - args = ['ditto', '--arch=i386', '--arch=x86_64', path, new_path] - commands.append(args) - commands.append(['chmod', '+w', new_path]) - LOGGER.info("Copying library '%s'", path) - return new_path - - -def CopyPlugin(path, subdir): - new_path = os.path.join(plugins_dir, subdir, os.path.basename(path)) - args = ['mkdir', '-p', os.path.dirname(new_path)] - commands.append(args) - #args = ['cp', path, new_path] - args = ['ditto', '--arch=i386', '--arch=x86_64', path, new_path] - commands.append(args) - commands.append(['chmod', '+w', new_path]) - LOGGER.info("Copying plugin '%s'", path) - return new_path - -def CopyFramework(path): - parts = path.split(os.sep) - for i, part in enumerate(parts): - if re.match(r'\w+\.framework', part): - full_path = os.path.join(frameworks_dir, *parts[i:-1]) - framework_name = part.split(".framework")[0] - break - -def CopyFramework(src_binary): - while os.path.islink(src_binary): - src_binary = os.path.realpath(src_binary) - - m = re.match(r'(.*/([^/]+)\.framework)/Versions/([^/]+)/.*', src_binary) - if not m: - raise CouldNotParseFrameworkNameError(src_binary) - - src_base = m.group(1) - name = m.group(2) - version = m.group(3) - - LOGGER.info('Copying framework %s version %s', name, version) - - dest_base = os.path.join(frameworks_dir, '%s.framework' % name) - dest_dir = os.path.join(dest_base, 'Versions', version) - dest_binary = os.path.join(dest_dir, name) - - commands.append(['mkdir', '-p', dest_dir]) - commands.append(['cp', src_binary, dest_binary]) - commands.append(['chmod', '+w', dest_binary]) - - # Copy special files from various places: - # QtCore has Resources/qt_menu.nib (copy to app's Resources) - # Sparkle has Resources/* - # Qt* have Resources/Info.plist - resources_src = os.path.join(src_base, 'Resources') - menu_nib = os.path.join(resources_src, 'qt_menu.nib') - if os.path.exists(menu_nib): - LOGGER.info("Copying qt_menu.nib '%s'", menu_nib) - commands.append(['cp', '-r', menu_nib, resources_dir]) - elif os.path.exists(resources_src): - LOGGER.info("Copying resources dir '%s'", resources_src) - commands.append(['cp', '-r', resources_src, dest_dir]) - - info_plist = os.path.join(src_base, 'Contents', 'Info.plist') - if os.path.exists(info_plist): - LOGGER.info("Copying special file '%s'", info_plist) - resources_dest = os.path.join(dest_dir, 'Resources') - commands.append(['mkdir', resources_dest]) - commands.append(['cp', '-r', info_plist, resources_dest]) - - # Create symlinks in the Framework to make it look like - # https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html - commands.append([ - 'ln', '-sf', 'Versions/Current/%s' % name, os.path.join(dest_base, name) - ]) - commands.append([ - 'ln', '-sf', 'Versions/Current/Resources', - os.path.join(dest_base, 'Resources') - ]) - commands.append( - ['ln', '-sf', version, os.path.join(dest_base, 'Versions/Current')]) - - return dest_binary - - -def FixId(path, library_name): - id = '@executable_path/../Frameworks/%s' % library_name - args = [INSTALL_NAME_TOOL, '-id', id, path] - commands.append(args) - - -def FixLibraryId(path): - library_name = os.path.basename(path) - FixId(path, library_name) - - -def FixFrameworkId(path, id): - FixId(path, id) - - -def FixInstallPath(library_path, library, new_path): - args = [INSTALL_NAME_TOOL, '-change', library_path, new_path, library] - commands.append(args) - - -def FindSystemLibrary(library_name): - for path in ['/lib', '/usr/lib']: - full_path = os.path.join(path, library_name) - if os.path.exists(full_path): - return full_path - return None - - -def FixLibraryInstallPath(library_path, library): - system_library = FindSystemLibrary(os.path.basename(library_path)) - if system_library is None or re.match(r'^\s*/usr/local', library_path): - new_path = '@executable_path/../Frameworks/%s' % os.path.basename(library_path) - FixInstallPath(library_path, library, new_path) - else: - FixInstallPath(library_path, library, system_library) - - -def FixFrameworkInstallPath(library_path, library): - parts = library_path.split(os.sep) - full_path = "" - for i, part in enumerate(parts): - if re.match(r'\w+\.framework', part): - full_path = os.path.join(*parts[i:]) - break - if full_path: - new_path = '@executable_path/../Frameworks/%s' % full_path - FixInstallPath(library_path, library, new_path) - - -def FindQtPlugin(name): - for path in QT_PLUGINS_SEARCH_PATH: - if os.path.exists(path): - if os.path.exists(os.path.join(path, name)): - return os.path.join(path, name) - raise CouldNotFindQtPluginError(name) - - -def FindGstreamerPlugin(name): - for path in GSTREAMER_SEARCH_PATH: - if os.path.exists(path): - for dir, dirs, files in os.walk(path): - if name in files: - return os.path.join(dir, name) - raise CouldNotFindGstreamerPluginError(name) - - -def FindGioModule(name): - for path in GIO_MODULES_SEARCH_PATH: - if os.path.exists(path): - for dir, dirs, files in os.walk(path): - if name in files: - return os.path.join(dir, name) - raise CouldNotFindGioModuleError(name) - - -def main(): - logging.basicConfig(filename='macdeploy.log', level=logging.DEBUG, format='%(asctime)s %(levelname)-8s %(message)s') - - fixed_frameworks.add('A/QtCore') - fixed_frameworks.add('A/QtSql') - fixed_frameworks.add('A/QtGui') - fixed_frameworks.add('A/QtWidgets') - fixed_frameworks.add('A/QtSvg') - - FixBinary(binary) - FixBinary(tagreader_binary) - - # macdeployqt needs to handle strawberry-tagreader for Qt deployment, so we can't use FixPlugin() here. - #try: - # FixPlugin('strawberry-tagreader', '.') - #except: - # print('Failed to find blob: %s' % traceback.format_exc()) - - for plugin in GSTREAMER_PLUGINS: - FixPlugin(FindGstreamerPlugin(plugin), 'gstreamer') - - FixPlugin(FindGstreamerPlugin('gst-plugin-scanner'), '.') - FixPlugin(FindGioModule('libgiognutls.so'), 'gio-modules') - #FixPlugin(FindGioModule('libgiognomeproxy.so'), 'gio-modules') - - for plugin in QT_PLUGINS: - FixPlugin(FindQtPlugin(plugin), os.path.dirname(plugin)) - - if len(sys.argv) <= 2: - print('Would run %d commands:' % len(commands)) - for command in commands: - print(' '.join(command)) - - #print('OK?') - #raw_input() - - for command in commands: - p = subprocess.Popen(command) - os.waitpid(p.pid, 0) - - -if __name__ == "__main__": - main()