| /**************************************************************************** |
| ** |
| ** 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$ |
| ** |
| ****************************************************************************/ |
| #include <QCoreApplication> |
| #include <QString> |
| #include <QStringList> |
| #include <QDebug> |
| #include <iostream> |
| #include <QProcess> |
| #include <QDir> |
| #include <QRegExp> |
| #include <QSet> |
| #include <QStack> |
| #include <QDirIterator> |
| #include <QLibraryInfo> |
| #include <QJsonDocument> |
| #include <QJsonObject> |
| #include <QJsonArray> |
| #include <QJsonValue> |
| #include <QRegularExpression> |
| #include "shared.h" |
| |
| #ifdef Q_OS_DARWIN |
| #include <CoreFoundation/CoreFoundation.h> |
| #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<QString> &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<FrameworkInfo> getQtFrameworks(const QList<DylibInfo> &dependencies, const QString &appBundlePath, const QSet<QString> &rpaths, bool useDebugLibs) |
| { |
| QList<FrameworkInfo> 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<QString> getBinaryRPaths(const QString &path, bool resolve = true, QString executablePath = QString()) |
| { |
| QSet<QString> 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<FrameworkInfo> getQtFrameworks(const QString &path, const QString &appBundlePath, const QSet<QString> &rpaths, bool useDebugLibs) |
| { |
| const OtoolInfo info = findDependencyInfo(path); |
| return getQtFrameworks(info.dependencies, appBundlePath, rpaths + getBinaryRPaths(path), useDebugLibs); |
| } |
| |
| QList<FrameworkInfo> getQtFrameworksForPaths(const QStringList &paths, const QString &appBundlePath, const QSet<QString> &rpaths, bool useDebugLibs) |
| { |
| QList<FrameworkInfo> result; |
| QSet<QString> 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<QString> &additionalBinariesContainingRpaths) |
| { |
| QStringList binaries; |
| |
| const auto dependencies = findDependencyInfo(path).dependencies; |
| |
| bool rpathsLoaded = false; |
| QSet<QString> 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<QString> 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, |
| const QRegularExpression &ignoreRegExp = QRegularExpression()) |
| { |
| if (!QDir(sourcePath).exists()) |
| return false; |
| QDir().mkpath(destinationPath); |
| |
| LogNormal() << "copy:" << sourcePath << destinationPath; |
| |
| QStringList files = QDir(sourcePath).entryList(QStringList() << "*", QDir::Files | QDir::NoDotAndDotDot); |
| const bool hasValidRegExp = ignoreRegExp.isValid(); |
| foreach (QString file, files) { |
| if (hasValidRegExp && ignoreRegExp.match(file).hasMatch()) |
| continue; |
| 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<QString> &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<FrameworkInfo> 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"; |
| // Ignore *.prl files that are in the Resources directory |
| recursiveCopy(resourcesSourcePath, resourcesDestianationPath, |
| QRegularExpression("\\A(?:[^/]*\\.prl)\\z")); |
| 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; |
| 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<QString> &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<QString> &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<FrameworkInfo> 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<QString> 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::location(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<FrameworkInfo> 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<QString> allLibraryPaths = getBinaryRPaths(applicationBundle.binaryPath, true); |
| allLibraryPaths.insert(QLibraryInfo::location(QLibraryInfo::LibrariesPath)); |
| QList<FrameworkInfo> 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<bool(QString)> &predicate = std::function<bool(QString)>()) { |
| 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<QString, std::vector<QString>> map { |
| {QStringLiteral("Multimedia"), {QStringLiteral("mediaservice"), QStringLiteral("audio")}}, |
| {QStringLiteral("3DRender"), {QStringLiteral("sceneparsers"), QStringLiteral("geometryloaders")}}, |
| {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<FrameworkInfo> 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<QString> &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::location(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::location(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 changeQtFrameworks(const QList<FrameworkInfo> frameworks, const QStringList &binaryPaths, const QString &absoluteQtPath) |
| { |
| LogNormal() << "Changing" << binaryPaths << "to link against"; |
| LogNormal() << "Qt in" << absoluteQtPath; |
| QString finalQtPath = absoluteQtPath; |
| |
| if (!absoluteQtPath.startsWith("/Library/Frameworks")) |
| finalQtPath += "/lib/"; |
| |
| foreach (FrameworkInfo framework, frameworks) { |
| const QString oldBinaryId = framework.installName; |
| const QString newBinaryId = finalQtPath + framework.frameworkName + framework.binaryPath; |
| foreach (const QString &binary, binaryPaths) |
| changeInstallName(oldBinaryId, newBinaryId, binary); |
| } |
| } |
| |
| void changeQtFrameworks(const QString appPath, const QString &qtPath, bool useDebugLibs) |
| { |
| const QString appBinaryPath = findAppBinary(appPath); |
| const QStringList libraryPaths = findAppLibraries(appPath); |
| const QList<FrameworkInfo> frameworks = getQtFrameworksForPaths(QStringList() << appBinaryPath << libraryPaths, appPath, getBinaryRPaths(appBinaryPath, true), useDebugLibs); |
| if (frameworks.isEmpty()) { |
| LogWarning(); |
| LogWarning() << "Could not find any _external_ Qt frameworks to change in" << appPath; |
| return; |
| } else { |
| const QString absoluteQtPath = QDir(qtPath).absolutePath(); |
| changeQtFrameworks(frameworks, QStringList() << appBinaryPath << libraryPaths, absoluteQtPath); |
| } |
| } |
| |
| 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<QString> codesignBundle(const QString &identity, |
| const QString &appBundlePath, |
| QList<QString> 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<QString> pendingBinaries; |
| QSet<QString> pendingBinariesSet; |
| QSet<QString> 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).toSet() |
| .subtract(signedBinaries) |
| .subtract(pendingBinariesSet) |
| .toList(); |
| |
| 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<QString>()); |
| } |
| |
| 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); |
| } |