diff --git a/src/macdeployqt/macdeployqt/main.cpp b/src/macdeployqt/macdeployqt/main.cpp index b4881789e20e5084e009888a981595610f0b3e72..313a57488b308b7f72283d3d6ec80c33402b1837 100644 --- a/src/macdeployqt/macdeployqt/main.cpp +++ b/src/macdeployqt/macdeployqt/main.cpp @@ -59,6 +59,7 @@ int main(int argc, char **argv) qDebug() << " -executable=<path> : Let the given executable use the deployed frameworks too"; qDebug() << " -qmldir=<path> : Deploy imports used by .qml files in the given path"; qDebug() << " -always-overwrite : Copy files enven if the target file exists"; + qDebug() << " -codesign=<ident> : Run codesing with the given identity on all executables"; qDebug() << ""; qDebug() << "macdeployqt takes an application bundle as input and makes it"; qDebug() << "self-contained by copying in the Qt frameworks and plugins that"; @@ -89,6 +90,8 @@ int main(int argc, char **argv) extern bool alwaysOwerwriteEnabled; QStringList additionalExecutables; QStringList qmlDirs; + extern bool runCodesign; + extern QString codesignIdentiy; for (int i = 2; i < argc; ++i) { QByteArray argument = QByteArray(argv[i]); @@ -131,6 +134,15 @@ int main(int argc, char **argv) } 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("-")) { LogError() << "Unknown argument" << argument << "\n"; return 0; @@ -157,6 +169,9 @@ int main(int argc, char **argv) if (!qmlDirs.isEmpty()) deployQmlImports(appBundlePath, qmlDirs); + if (runCodesign) + codesign(codesignIdentiy, appBundlePath); + if (dmg) { LogNormal(); createDiskImage(appBundlePath); diff --git a/src/macdeployqt/shared/shared.cpp b/src/macdeployqt/shared/shared.cpp index 903d5b2c5b782fc3b7110922dd8124ed656949ef..faf46360f9df21ab698003b019a6773645a27bd1 100644 --- a/src/macdeployqt/shared/shared.cpp +++ b/src/macdeployqt/shared/shared.cpp @@ -46,6 +46,7 @@ #include <QDir> #include <QRegExp> #include <QSet> +#include <QStack> #include <QDirIterator> #include <QLibraryInfo> #include <QJsonDocument> @@ -56,6 +57,8 @@ bool runStripEnabled = true; bool alwaysOwerwriteEnabled = false; +bool runCodesign = false; +QString codesignIdentiy; int logLevel = 1; using std::cout; @@ -276,6 +279,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) { @@ -289,7 +308,7 @@ QList<FrameworkInfo> getQtFrameworks(const QStringList &otoolLines, bool useDebu } } return libraries; -} +}; QList<FrameworkInfo> getQtFrameworks(const QString &path, bool useDebugLibs) { @@ -327,6 +346,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) { @@ -561,6 +609,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); @@ -679,8 +729,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); + } } } @@ -836,6 +888,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) { diff --git a/src/macdeployqt/shared/shared.h b/src/macdeployqt/shared/shared.h index 0fe7400fb64860c99236a12bc743907c66747b64..257c3eb341a84e0b1ed1ffbf6e374cfa2c92cb60 100644 --- a/src/macdeployqt/shared/shared.h +++ b/src/macdeployqt/shared/shared.h @@ -111,6 +111,8 @@ void changeIdentification(const QString &id, const QString &binaryPath); void changeInstallName(const QString &oldName, const QString &newName, const QString &binaryPath); void runStrip(const QString &binaryPath); QString findAppBinary(const QString &appBundlePath); +void codesignFile(const QString &identity, const QString &filePath); +void codesign(const QString &identity, const QString &appBundlePath); void createDiskImage(const QString &appBundlePath);