diff options
author | Frederik Gladhorn <frederik.gladhorn@theqtcompany.com> | 2014-10-20 19:13:53 +0200 |
---|---|---|
committer | Frederik Gladhorn <frederik.gladhorn@theqtcompany.com> | 2014-10-20 19:13:53 +0200 |
commit | b22b397f6654ca34858943933bf61d363a389aab (patch) | |
tree | bbe6c59d2eaf76caa53e3414fc3dcf983d5cdaf1 /src/macdeployqt/shared/shared.cpp | |
parent | 9a58492ce6b62c0fdab373337f39103f4072ad9d (diff) | |
parent | dac2cd7b261bd7e9b4af094ca53919fbc6da51c1 (diff) | |
download | qttools-b22b397f6654ca34858943933bf61d363a389aab.tar.gz |
Merge remote-tracking branch 'origin/5.4' into dev
Change-Id: Ib8043f1dce5bcb3999965b91d3d7522fd1289eb0
Diffstat (limited to 'src/macdeployqt/shared/shared.cpp')
-rw-r--r-- | src/macdeployqt/shared/shared.cpp | 320 |
1 files changed, 276 insertions, 44 deletions
diff --git a/src/macdeployqt/shared/shared.cpp b/src/macdeployqt/shared/shared.cpp index 571a0cd8e..0f9f7c761 100644 --- a/src/macdeployqt/shared/shared.cpp +++ b/src/macdeployqt/shared/shared.cpp @@ -38,6 +38,7 @@ #include <QDir> #include <QRegExp> #include <QSet> +#include <QStack> #include <QDirIterator> #include <QLibraryInfo> #include <QJsonDocument> @@ -52,6 +53,8 @@ bool runStripEnabled = true; bool alwaysOwerwriteEnabled = false; +bool runCodesign = false; +QString codesignIdentiy; int logLevel = 1; using std::cout; @@ -74,7 +77,8 @@ QDebug operator<<(QDebug debug, const FrameworkInfo &info) debug << "Install name" << info.installName << "\n"; debug << "Deployed install name" << info.deployedInstallName << "\n"; debug << "Source file Path" << info.sourceFilePath << "\n"; - debug << "Destination Directory (relative to bundle)" << info.destinationDirectory << "\n"; + debug << "Framework Destination Directory (relative to bundle)" << info.frameworkDestinationDirectory << "\n"; + debug << "Binary Destination Directory (relative to bundle)" << info.binaryDestinationDirectory << "\n"; return debug; } @@ -126,6 +130,38 @@ bool copyFilePrintStatus(const QString &from, const QString &to) } } +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); +} + FrameworkInfo parseOtoolLibraryLine(const QString &line, bool useDebugLibs) { FrameworkInfo info; @@ -185,19 +221,22 @@ FrameworkInfo parseOtoolLibraryLine(const QString &line, bool useDebugLibs) // remove ".framework" name = currentPart; name.chop(QString(".framework").length()); + info.isDylib = false; info.frameworkName = currentPart; state = Version; ++part; continue; } if (state == DylibName) { name = currentPart.split(" (compatibility").at(0); + info.isDylib = true; info.frameworkName = name; info.binaryName = name.left(name.indexOf('.')) + suffix + name.mid(name.indexOf('.')); info.installName += name; info.deployedInstallName = "@executable_path/../Frameworks/" + info.binaryName; info.frameworkPath = info.frameworkDirectory + info.binaryName; info.sourceFilePath = info.frameworkPath; - info.destinationDirectory = bundleFrameworkDirectory + "/"; + info.frameworkDestinationDirectory = bundleFrameworkDirectory + "/"; + info.binaryDestinationDirectory = info.frameworkDestinationDirectory; info.binaryDirectory = info.frameworkDirectory; info.binaryPath = info.frameworkPath; state = End; @@ -212,7 +251,8 @@ FrameworkInfo parseOtoolLibraryLine(const QString &line, bool useDebugLibs) info.deployedInstallName = "@executable_path/../Frameworks/" + info.frameworkName + info.binaryPath; info.frameworkPath = info.frameworkDirectory + info.frameworkName; info.sourceFilePath = info.frameworkPath + info.binaryPath; - info.destinationDirectory = bundleFrameworkDirectory + "/" + info.frameworkName + "/" + info.binaryDirectory; + info.frameworkDestinationDirectory = bundleFrameworkDirectory + "/" + info.frameworkName; + info.binaryDestinationDirectory = info.frameworkDestinationDirectory + "/" + info.binaryDirectory; state = End; } else if (state == End) { break; @@ -272,6 +312,22 @@ QStringList findAppLibraries(const QString &appBundlePath) return result; } +QStringList findAppBundleFiles(const QString &appBundlePath) +{ + QStringList result; + + QDirIterator iter(appBundlePath, QStringList() << QString::fromLatin1("*"), + QDir::Files, QDirIterator::Subdirectories); + + while (iter.hasNext()) { + iter.next(); + if (iter.fileInfo().isSymLink()) + continue; + result << iter.fileInfo().filePath(); + } + + return result; +} QList<FrameworkInfo> getQtFrameworks(const QStringList &otoolLines, bool useDebugLibs) { @@ -285,7 +341,7 @@ QList<FrameworkInfo> getQtFrameworks(const QStringList &otoolLines, bool useDebu } } return libraries; -} +}; QList<FrameworkInfo> getQtFrameworks(const QString &path, bool useDebugLibs) { @@ -323,6 +379,35 @@ QList<FrameworkInfo> getQtFrameworksForPaths(const QStringList &paths, bool useD return result; } +QStringList getBinaryDependencies(const QString executablePath, const QString &path) +{ + QStringList binaries; + + QProcess otool; + otool.start("otool", QStringList() << "-L" << path); + otool.waitForFinished(); + + if (otool.exitCode() != 0) { + LogError() << otool.readAllStandardError(); + } + + QString output = otool.readAllStandardOutput(); + QStringList outputLines = output.split("\n"); + outputLines.removeFirst(); // remove line containing the binary path + + // return bundle-local dependencies. (those starting with @executable_path) + foreach (const QString &line, outputLines) { + QString trimmedLine = line.mid(0, line.indexOf("(")).trimmed(); // remove "(compatibility version ...)" and whitespace + if (trimmedLine.startsWith("@executable_path/")) { + QString binary = QDir::cleanPath(executablePath + trimmedLine.mid(QStringLiteral("@executable_path/").length())); + if (binary != path) + binaries.append(binary); + } + } + + return binaries; +} + // copies everything _inside_ sourcePath to destinationPath void recursiveCopy(const QString &sourcePath, const QString &destinationPath) { @@ -352,12 +437,37 @@ void recursiveCopyAndDeploy(const QString &appBundlePath, const QString &sourceP QStringList files = QDir(sourcePath).entryList(QStringList() << QStringLiteral("*"), QDir::Files | QDir::NoDotAndDotDot); foreach (QString file, files) { const QString fileSourcePath = sourcePath + QLatin1Char('/') + file; - const QString fileDestinationPath = destinationPath + QLatin1Char('/') + file; if (file.endsWith("_debug.dylib")) { continue; // Skip debug versions } else if (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("/")); + 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; @@ -365,6 +475,7 @@ void recursiveCopyAndDeploy(const QString &appBundlePath, const QString &sourceP deployQtFrameworks(frameworks, appBundlePath, QStringList(fileDestinationPath), useDebugLibs, useLoaderPath); } } else { + QString fileDestinationPath = destinationPath + QLatin1Char('/') + file; copyFilePrintStatus(fileSourcePath, fileDestinationPath); } } @@ -375,56 +486,87 @@ void recursiveCopyAndDeploy(const QString &appBundlePath, const QString &sourceP } } - -QString copyFramework(const FrameworkInfo &framework, const QString path) +QString copyDylib(const FrameworkInfo &framework, const QString path) { - QString from = framework.sourceFilePath; - - if (!QFile::exists(from)) { - LogError() << "no file at" << from; + if (!QFile::exists(framework.sourceFilePath)) { + LogError() << "no file at" << framework.sourceFilePath; return QString(); } - QFileInfo fromDirInfo(framework.frameworkPath + QLatin1Char('/') - + framework.binaryDirectory); - bool fromDirIsSymLink = fromDirInfo.isSymLink(); - QString unresolvedToDir = path + QLatin1Char('/') + framework.destinationDirectory; - QString resolvedToDir; - QString relativeLinkTarget; // will contain the link from Current to e.g. 4 in the Versions directory - if (fromDirIsSymLink) { - // handle the case where framework is referenced with Versions/Current - // which is a symbolic link, so copy to target and recreate as symbolic link - relativeLinkTarget = QDir(fromDirInfo.canonicalPath()) - .relativeFilePath(QFileInfo(fromDirInfo.symLinkTarget()).canonicalFilePath()); - resolvedToDir = QFileInfo(unresolvedToDir).path() + QLatin1Char('/') + relativeLinkTarget; - } else { - resolvedToDir = unresolvedToDir; + // 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(); } - QString to = resolvedToDir + "/" + framework.binaryName; + // Retrun if the dylib has aleardy been deployed + if (QFileInfo(dylibDestinationBinaryPath).exists() && !alwaysOwerwriteEnabled) + return dylibDestinationBinaryPath; + + // Copy dylib binary + copyFilePrintStatus(framework.sourceFilePath, dylibDestinationBinaryPath); + return dylibDestinationBinaryPath; +} - // create the (non-symlink) dir - QDir dir; - if (!dir.mkpath(resolvedToDir)) { - LogError() << "could not create destination directory" << to; +QString copyFramework(const FrameworkInfo &framework, const QString path) +{ + if (!QFile::exists(framework.sourceFilePath)) { + LogError() << "no file at" << framework.sourceFilePath; return QString(); } - if (!QFile::exists(to) || alwaysOwerwriteEnabled) { // copy the binary and resources if that wasn't done before - copyFilePrintStatus(from, to); + // 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; - const QString resourcesSourcePath = framework.frameworkPath + "/Resources"; - const QString resourcesDestianationPath = path + "/Contents/Frameworks/" + framework.frameworkName + "/Resources"; - recursiveCopy(resourcesSourcePath, resourcesDestianationPath); + // 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(); } - // create the Versions/Current symlink dir if necessary - if (fromDirIsSymLink) { - QFile::link(relativeLinkTarget, unresolvedToDir); - LogNormal() << " linked:" << unresolvedToDir; - LogNormal() << " to" << resolvedToDir << "(" << relativeLinkTarget << ")"; + // 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/ + const QString resourcesSourcePath = framework.frameworkPath + "/Resources"; + const QString resourcesDestianationPath = frameworkDestinationDirectory + "/Versions/" + framework.version + "/Resources"; + recursiveCopy(resourcesSourcePath, resourcesDestianationPath); + + // 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"); + 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 to; + return frameworkDestinationBinaryPath; } void runInstallNameTool(QStringList options) @@ -453,7 +595,7 @@ void changeInstallName(const QString &bundlePath, const FrameworkInfo &framework QString deployedInstallName; if (useLoaderPath) { deployedInstallName = QLatin1String("@loader_path/") - + QFileInfo(binary).absoluteDir().relativeFilePath(absBundlePath + QLatin1Char('/') + framework.destinationDirectory + QLatin1Char('/') + framework.binaryName); + + QFileInfo(binary).absoluteDir().relativeFilePath(absBundlePath + QLatin1Char('/') + framework.binaryDestinationDirectory + QLatin1Char('/') + framework.binaryName); } else { deployedInstallName = framework.deployedInstallName; } @@ -523,8 +665,9 @@ DeploymentInfo deployQtFrameworks(QList<FrameworkInfo> frameworks, // Install_name_tool the new id into the binaries changeInstallName(bundlePath, framework, binaryPaths, useLoaderPath); - // Copy farmework to app bundle. - const QString deployedBinaryPath = copyFramework(framework, bundlePath); + // Copy the framework/dylib to the app bundle. + const QString deployedBinaryPath = framework.isDylib ? copyDylib(framework, bundlePath) + : copyFramework(framework, bundlePath); // Skip the rest if already was deployed. if (deployedBinaryPath.isNull()) continue; @@ -533,6 +676,8 @@ DeploymentInfo deployQtFrameworks(QList<FrameworkInfo> frameworks, // Install_name_tool it a new id. changeIdentification(framework.deployedInstallName, deployedBinaryPath); + + // Check for framework dependencies QList<FrameworkInfo> dependencies = getQtFrameworks(deployedBinaryPath, useDebugLibs); @@ -645,8 +790,10 @@ void deployPlugins(const ApplicationBundleInfo &appBundleInfo, const QString &pl if (copyFilePrintStatus(sourcePath, destinationPath)) { runStrip(destinationPath); + QList<FrameworkInfo> frameworks = getQtFrameworks(destinationPath, useDebugLibs); deployQtFrameworks(frameworks, appBundleInfo.path, QStringList() << destinationPath, useDebugLibs, deploymentInfo.useLoaderPath); + } } } @@ -695,6 +842,12 @@ void deployPlugins(const QString &appBundlePath, DeploymentInfo deploymentInfo, void deployQmlImport(const QString &appBundlePath, 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, importSourcePath, importDestinationPath); } @@ -824,6 +977,85 @@ void changeQtFrameworks(const QString appPath, const QString &qtPath, bool useDe } } +void codesignFile(const QString &identity, const QString &filePath) +{ + if (!runCodesign) + return; + + LogNormal() << "codesign" << filePath; + + QProcess codesign; + codesign.start("codesign", QStringList() << "--preserve-metadata=identifier,entitlements,resource-rules" + << "--force" << "-s" << identity << filePath); + codesign.waitForFinished(-1); + + QByteArray err = codesign.readAllStandardError(); + if (codesign.exitCode() > 0) { + LogError() << "Codesign signing error:"; + LogError() << err; + } else if (!err.isEmpty()) { + LogDebug() << err; + } +} + +void codesign(const QString &identity, const QString &appBundlePath) +{ + // 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> signedBinaries; + + // Create the root code-binary set. This set consists of the application + // executable(s) and the plugins. + QString rootBinariesPath = appBundlePath + "/Contents/MacOS/"; + QStringList foundRootBinaries = QDir(rootBinariesPath).entryList(QStringList() << "*", QDir::Files); + foreach (const QString &binary, foundRootBinaries) + pendingBinaries.push(rootBinariesPath + binary); + + QStringList foundPluginBinaries = findAppBundleFiles(appBundlePath + "/Contents/PlugIns/"); + foreach (const QString &binary, foundPluginBinaries) + pendingBinaries.push(binary); + + + // Sign all binares; 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).toSet().subtract(signedBinaries).toList(); + if (!dependencies.isEmpty()) { + pendingBinaries.push(binary); + foreach (const QString &dependency, dependencies) + pendingBinaries.push(dependency); + continue; + } + // All dependencies are signed, now sign this binary + codesignFile(identity, binary); + signedBinaries.insert(binary); + } + + // 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; + } +} void createDiskImage(const QString &appBundlePath) { |