From 2bc823ec00cec8a1d58981710eb50ba85b4f58d7 Mon Sep 17 00:00:00 2001 From: Jake Petroules Date: Fri, 22 Apr 2016 01:20:24 -0700 Subject: Implement codesign module This moves code signing functionality into a dedicated module, and also implements automatic provisioning for Apple platforms, which automatically selects appropriate signing identities and provisioning profiles based on the product being built. This also results in a significant performance improvement since all code signing setup information is retrieved in process instead of forking off the openssl and security command line tools. Task-number: QBS-899 Change-Id: I60d0aeaeb2d1004929505bcb1e0bc77512fe77bc Reviewed-by: Christian Kandeler --- doc/reference/modules/codesign-module.qdoc | 246 +++++++++++++ examples/cocoa-application/app.qbs | 9 +- share/qbs/imports/qbs/DarwinTools/darwin-tools.js | 16 - share/qbs/imports/qbs/Probes/XcodeProbe.qbs | 3 + share/qbs/modules/bundle/BundleModule.qbs | 71 ++-- share/qbs/modules/codesign/CodeSignModule.qbs | 45 +++ share/qbs/modules/codesign/apple.qbs | 385 +++++++++++++++++++++ share/qbs/modules/codesign/codesign.js | 279 +++++++++++++++ share/qbs/modules/codesign/noop.qbs | 37 ++ share/qbs/modules/cpp/GenericGCC.qbs | 20 +- share/qbs/modules/cpp/gcc.js | 25 +- share/qbs/modules/xcode/xcode.js | 21 ++ share/qbs/modules/xcode/xcode.qbs | 272 +-------------- tests/auto/api/tst_api.cpp | 4 +- .../auto/blackbox/testdata-apple/codesign/app.cpp | 1 + .../blackbox/testdata-apple/codesign/codesign.qbs | 38 ++ tests/auto/blackbox/tst_blackbox.cpp | 4 +- tests/auto/blackbox/tst_blackboxapple.cpp | 99 ++++++ tests/auto/blackbox/tst_blackboxapple.h | 2 + tests/auto/blackbox/tst_blackboxbase.h | 9 +- 20 files changed, 1233 insertions(+), 353 deletions(-) create mode 100644 doc/reference/modules/codesign-module.qdoc create mode 100644 share/qbs/modules/codesign/CodeSignModule.qbs create mode 100644 share/qbs/modules/codesign/apple.qbs create mode 100644 share/qbs/modules/codesign/codesign.js create mode 100644 share/qbs/modules/codesign/noop.qbs create mode 100644 tests/auto/blackbox/testdata-apple/codesign/app.cpp create mode 100644 tests/auto/blackbox/testdata-apple/codesign/codesign.qbs diff --git a/doc/reference/modules/codesign-module.qdoc b/doc/reference/modules/codesign-module.qdoc new file mode 100644 index 000000000..ab95798a7 --- /dev/null +++ b/doc/reference/modules/codesign-module.qdoc @@ -0,0 +1,246 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Copyright (C) 2021 Ivan Komissarov (abbapoh@gmail.com) +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:FDL$ +** 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 Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \qmltype codesign + \inqmlmodule QbsModules + \since Qbs 1.19 + + \brief Provides code signing support. + + The \c codesign module contains properties and rules for code signing on Apple platforms. + + \section2 Relevant File Tags + + \table + \header + \li Tag + \li Auto-tagged File Names + \li Since + \li Description + \row + \li \c{"codesign.entitlements"} + \li \c{*.entitlements} + \li 1.19.0 + \li \l{https://developer.apple.com/documentation/bundleresources/entitlements}{Xcode entitlements} + \row + \li \c{"codesign.provisioningprofile"} + \li \c{*.mobileprovision, *.provisionprofile} + \li 1.19.0 + \li Xcode provisioning profiles + \row + \li \c{"codesign.signed_artifact"} + \li n/a + \li 1.19.0 + \li This tag is attached to all signed artifacts such as applications or libraries + \endtable +*/ + +/*! + \qmlproperty string codesign::codesignFlags + + Additional flags passed to the \c{codesign} tool. + + \since Qbs 1.19 + + \defaultvalue \c{undefined} + + \appleproperty +*/ + +/*! + \qmlproperty string codesign::codesignName + + The name of the \c{codesign} binary. + + \since Qbs 1.19 + + \defaultvalue \c{"codesign"} + + \appleproperty +*/ + +/*! + \qmlproperty string codesign::codesignPath + + Path to the \c{codesign} tool. + + \since Qbs 1.19 + + \defaultvalue Determined automatically + + \appleproperty +*/ + +/*! + \qmlproperty bool codesign::enableCodeSigning + + Whether to actually perform code signing. + + \since Qbs 1.19 + \defaultvalue \c false +*/ + +/*! + \qmlproperty string codesign::provisioningProfile + + Name or UUID of the provisioning profile to embed in the product. + Typically this should be left blank to allow \QBS to use automatic provisioning. + + \since Qbs 1.19 + + \defaultvalue \c undefined + + \appleproperty +*/ + +/*! + \qmlproperty path codesign::provisioningProfilesPath + + Path to directory containing provisioning profiles installed on the system. + This should not normally need to be changed. + + \since Qbs 1.19 + + \defaultvalue \c{"~/Library/MobileDevice/Provisioning Profiles"} + + \appleproperty +*/ + +/*! + \qmlproperty string codesign::signingIdentity + + Search string used to find the certificate to sign the product. This does not have to be + a full certificate name like "Mac Developer: John Doe (XXXXXXXXXX)", and can instead be + a partial string like "Mac Developer" or the certificate's SHA1 fingerprint. + The search string should generally be one of the following: + \list + \li 3rd Party Mac Developer Application + \li 3rd Party Mac Developer Installer + \li Developer ID Application + \li Developer ID Installer + \li iPhone Developer + \li iPhone Distribution + \li Mac Developer + \endlist + + It is also possible to use the special \c "-" value to use the ad-hoc signing. + + See \l{https://developer.apple.com/library/mac/documentation/IDEs/Conceptual/AppDistributionGuide/MaintainingCertificates/MaintainingCertificates.html#//apple_ref/doc/uid/TP40012582-CH31-SW41}{Maintaining Your Signing Identities and Certificates} + for complete documentation on the existing certificate types. + In general you should use \l{codesign::signingType}{signingType} instead. + + \since Qbs 1.19 + + \defaultvalue Determined by \l{codesign::signingType}{signingType} + + \appleproperty +*/ + +/*! + \qmlproperty string codesign::signingTimestamp + + URL of the timestamp authority server to be contacted to authenticate code signing. + \c{undefined} indicates that a system-specific default should be used, and the empty + string indicates the default server provided by Apple. \c{"none"} explicitly disables + the use of timestamp services and this should not usually need to be changed. + + \since Qbs 1.19 + + \defaultvalue \c{"none"} + + \appleproperty +*/ + +/*! + \qmlproperty string codesign::signingType + + Type of code signing to use. This should generally be used in preference to an explicit + signing identity like "Mac Developer: John Doe (XXXXXXXXXX)" since it is not user + specific and can be set in a project file. + Possible values include: \c{"app-store"}, \c{"apple-id"}, \c{"ad-hoc"}, which sign for + the App Store or Mac App Store, Developer ID, and Ad-hoc code signing, respectively. + + \section1 Relation between the signingType and signingIdentity + + The following table shows how the signingIdentity's default value is calculated. + + \table + \header + \li \c qbs.targetOS + \li \c codesign.signingType + \li \c qbs.buildVariant + \li \c codesign.signingIdentity + \row + \li {1, 4} \c "macos" + \li \c "ad-hoc" + \li any + \li \c "-" + \row + \li {1, 2} \c "app-store" + \li \c "debug", \c "profiling" + \li \c "Mac Developer" + \row + \li \c "release" + \li \c "3rd Party Mac Developer Application" + \row + \li \c "apple-id" + \li any + \li \c "Developer ID Application" + \row + \li {1, 2} \c "ios", \c "tvos", \c "watchos" + \li {1, 2} \c "app-store" + \li \c "debug", \c "profiling" + \li \c "iPhone Developer" + \row + \li \c "release" + \li \c "iPhone Distribution" + \endtable + + \since Qbs 1.19 + + \defaultvalue Determined automatically + + \appleproperty +*/ + +/*! + \qmlproperty string codesign::teamIdentifier + + Human readable name or 10-digit identifier of the Apple development team that the + signing identity belongs to. This is used to disambiguate between multiple certificates + of the same type in different teams. Typically this can be left blank if the development + machine is only signed in to a single development team, and should be set in a profile + otherwise. + + \since Qbs 1.19 + + \defaultvalue \c undefined + + \appleproperty +*/ diff --git a/examples/cocoa-application/app.qbs b/examples/cocoa-application/app.qbs index f51f94e8b..67558e8a5 100644 --- a/examples/cocoa-application/app.qbs +++ b/examples/cocoa-application/app.qbs @@ -48,7 +48,7 @@ ** ****************************************************************************/ -import qbs +import qbs.Utilities CppApplication { Depends { condition: product.condition; name: "ib" } @@ -100,4 +100,11 @@ CppApplication { } ib.appIconName: "AppIcon" + + Properties { + // codesign module only present starting from 1.19 + condition: Utilities.versionCompare(qbs.version, "1.19") >= 0 + codesign.enableCodeSigning: true + codesign.signingType: "ad-hoc" + } } diff --git a/share/qbs/imports/qbs/DarwinTools/darwin-tools.js b/share/qbs/imports/qbs/DarwinTools/darwin-tools.js index 9b81310f0..0a944802d 100644 --- a/share/qbs/imports/qbs/DarwinTools/darwin-tools.js +++ b/share/qbs/imports/qbs/DarwinTools/darwin-tools.js @@ -260,19 +260,3 @@ function cleanPropertyList(plist) { cleanPropertyList(plist[key]); } } - -function _codeSignTimestampFlags(product) { - // If signingTimestamp is undefined, do not specify the flag at all - - // this uses the system-specific default behavior - var signingTimestamp = product.moduleProperty("xcode", "signingTimestamp"); - if (signingTimestamp !== undefined) { - // If signingTimestamp is an empty string, specify the flag but do - // not specify a value - this uses a default Apple-provided server - var flag = "--timestamp"; - if (signingTimestamp) - flag += "=" + signingTimestamp; - return [flag]; - } - - return []; -} diff --git a/share/qbs/imports/qbs/Probes/XcodeProbe.qbs b/share/qbs/imports/qbs/Probes/XcodeProbe.qbs index e0ed99346..edd67433b 100644 --- a/share/qbs/imports/qbs/Probes/XcodeProbe.qbs +++ b/share/qbs/imports/qbs/Probes/XcodeProbe.qbs @@ -48,6 +48,7 @@ Probe { // Outputs property var architectureSettings property var availableSdks + property var platformSettings property string xcodeVersion configure: { @@ -97,6 +98,8 @@ Probe { }); availableSdks = Xcode.sdkInfoList(sdksPath); + var platformInfoPlist = FileInfo.joinPaths(platformPath, "Info.plist"); + platformSettings = Xcode.platformInfo(platformInfoPlist); found = !!xcodeVersion; } } diff --git a/share/qbs/modules/bundle/BundleModule.qbs b/share/qbs/modules/bundle/BundleModule.qbs index 05e77c81b..63f12db07 100644 --- a/share/qbs/modules/bundle/BundleModule.qbs +++ b/share/qbs/modules/bundle/BundleModule.qbs @@ -38,9 +38,11 @@ import qbs.PropertyList import qbs.TextFile import qbs.Utilities import "bundle.js" as Bundle +import "../codesign/codesign.js" as Codesign Module { Depends { name: "xcode"; required: false; } + Depends { name: "codesign"; required: false; } Probe { id: bundleSettingsProbe @@ -510,9 +512,9 @@ Module { multiplex: true inputs: ["bundle.input", "aggregate_infoplist", "pkginfo", "hpp", - "icns", "xcent", + "icns", "codesign.xcent", "compiled_ibdoc", "compiled_assetcatalog", - "xcode.provisioningprofile.main"] + "codesign.embedded_provisioningprofile"] // Make sure the inputs of this rule are only those rules which produce outputs compatible // with the type of the bundle being produced. @@ -540,13 +542,13 @@ Module { }); } - for (i in inputs["xcode.provisioningprofile.main"]) { - var ext = inputs["xcode.provisioningprofile.main"][i].fileName.split('.')[1]; + var provprofiles = inputs["codesign.embedded_provisioningprofile"]; + for (i in provprofiles) { artifacts.push({ filePath: FileInfo.joinPaths(product.destinationDirectory, ModUtils.moduleProperty(product, "contentsFolderPath"), - "embedded." + ext), + provprofiles[i].fileName), fileTags: ["bundle.provisioningprofile", "bundle.content"] }); } @@ -613,8 +615,8 @@ Module { for (var i = 0; i < artifacts.length; ++i) artifacts[i].bundle = { wrapperPath: wrapperPath }; - if (product.qbs.hostOS.contains("darwin") && product.xcode - && product.xcode.signingIdentity) { + if (product.qbs.hostOS.contains("darwin") && product.codesign + && product.codesign.enableCodeSigning) { artifacts.push({ filePath: FileInfo.joinPaths(product.bundle.contentsFolderPath, "_CodeSignature/CodeResources"), fileTags: ["bundle.code-signature", "bundle.content"] @@ -706,18 +708,21 @@ Module { commands.push(cmd); } - var provisioningProfiles = outputs["bundle.provisioningprofile"]; - for (i in provisioningProfiles) { - cmd = new JavaScriptCommand(); - cmd.description = "copying provisioning profile"; - cmd.highlight = "filegen"; - cmd.source = inputs["xcode.provisioningprofile.main"][i].filePath; - cmd.destination = provisioningProfiles[i].filePath; - cmd.sourceCode = function() { - File.copy(source, destination); - }; + cmd = new JavaScriptCommand(); + cmd.description = "copying provisioning profile"; + cmd.highlight = "filegen"; + cmd.sources = (inputs["codesign.embedded_provisioningprofile"] || []) + .map(function(artifact) { return artifact.filePath; }); + cmd.destination = (outputs["bundle.provisioningprofile"] || []) + .map(function(artifact) { return artifact.filePath; }); + cmd.sourceCode = function() { + var i; + for (var i in sources) { + File.copy(sources[i], destination[i]); + } + }; + if (cmd.sources && cmd.sources.length) commands.push(cmd); - } cmd = new JavaScriptCommand(); cmd.description = "copying public headers"; @@ -762,34 +767,8 @@ Module { commands.push(cmd); if (product.moduleProperty("qbs", "hostOS").contains("darwin")) { - var actualSigningIdentity = product.moduleProperty("xcode", "actualSigningIdentity"); - var codesignDisplayName = product.moduleProperty("xcode", "actualSigningIdentityDisplayName"); - if (actualSigningIdentity) { - var args = product.moduleProperty("xcode", "codesignFlags") || []; - args.push("--force"); - args.push("--sign", actualSigningIdentity); - args = args.concat(DarwinTools._codeSignTimestampFlags(product)); - - for (var j in inputs.xcent) { - args.push("--entitlements", inputs.xcent[j].filePath); - break; // there should only be one - } - - // If this is a framework, we need to sign its versioned directory - if (bundleType === "framework") { - args.push(product.bundle.contentsFolderPath); - } else { - args.push(product.bundle.bundleName); - } - - cmd = new Command(product.moduleProperty("xcode", "codesignPath"), args); - cmd.workingDirectory = product.destinationDirectory; - cmd.description = "codesign " - + ModUtils.moduleProperty(product, "bundleName") - + " using " + codesignDisplayName - + " (" + actualSigningIdentity + ")"; - commands.push(cmd); - } + Array.prototype.push.apply(commands, Codesign.prepareSign( + project, product, inputs, outputs, input, output)); if (bundleType === "application" && product.moduleProperty("qbs", "targetOS").contains("macos")) { diff --git a/share/qbs/modules/codesign/CodeSignModule.qbs b/share/qbs/modules/codesign/CodeSignModule.qbs new file mode 100644 index 000000000..caa9e2e60 --- /dev/null +++ b/share/qbs/modules/codesign/CodeSignModule.qbs @@ -0,0 +1,45 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Copyright (C) 2021 Ivan Komissarov (abbapoh@gmail.com) +** Contact: http://www.qt.io/licensing +** +** This file is part of Qbs. +** +** 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 http://www.qt.io/terms-conditions. For further information +** use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +import qbs +import qbs.File +import qbs.FileInfo +import "codesign.js" as CodeSign + +Module { + condition: false + + property bool enableCodeSigning: false + + property string codesignName + property string codesignPath: codesignName + property stringList codesignFlags +} diff --git a/share/qbs/modules/codesign/apple.qbs b/share/qbs/modules/codesign/apple.qbs new file mode 100644 index 000000000..dbcd0a215 --- /dev/null +++ b/share/qbs/modules/codesign/apple.qbs @@ -0,0 +1,385 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Copyright (C) 2021 Ivan Komissarov (abbapoh@gmail.com) +** Contact: http://www.qt.io/licensing +** +** This file is part of Qbs. +** +** 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 http://www.qt.io/terms-conditions. For further information +** use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +import qbs +import qbs.BundleTools +import qbs.DarwinTools +import qbs.Environment +import qbs.File +import qbs.FileInfo +import qbs.ModUtils +import qbs.PropertyList +import qbs.Probes +import qbs.Utilities +import "codesign.js" as CodeSign +import "../xcode/xcode.js" as XcodeUtils + +CodeSignModule { + Depends { name: "xcode"; required: qbs.toolchain && qbs.toolchain.contains("xcode") } + + Probes.BinaryProbe { + id: codesignProbe + names: [codesignName] + } + + condition: qbs.hostOS.contains("macos") && qbs.targetOS.contains("darwin") + priority: 0 + + enableCodeSigning: _codeSigningRequired + + codesignName: "codesign" + codesignPath: codesignProbe.filePath + + property string signingType: { + if (_adHocCodeSigningAllowed) + return "ad-hoc"; + if (_codeSigningAllowed) + return "app-store"; + } + + PropertyOptions { + name: "signingType" + allowedValues: ["app-store", "apple-id", "ad-hoc"] + } + + property string signingIdentity: { + if (signingType === "ad-hoc") // only useful on macOS + return "-"; + + var isDebug = qbs.buildVariant !== "release"; + + if (qbs.targetOS.contains("ios") || qbs.targetOS.contains("tvos") + || qbs.targetOS.contains("watchos")) { + switch (signingType) { + case "app-store": + return isDebug ? "iPhone Developer" : "iPhone Distribution"; + } + } + + if (qbs.targetOS.contains("macos")) { + switch (signingType) { + case "app-store": + return isDebug ? "Mac Developer" : "3rd Party Mac Developer Application"; + case "apple-id": + return "Developer ID Application"; + } + } + } + + property string signingTimestamp: "none" + + property string provisioningProfile + PropertyOptions { + name: "provisioningProfile" + description: "Name or UUID of the provisioning profile to embed in the application; " + + "typically left blank to allow automatic provisioning" + } + + property string teamIdentifier + PropertyOptions { + name: "teamIdentifier" + description: "Name or identifier of the development team whose identities will be used; " + + "typically left blank unless signed into multiple development teams" + } + + property path provisioningProfilesPath: "~/Library/MobileDevice/Provisioning Profiles" + + readonly property var _actualSigningIdentity: { + if (signingIdentity === "-") { + return { + SHA1: signingIdentity, + subjectInfo: { CN: "ad hoc" } + } + } + + var identities = CodeSign.findSigningIdentities(signingIdentity, teamIdentifier); + if (identities && Object.keys(identities).length > 1) { + throw "Multiple codesigning identities (i.e. certificate and private key pairs) " + + "matching “" + signingIdentity + "” were found." + + CodeSign.humanReadableIdentitySummary(identities); + } + + for (var i in identities) + return identities[i]; + } + + // Allowed for macOS + readonly property bool _adHocCodeSigningAllowed: + XcodeUtils.boolFromSdkOrPlatform("AD_HOC_CODE_SIGNING_ALLOWED", + xcode._sdkProps, xcode._platformProps, true) + + // Allowed for all device platforms (not simulators) + readonly property bool _codeSigningAllowed: + XcodeUtils.boolFromSdkOrPlatform("CODE_SIGNING_ALLOWED", + xcode._sdkProps, xcode._platformProps, true) + + // Required for tvOS, iOS, and watchOS (not simulators) + property bool _codeSigningRequired: { + // allow to override value from Xcode so tests do not require signing + var envRequired = Environment.getEnv("QBS_AUTOTEST_CODE_SIGNING_REQUIRED"); + if (envRequired) + return envRequired === "1"; + return XcodeUtils.boolFromSdkOrPlatform("CODE_SIGNING_REQUIRED", + xcode._sdkProps, xcode._platformProps, false) + } + + // Required for tvOS, iOS, and watchOS (not simulators) + readonly property bool _entitlementsRequired: + XcodeUtils.boolFromSdkOrPlatform("ENTITLEMENTS_REQUIRED", + xcode._sdkProps, xcode._platformProps, false) + + readonly property bool _provisioningProfileAllowed: + product.bundle + && product.bundle.isBundle + && product.type.contains("application") + && xcode.platformType !== "simulator" + + // Required for tvOS, iOS, and watchOS (not simulators) + // PROVISIONING_PROFILE_REQUIRED is specified only in Embedded-Device.xcspec in the + // IDEiOSSupportCore IDE plugin, so we'll just write out the logic here manually + readonly property bool _provisioningProfileRequired: + _provisioningProfileAllowed && !qbs.targetOS.contains("macos") + + // Not used on simulator platforms either but provisioning profiles aren't used there anyways + readonly property string _provisioningProfilePlatform: { + if (qbs.targetOS.contains("macos")) + return "OSX"; + if (qbs.targetOS.contains("ios") || qbs.targetOS.contains("watchos")) + return "iOS"; + if (qbs.targetOS.contains("tvos")) + return "tvOS"; + } + + readonly property string _embeddedProfileName: + (xcode._platformProps || {})["EMBEDDED_PROFILE_NAME"] + + setupBuildEnvironment: { + var prefixes = product.xcode ? [ + product.xcode.platformPath + "/Developer", + product.xcode.toolchainPath, + product.xcode.developerPath + ] : []; + for (var i = 0; i < prefixes.length; ++i) { + var codesign_allocate = prefixes[i] + "/usr/bin/codesign_allocate"; + if (File.exists(codesign_allocate)) { + var v = new ModUtils.EnvironmentVariable("CODESIGN_ALLOCATE"); + v.value = codesign_allocate; + v.set(); + break; + } + } + } + + Group { + name: "Provisioning Profiles" + prefix: codesign.provisioningProfilesPath + "/" + files: ["*.mobileprovision", "*.provisionprofile"] + } + + FileTagger { + fileTags: ["codesign.entitlements"] + patterns: ["*.entitlements"] + } + + FileTagger { + fileTags: ["codesign.provisioningprofile"] + patterns: ["*.mobileprovision", "*.provisionprofile"] + } + + Rule { + multiplex: true + condition: product.codesign.enableCodeSigning && + product.codesign._provisioningProfileAllowed + inputs: ["codesign.provisioningprofile"] + + outputFileTags: ["codesign.embedded_provisioningprofile"] + outputArtifacts: { + var artifacts = []; + var provisioningProfiles = (inputs["codesign.provisioningprofile"] || []) + .map(function (a) { return a.filePath; }); + var bestProfile = CodeSign.findBestProvisioningProfile(product, provisioningProfiles); + var uuid = product.provisioningProfile; + if (bestProfile) { + artifacts.push({ + filePath: FileInfo.joinPaths(product.destinationDirectory, + product.codesign._embeddedProfileName), + fileTags: ["codesign.embedded_provisioningprofile"], + codesign: { + _provisioningProfileFilePath: bestProfile.filePath, + _provisioningProfileData: JSON.stringify(bestProfile.data), + } + }); + } else if (uuid) { + throw "Your build settings specify a provisioning profile with the UUID “" + + uuid + "”, however, no such provisioning profile was found."; + } else if (product._provisioningProfileRequired) { + var hasProfiles = !!((inputs["codesign.provisioningprofile"] || []).length); + var teamIdentifier = product.teamIdentifier; + var codeSignIdentity = product.signingIdentity; + if (hasProfiles) { + if (codeSignIdentity) { + console.warn("No provisioning profiles matching the bundle identifier “" + + product.bundle.identifier + + "” were found."); + } else { + console.warn("No provisioning profiles matching an applicable signing " + + "identity were found."); + } + } else { + if (codeSignIdentity) { + if (teamIdentifier) { + console.warn("No provisioning profiles with a valid signing identity " + + "(i.e. certificate and private key pair) matching the " + + "team ID “" + teamIdentifier + "” were found.") + } else { + console.warn("No provisioning profiles with a valid signing identity " + + "(i.e. certificate and private key pair) were found."); + } + } else { + console.warn("No non–expired provisioning profiles were found."); + } + } + } + return artifacts; + } + + prepare: { + var cmd = new JavaScriptCommand(); + var data = JSON.parse(output.codesign._provisioningProfileData); + cmd.source = output.codesign._provisioningProfileFilePath; + cmd.destination = output.filePath; + cmd.description = "using provisioning profile " + data.Name + " (" + data.UUID + ")"; + cmd.highlight = "filegen"; + cmd.sourceCode = function() { + File.copy(source, destination); + }; + return [cmd]; + } + } + + Rule { + multiplex: true + condition: product.codesign.enableCodeSigning + inputs: ["codesign.entitlements", "codesign.embedded_provisioningprofile"] + + Artifact { + filePath: FileInfo.joinPaths(product.destinationDirectory, + product.targetName + ".xcent") + fileTags: ["codesign.xcent"] + } + + prepare: { + var cmd = new JavaScriptCommand(); + cmd.description = "generating entitlements"; + cmd.highlight = "codegen"; + cmd.bundleIdentifier = product.bundle.identifier; + cmd.signingEntitlements = (inputs["codesign.entitlements"] || []) + .map(function (a) { return a.filePath; }); + cmd.provisioningProfiles = (inputs["codesign.embedded_provisioningprofile"] || []) + .map(function (a) { return a.filePath; }); + cmd.platformPath = product.xcode ? product.xcode.platformPath : undefined; + cmd.sdkPath = product.xcode ? product.xcode.sdkPath : undefined; + cmd.sourceCode = function() { + var i; + var provData = {}; + var provisionProfiles = inputs["codesign.embedded_provisioningprofile"]; + for (i in provisionProfiles) { + var plist = new PropertyList(); + try { + plist.readFromData(Utilities.smimeMessageContent( + provisionProfiles[i].filePath)); + provData = plist.toObject(); + } finally { + plist.clear(); + } + } + + var aggregateEntitlements = {}; + + // Start building up an aggregate entitlements plist from the files in the SDKs, + // which contain placeholders in the same manner as Info.plist + function entitlementsFileContents(path) { + return File.exists(path) ? BundleTools.infoPlistContents(path) : undefined; + } + var entitlementsSources = []; + if (platformPath) { + entitlementsSources.push( + entitlementsFileContents( + FileInfo.joinPaths(platformPath, "Entitlements.plist"))); + } + if (sdkPath) { + entitlementsSources.push( + entitlementsFileContents( + FileInfo.joinPaths(sdkPath, "Entitlements.plist"))); + } + + for (i = 0; i < signingEntitlements.length; ++i) { + entitlementsSources.push(entitlementsFileContents(signingEntitlements[i])); + } + + for (i = 0; i < entitlementsSources.length; ++i) { + var contents = entitlementsSources[i]; + for (var key in contents) { + if (contents.hasOwnProperty(key)) + aggregateEntitlements[key] = contents[key]; + } + } + + contents = provData["Entitlements"]; + for (key in contents) { + if (contents.hasOwnProperty(key) && !aggregateEntitlements.hasOwnProperty(key)) + aggregateEntitlements[key] = contents[key]; + } + + // Expand entitlements variables with data from the provisioning profile + var env = { + "AppIdentifierPrefix": (provData["ApplicationIdentifierPrefix"] || "") + ".", + "CFBundleIdentifier": bundleIdentifier + }; + DarwinTools.expandPlistEnvironmentVariables(aggregateEntitlements, env, true); + + // Anything with an undefined or otherwise empty value should be removed + // Only JSON-formatted plists can have null values, other formats error out + // This also follows Xcode behavior + DarwinTools.cleanPropertyList(aggregateEntitlements); + + var plist = new PropertyList(); + try { + plist.readFromObject(aggregateEntitlements); + plist.writeToFile(outputs["codesign.xcent"][0].filePath, "xml1"); + } finally { + plist.clear(); + } + }; + return [cmd]; + } + } +} diff --git a/share/qbs/modules/codesign/codesign.js b/share/qbs/modules/codesign/codesign.js new file mode 100644 index 000000000..1d039e7ca --- /dev/null +++ b/share/qbs/modules/codesign/codesign.js @@ -0,0 +1,279 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Copyright (C) 2021 Ivan Komissarov (abbapoh@gmail.com) +** Contact: http://www.qt.io/licensing +** +** This file is part of Qbs. +** +** 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 http://www.qt.io/terms-conditions. For further information +** use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +var File = require("qbs.File"); +var FileInfo = require("qbs.FileInfo"); +var PathTools = require("qbs.PathTools"); +var PropertyList = require("qbs.PropertyList"); +var Utilities = require("qbs.Utilities"); + +function findSigningIdentities(searchString, team) { + if (!searchString) + return {}; + var identities = Utilities.signingIdentities(); + var matchedIdentities = {}; + for (var key in identities) { + var identity = identities[key]; + if (team && ![identity.subjectInfo.O, identity.subjectInfo.OU].contains(team)) + continue; + if (searchString === key || identity.subjectInfo.CN.startsWith(searchString)) + matchedIdentities[key] = identity; + } + return matchedIdentities; +} + +function humanReadableIdentitySummary(identities) { + return "\n\t" + Object.keys(identities).map(function (key) { + return identities[key].subjectInfo.CN + + " in team " + + identities[key].subjectInfo.O + + " (" + identities[key].subjectInfo.OU + ")"; + }).join("\n\t"); +} + +/** + * Returns the best provisioning profile for code signing a binary with the given parameters. + * Ideally, this should behave identically as Xcode but the algorithm is not documented + * \l{https://developer.apple.com/library/ios/qa/qa1814/_index.html}{Automatic Provisioning} + */ +function findBestProvisioningProfile(product, files) { + var actualSigningIdentity = product.codesign._actualSigningIdentity || {}; + var teamIdentifier = product.codesign.teamIdentifier; + var bundleIdentifier = product.bundle.identifier; + var targetOS = product.qbs.targetOS; + var buildVariant = product.qbs.buildVariant; + var query = product.codesign.provisioningProfile; + var profilePlatform = product.codesign._provisioningProfilePlatform; + + // Read all provisioning profiles on disk into plist objects in memory + var profiles = files.map(function(filePath) { + var plist = new PropertyList(); + try { + plist.readFromData(Utilities.smimeMessageContent(filePath)); + return { + data: plist.toObject(), + filePath: filePath + }; + } finally { + plist.clear(); + } + }); + + // Do a simple search by matching UUID or Name + if (query) { + for (var i = 0; i < profiles.length; ++i) { + var obj = profiles[i]; + if (obj.data && (obj.data.UUID === query || obj.data.Name === query)) + return obj; + } + + // If we asked for a specific provisioning profile, don't select one automatically + return undefined; + } + + // Provisioning profiles are not normally used with ad-hoc code signing or non-apps + // We do these checks down here only for the automatic selection but not above because + // if the user explicitly selects a provisioning profile it should be used no matter what + if (actualSigningIdentity.SHA1 === "-" || !product.type.contains("application")) + return undefined; + + // Filter out any provisioning profiles we know to be unsuitable from the start + profiles = profiles.filter(function (profile) { + var data = profile.data; + + if (actualSigningIdentity.subjectInfo) { + var certCommonNames = (data["DeveloperCertificates"] || []).map(function (cert) { + return Utilities.certificateInfo(cert).subjectInfo.CN; + }); + if (!certCommonNames.contains(actualSigningIdentity.subjectInfo.CN)) { + console.log("Skipping provisioning profile with no matching certificate names for '" + + actualSigningIdentity.subjectInfo.CN + + "' (found " + certCommonNames.join(", ") + "): " + + profile.filePath); + return false; + } + } + + var platforms = data["Platform"] || []; + if (platforms.length > 0 && profilePlatform && !platforms.contains(profilePlatform)) { + console.log("Skipping provisioning profile for platform " + platforms.join(", ") + + " (current platform " + profilePlatform + ")" + + ": " + profile.filePath); + return false; + } + + if (teamIdentifier + && !data["TeamIdentifier"].contains(teamIdentifier) + && data["TeamName"] !== teamIdentifier) { + console.log("Skipping provisioning profile for team " + data["TeamIdentifier"] + + " (" + data["TeamName"] + ") (current team " + teamIdentifier + ")" + + ": " + profile.filePath); + return false; + } + + if (Date.parse(data["ExpirationDate"]) <= Date.now()) { + console.log("Skipping expired provisioning profile: " + profile.filePath); + return false; + } + + // Filter development vs distribution profiles; + // though the certificate common names check should have been sufficient + var isDebug = buildVariant === "debug"; + if (data["Entitlements"]["get-task-allow"] !== isDebug) { + console.log("Skipping provisioning profile for wrong debug mode: " + profile.filePath); + return false; + } + + var prefix = data["ApplicationIdentifierPrefix"]; + var fullAppId = data["Entitlements"]["application-identifier"]; + if ([prefix, bundleIdentifier].join(".") !== fullAppId + && [prefix, "*"].join(".") !== fullAppId) { + console.log("Skipping provisioning profile not matching full (" + + [prefix, bundleIdentifier].join(".") + ") or wildcard (" + + [prefix, "*"].join(".") + ") app ID (found " + fullAppId + "): " + + profile.filePath); + return false; + } + + return true; + }); + + // Sort by expiration date - sooner expiration dates come last + profiles.sort(function(profileA, profileB) { + var expA = Date.parse(profileA.data["ExpirationDate"]); + var expB = Date.parse(profileB.data["ExpirationDate"]); + if (expA < expB) + return -1; + if (expA > expB) + return 1; + return 0; + }); + + // Sort by application identifier - wildcard profiles come last + profiles.sort(function(profileA, profileB) { + var idA = profileA.data["Entitlements"]["application-identifier"]; + var idB = profileB.data["Entitlements"]["application-identifier"]; + if (!idA.endsWith(".*") && idB.endsWith(".*")) + return -1; + if (idA.endsWith(".*") && !idB.endsWith(".*")) + return 1; + return 0; + }); + + if (profiles.length) { + console.log("Automatic provisioning using profile " + + profiles[0].data.UUID + + " (" + + profiles[0].data.TeamName + + " - " + + profiles[0].data.Name + + ") in product " + + product.name); + return profiles[0]; + } +} + +function prepareSign(project, product, inputs, outputs, input, output) { + var cmd, cmds = []; + + if (!product.codesign.enableCodeSigning) + return cmds; + + var isBundle = "bundle.content" in outputs; + var outputFilePath = isBundle + ? FileInfo.joinPaths(product.destinationDirectory, product.bundle.bundleName) + : outputs["codesign.signed_artifact"][0].filePath; + var outputFileName = isBundle + ? product.bundle.bundleName + : outputs["codesign.signed_artifact"][0].fileName; + var isProductBundle = product.bundle && product.bundle.isBundle; + + // If the product is a bundle, just sign the bundle + // instead of signing the bundle and executable separately + var shouldSignArtifact = !isProductBundle || isBundle; + + var enableCodeSigning = product.codesign.enableCodeSigning; + if (enableCodeSigning && shouldSignArtifact) { + var actualSigningIdentity = product.codesign._actualSigningIdentity; + if (!actualSigningIdentity) { + throw "No codesigning identities (i.e. certificate and private key pairs) matching “" + + product.codesign.signingIdentity + "” were found."; + } + + // If this is a framework, we need to sign its versioned directory + var subpath = ""; + if (isBundle) { + var frameworkVersion = product.bundle.frameworkVersion; + if (frameworkVersion) { + subpath = product.bundle.contentsFolderPath; + subpath = subpath.substring(product.bundle.bundleName.length); + } + } + + var args = product.codesign.codesignFlags || []; + args.push("--force"); + args.push("--sign", actualSigningIdentity.SHA1); + + // If signingTimestamp is undefined, do not specify the flag at all - + // this uses the system-specific default behavior + var signingTimestamp = product.codesign.signingTimestamp; + if (signingTimestamp !== undefined) { + // If signingTimestamp is an empty string, specify the flag but do + // not specify a value - this uses a default Apple-provided server + var flag = "--timestamp"; + if (signingTimestamp) + flag += "=" + signingTimestamp; + args.push(flag); + } + + for (var j in inputs["codesign.xcent"]) { + args.push("--entitlements", inputs["codesign.xcent"][j].filePath); + break; // there should only be one + } + args.push(outputFilePath + subpath); + cmd = new Command(product.codesign.codesignPath, args); + cmd.description = "codesign " + outputFileName + + " (" + actualSigningIdentity.subjectInfo.CN + ")"; + cmd.outputFilePath = outputFilePath; + cmd.stderrFilterFunction = function(stderr) { + return stderr.replace(outputFilePath + ": replacing existing signature\n", ""); + }; + cmds.push(cmd); + } + + if (isBundle) { + cmd = new Command("touch", ["-c", outputFilePath]); + cmd.silent = true; + cmds.push(cmd); + } + + return cmds; +} diff --git a/share/qbs/modules/codesign/noop.qbs b/share/qbs/modules/codesign/noop.qbs new file mode 100644 index 000000000..3234d7476 --- /dev/null +++ b/share/qbs/modules/codesign/noop.qbs @@ -0,0 +1,37 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Copyright (C) 2021 Ivan Komissarov (abbapoh@gmail.com) +** Contact: http://www.qt.io/licensing +** +** This file is part of Qbs. +** +** 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 http://www.qt.io/terms-conditions. For further information +** use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +import qbs + +CodeSignModule { + condition: true + priority: -100 +} diff --git a/share/qbs/modules/cpp/GenericGCC.qbs b/share/qbs/modules/cpp/GenericGCC.qbs index 702a1ad93..34a0b47de 100644 --- a/share/qbs/modules/cpp/GenericGCC.qbs +++ b/share/qbs/modules/cpp/GenericGCC.qbs @@ -44,6 +44,8 @@ CppModule { condition: qbs.toolchain && qbs.toolchain.contains("gcc") priority: -100 + Depends { name: "codesign" } + Probes.GccBinaryProbe { id: compilerPathProbe condition: !toolchainInstallPath && !_skipAllChecks @@ -400,12 +402,15 @@ CppModule { "bundle.input", "dynamiclibrary", "dynamiclibrary_symlink", "dynamiclibrary_symbols", "debuginfo_dll", "debuginfo_bundle","dynamiclibrary_import", "debuginfo_plist", + "codesign.signed_artifact", ] outputArtifacts: { var artifacts = [{ filePath: product.destinationDirectory + "/" + PathTools.dynamicLibraryFilePath(product), - fileTags: ["bundle.input", "dynamiclibrary"], + fileTags: ["bundle.input", "dynamiclibrary"] + .concat(product.codesign.enableCodeSigning + ? ["codesign.signed_artifact"] : []), bundle: { _bundleFilePath: product.destinationDirectory + "/" + PathTools.bundleExecutableFilePath(product) @@ -510,12 +515,14 @@ CppModule { inputsFromDependencies: ["dynamiclibrary_symbols", "dynamiclibrary_import", "staticlibrary"] outputFileTags: ["bundle.input", "loadablemodule", "debuginfo_loadablemodule", - "debuginfo_bundle","debuginfo_plist"] + "debuginfo_bundle", "debuginfo_plist", "codesign.signed_artifact"] outputArtifacts: { var app = { filePath: FileInfo.joinPaths(product.destinationDirectory, PathTools.loadableModuleFilePath(product)), - fileTags: ["bundle.input", "loadablemodule"], + fileTags: ["bundle.input", "loadablemodule"] + .concat(product.codesign.enableCodeSigning + ? ["codesign.signed_artifact"] : []), bundle: { _bundleFilePath: FileInfo.joinPaths(product.destinationDirectory, PathTools.bundleExecutableFilePath(product)) @@ -547,13 +554,14 @@ CppModule { } inputsFromDependencies: ["dynamiclibrary_symbols", "dynamiclibrary_import", "staticlibrary"] - outputFileTags: ["bundle.input", "application", "debuginfo_app","debuginfo_bundle", - "debuginfo_plist", "mem_map"] + outputFileTags: ["bundle.input", "application", "debuginfo_app", "debuginfo_bundle", + "debuginfo_plist", "mem_map", "codesign.signed_artifact"] outputArtifacts: { var app = { filePath: FileInfo.joinPaths(product.destinationDirectory, PathTools.applicationFilePath(product)), - fileTags: ["bundle.input", "application"], + fileTags: ["bundle.input", "application"].concat( + product.codesign.enableCodeSigning ? ["codesign.signed_artifact"] : []), bundle: { _bundleFilePath: FileInfo.joinPaths(product.destinationDirectory, PathTools.bundleExecutableFilePath(product)) diff --git a/share/qbs/modules/cpp/gcc.js b/share/qbs/modules/cpp/gcc.js index f49609f94..99452b6da 100644 --- a/share/qbs/modules/cpp/gcc.js +++ b/share/qbs/modules/cpp/gcc.js @@ -28,6 +28,7 @@ ** ****************************************************************************/ +var Codesign = require("../codesign/codesign.js"); var Cpp = require("cpp.js"); var File = require("qbs.File"); var FileInfo = require("qbs.FileInfo"); @@ -1384,28 +1385,8 @@ function prepareLinker(project, product, inputs, outputs, input, output) { } } - if (product.xcode && product.bundle) { - var actualSigningIdentity = product.xcode.actualSigningIdentity; - var codesignDisplayName = product.xcode.actualSigningIdentityDisplayName; - if (actualSigningIdentity && !product.bundle.isBundle) { - args = product.xcode.codesignFlags || []; - args.push("--force"); - args.push("--sign", actualSigningIdentity); - args = args.concat(DarwinTools._codeSignTimestampFlags(product)); - - for (var j in inputs.xcent) { - args.push("--entitlements", inputs.xcent[j].filePath); - break; // there should only be one - } - args.push(primaryOutput.filePath); - cmd = new Command(product.xcode.codesignPath, args); - cmd.description = "codesign " - + primaryOutput.fileName - + " using " + codesignDisplayName - + " (" + actualSigningIdentity + ")"; - commands.push(cmd); - } - } + Array.prototype.push.apply( + commands, Codesign.prepareSign(project, product, inputs, outputs, input, output)); return commands; } diff --git a/share/qbs/modules/xcode/xcode.js b/share/qbs/modules/xcode/xcode.js index 9c87e09dc..1060894d4 100644 --- a/share/qbs/modules/xcode/xcode.js +++ b/share/qbs/modules/xcode/xcode.js @@ -93,6 +93,16 @@ var XcodeArchSpecsReader = (function () { return XcodeArchSpecsReader; }()); +function platformInfo(platformInfoPlist) { + var propertyList = new PropertyList(); + try { + propertyList.readFromFile(platformInfoPlist); + return propertyList.toObject(); + } finally { + propertyList.clear(); + } +} + function sdkInfoList(sdksPath) { var sdkInfo = []; var sdks = File.directoryEntries(sdksPath, File.Dirs | File.NoDotAndDotDot); @@ -181,6 +191,17 @@ function provisioningProfilePlistContents(filePath) { } } +function boolFromSdkOrPlatform(varName, sdkProps, platformProps, defaultValue) { + var values = [(sdkProps || {})[varName], (platformProps || {})[varName]]; + for (var i = 0; i < values.length; ++i) { + if (values[i] === "YES") + return true; + if (values[i] === "NO") + return false; + } + return defaultValue; +} + function archsSpecsPath(version, targetOS, platformType, platformPath, devicePlatformPath) { var _specsPluginBaseName; if (Utilities.versionCompare(version, "12") >= 0) { diff --git a/share/qbs/modules/xcode/xcode.qbs b/share/qbs/modules/xcode/xcode.qbs index e4df1f20b..72120ed37 100644 --- a/share/qbs/modules/xcode/xcode.qbs +++ b/share/qbs/modules/xcode/xcode.qbs @@ -7,7 +7,6 @@ import qbs.ModUtils import qbs.Probes import qbs.PropertyList import qbs.Utilities -import 'xcode.js' as Xcode Module { id: xcodeModule @@ -80,31 +79,6 @@ Module { } } - property string signingIdentity - readonly property string actualSigningIdentity: { - if (_actualSigningIdentity && _actualSigningIdentity.length === 2) - return _actualSigningIdentity[0]; - } - - readonly property string actualSigningIdentityDisplayName: { - if (_actualSigningIdentity && _actualSigningIdentity.length === 2) - return _actualSigningIdentity[1]; - } - - property string signingTimestamp: "none" - - property string provisioningProfile - - property string xcodebuildName: "xcodebuild" - property string xcodebuildPath: FileInfo.joinPaths(developerPath, "usr", "bin", xcodebuildName) - - property string securityName: "security" - property string securityPath: securityName - - property string codesignName: "codesign" - property string codesignPath: codesignName - property stringList codesignFlags - readonly property path toolchainPath: FileInfo.joinPaths(toolchainsPath, "XcodeDefault" + ".xctoolchain") readonly property path platformPath: FileInfo.joinPaths(platformsPath, @@ -136,35 +110,11 @@ Module { readonly property path toolchainInfoPlist: FileInfo.joinPaths(toolchainPath, "ToolchainInfo.plist") - readonly property stringList _actualSigningIdentity: { - if (/^[A-Fa-f0-9]{40}$/.test(signingIdentity)) { - return [signingIdentity, signingIdentity]; - } - - var result = []; - - if (signingIdentity) { - var identities = Utilities.signingIdentities(); - for (var key in identities) { - if (identities[key].subjectInfo.CN === signingIdentity) { - result.push([key, signingIdentity]); - } - } - - if (result.length == 0) { - throw "Unable to find signingIdentity '" + signingIdentity + "'"; - } - - if (result.length > 1) { - throw "Signing identity '" + signingIdentity + "' is ambiguous"; - } - } - - return result[0]; - } + readonly property var _platformSettings: xcodeProbe.platformSettings - property path provisioningProfilesPath: { - return FileInfo.joinPaths(Environment.getEnv("HOME"), "Library/MobileDevice/Provisioning Profiles"); + readonly property var _platformProps: { + if (_platformSettings) + return _platformSettings["DefaultProperties"]; } readonly property stringList standardArchitectures: _architectureSettings["ARCHS_STANDARD"] @@ -190,6 +140,12 @@ Module { } } + readonly property var _sdkProps: { + if (_sdkSettings) { + return _sdkSettings["DefaultProperties"]; + } + } + qbs.sysroot: sdkPath validate: { @@ -224,23 +180,10 @@ Module { validator.validate(); } - property var buildEnv: { - var env = { - "DEVELOPER_DIR": developerPath, - "SDKROOT": sdkPath - }; - - var prefixes = [platformPath + "/Developer", toolchainPath, developerPath]; - for (var i = 0; i < prefixes.length; ++i) { - var codesign_allocate = prefixes[i] + "/usr/bin/codesign_allocate"; - if (File.exists(codesign_allocate)) { - env["CODESIGN_ALLOCATE"] = codesign_allocate; - break; - } - } - - return env; - } + property var buildEnv: ({ + "DEVELOPER_DIR": developerPath, + "SDKROOT": sdkPath + }) setupBuildEnvironment: { var v = new ModUtils.EnvironmentVariable("PATH", product.qbs.pathListSeparator, false); @@ -254,191 +197,4 @@ Module { v.set(); } } - - Group { - name: "Provisioning Profiles" - prefix: xcode.provisioningProfilesPath + "/" - files: ["*.mobileprovision", "*.provisionprofile"] - fileTags: ["xcode.provisioningprofile"] - } - - FileTagger { - fileTags: ["xcode.entitlements"] - patterns: ["*.entitlements"] - } - - FileTagger { - fileTags: ["xcode.provisioningprofile"] - patterns: ["*.mobileprovision", "*.provisionprofile"] - } - - Rule { - inputs: ["xcode.provisioningprofile"] - - Artifact { - filePath: FileInfo.joinPaths("provisioning-profiles", input.fileName + ".xml") - fileTags: ["xcode.provisioningprofile.data"] - } - - prepare: { - var cmds = []; - - var cmd = new Command("openssl", ["smime", "-verify", "-noverify", "-inform", "DER", - "-in", input.filePath, "-out", output.filePath]); - cmd.silent = true; - cmd.stderrFilterFunction = function (output) { - return output.replace("Verification successful\n", ""); - }; - cmds.push(cmd); - - cmd = new JavaScriptCommand(); - cmd.silent = true; - cmd.inputFilePath = input.filePath; - cmd.outputFilePath = output.filePath; - cmd.sourceCode = function() { - var propertyList = new PropertyList(); - try { - propertyList.readFromFile(outputFilePath); - propertyList.readFromObject({ - data: propertyList.toObject(), - fileName: FileInfo.fileName(inputFilePath), - filePath: inputFilePath - }); - propertyList.writeToFile(outputFilePath, "xml1"); - } finally { - propertyList.clear(); - } - }; - cmds.push(cmd); - - return cmds; - } - } - - Rule { - multiplex: true - inputs: ["xcode.provisioningprofile.data"] - outputFileTags: ["xcode.provisioningprofile.main", "xcode.provisioningprofile.data.main"] - - outputArtifacts: { - var artifacts = []; - for (var i = 0; i < inputs["xcode.provisioningprofile.data"].length; ++i) { - var dataFile = inputs["xcode.provisioningprofile.data"][i].filePath; - var query = product.moduleProperty("xcode", "provisioningProfile"); - var obj = Xcode.provisioningProfilePlistContents(dataFile); - if (obj && obj.data && (obj.data.UUID === query || obj.data.Name === query)) { - console.log("Using provisioning profile: " + obj.filePath); - artifacts.push({ - filePath: obj.fileName, - fileTags: ["xcode.provisioningprofile.main"], - qbs: { _inputFilePath: obj.filePath } - }); - - artifacts.push({ - filePath: obj.fileName + ".xml", - fileTags: ["xcode.provisioningprofile.data.main"], - qbs: { _inputFilePath: dataFile } - }); - } - } - return artifacts; - } - - prepare: { - var cmds = []; - for (var tag in outputs) { - for (var i = 0; i < outputs[tag].length; ++i) { - var output = outputs[tag][i]; - var cmd = new JavaScriptCommand(); - cmd.silent = true; - cmd.inputFilePath = output.qbs._inputFilePath; // there's no such prop in qbs, see QBS-754 - cmd.outputFilePath = output.filePath; - cmd.sourceCode = function() { - File.copy(inputFilePath, outputFilePath); - }; - cmds.push(cmd); - } - } - return cmds; - } - } - - Rule { - inputs: ["xcode.entitlements", "xcode.provisioningprofile.data.main"] - - Artifact { - filePath: FileInfo.joinPaths(product.destinationDirectory, - product.targetName + ".xcent") - fileTags: ["xcent"] - } - - prepare: { - var cmd = new JavaScriptCommand(); - cmd.description = "generating entitlements"; - cmd.highlight = "codegen"; - cmd.bundleIdentifier = product.moduleProperty("bundle", "identifier"); - cmd.signingEntitlements = inputs["xcode.entitlements"] - ? inputs["xcode.entitlements"].map(function (a) { return a.filePath; }) - : []; - cmd.platformPath = ModUtils.moduleProperty(product, "platformPath"); - cmd.sdkPath = ModUtils.moduleProperty(product, "sdkPath"); - cmd.sourceCode = function() { - var i; - var provData = Xcode.provisioningProfilePlistContents(input.filePath); - if (provData) - provData = provData.data; - - var aggregateEntitlements = {}; - - // Start building up an aggregate entitlements plist from the files in the SDKs, - // which contain placeholders in the same manner as Info.plist - function entitlementsFileContents(path) { - return File.exists(path) ? BundleTools.infoPlistContents(path) : undefined; - } - var entitlementsSources = [ - entitlementsFileContents(FileInfo.joinPaths(platformPath, "Entitlements.plist")), - entitlementsFileContents(FileInfo.joinPaths(sdkPath, "Entitlements.plist")) - ]; - - for (i = 0; i < signingEntitlements.length; ++i) { - entitlementsSources.push(entitlementsFileContents(signingEntitlements[i])); - } - - for (i = 0; i < entitlementsSources.length; ++i) { - var contents = entitlementsSources[i]; - for (var key in contents) { - if (contents.hasOwnProperty(key)) - aggregateEntitlements[key] = contents[key]; - } - } - - contents = provData["Entitlements"]; - for (key in contents) { - if (contents.hasOwnProperty(key) && !aggregateEntitlements.hasOwnProperty(key)) - aggregateEntitlements[key] = contents[key]; - } - - // Expand entitlements variables with data from the provisioning profile - var env = { - "AppIdentifierPrefix": provData["ApplicationIdentifierPrefix"] + ".", - "CFBundleIdentifier": bundleIdentifier - }; - DarwinTools.expandPlistEnvironmentVariables(aggregateEntitlements, env, true); - - // Anything with an undefined or otherwise empty value should be removed - // Only JSON-formatted plists can have null values, other formats error out - // This also follows Xcode behavior - DarwinTools.cleanPropertyList(aggregateEntitlements); - - var plist = new PropertyList(); - try { - plist.readFromObject(aggregateEntitlements); - plist.writeToFile(outputs.xcent[0].filePath, "xml1"); - } finally { - plist.clear(); - } - }; - return [cmd]; - } - } } diff --git a/tests/auto/api/tst_api.cpp b/tests/auto/api/tst_api.cpp index 7a585f641..a51eb3e0c 100644 --- a/tests/auto/api/tst_api.cpp +++ b/tests/auto/api/tst_api.cpp @@ -2507,7 +2507,9 @@ qbs::SetupProjectParameters TestApi::defaultSetupParameters(const QString &proje } qbs::SetupProjectParameters setupParams; - setupParams.setEnvironment(QProcessEnvironment::systemEnvironment()); + auto environment = QProcessEnvironment::systemEnvironment(); + environment.insert("QBS_AUTOTEST_CODE_SIGNING_REQUIRED", "0"); + setupParams.setEnvironment(environment); setupParams.setProjectFilePath(projectFilePath); setupParams.setPropertyCheckingMode(qbs::ErrorHandlingMode::Strict); setupParams.setOverrideBuildGraphData(true); diff --git a/tests/auto/blackbox/testdata-apple/codesign/app.cpp b/tests/auto/blackbox/testdata-apple/codesign/app.cpp new file mode 100644 index 000000000..76e819701 --- /dev/null +++ b/tests/auto/blackbox/testdata-apple/codesign/app.cpp @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/tests/auto/blackbox/testdata-apple/codesign/codesign.qbs b/tests/auto/blackbox/testdata-apple/codesign/codesign.qbs new file mode 100644 index 000000000..312e9f001 --- /dev/null +++ b/tests/auto/blackbox/testdata-apple/codesign/codesign.qbs @@ -0,0 +1,38 @@ +Project { + name: "p" + + property bool isBundle: true + property bool enableSigning: true + + CppApplication { + name: "A" + bundle.isBundle: project.isBundle + files: "app.cpp" + codesign.enableCodeSigning: project.enableSigning + codesign.signingType: "ad-hoc" + install: true + installDir: "" + } + + DynamicLibrary { + Depends { name: "cpp" } + name: "B" + bundle.isBundle: project.isBundle + files: "app.cpp" + codesign.enableCodeSigning: project.enableSigning + codesign.signingType: "ad-hoc" + install: true + installDir: "" + } + + LoadableModule { + Depends { name: "cpp" } + name: "C" + bundle.isBundle: project.isBundle + files: "app.cpp" + codesign.enableCodeSigning: project.enableSigning + codesign.signingType: "ad-hoc" + install: true + installDir: "" + } +} diff --git a/tests/auto/blackbox/tst_blackbox.cpp b/tests/auto/blackbox/tst_blackbox.cpp index 20116ff56..2a3c40124 100644 --- a/tests/auto/blackbox/tst_blackbox.cpp +++ b/tests/auto/blackbox/tst_blackbox.cpp @@ -2173,7 +2173,7 @@ void TestBlackbox::trackExternalProductChanges() const QStringList toolchainTypes = profileToolchain(profile); if (!toolchainTypes.contains("gcc")) QSKIP("Need GCC-like compiler to run this test"); - params.environment = QProcessEnvironment::systemEnvironment(); + params.environment = QbsRunParameters::defaultEnvironment(); params.environment.insert("INCLUDE_PATH_TEST", "1"); params.expectFailure = true; QVERIFY(runQbs(params) != 0); @@ -6054,7 +6054,7 @@ void TestBlackbox::qbsSession() QJsonObject overriddenValues; overriddenValues.insert("products.theLib.cpp.cxxLanguageVersion", "c++17"); resolveMessage.insert("overridden-properties", overriddenValues); - resolveMessage.insert("environment", envToJson(QProcessEnvironment::systemEnvironment())); + resolveMessage.insert("environment", envToJson(QbsRunParameters::defaultEnvironment())); resolveMessage.insert("data-mode", "only-if-changed"); resolveMessage.insert("log-time", true); resolveMessage.insert("module-properties", diff --git a/tests/auto/blackbox/tst_blackboxapple.cpp b/tests/auto/blackbox/tst_blackboxapple.cpp index adde389ec..8915ac8b2 100644 --- a/tests/auto/blackbox/tst_blackboxapple.cpp +++ b/tests/auto/blackbox/tst_blackboxapple.cpp @@ -140,6 +140,39 @@ static QString findFatLibrary(const QString &dir, const QString &libraryName) return {}; } +enum class CodeSignResult { Failed = 0, Signed, Unsigned }; +using CodeSignData = QMap; +static std::pair parseCodeSignOutput(const QByteArray &output) +{ + CodeSignData data; + if (output.contains("code object is not signed at all")) + return {CodeSignResult::Unsigned, data}; + const auto lines = output.split('\n'); + for (const auto &line: lines) { + if (line.isEmpty() + || line.startsWith("CodeDirectory") + || line.startsWith("Sealed Resources") + || line.startsWith("Internal requirements")) { + continue; + } + const int index = line.indexOf('='); + if (index == -1) + return {CodeSignResult::Failed, {}}; + data[line.mid(0, index)] = line.mid(index + 1); + } + return {CodeSignResult::Signed, data}; +} + +static std::pair getCodeSignInfo(const QString &path) +{ + QProcess codesign; + codesign.start("codesign", { QStringLiteral("-dv"), path }); + if (!codesign.waitForStarted() || !codesign.waitForFinished()) + return {CodeSignResult::Failed, {}}; + const auto output = codesign.readAllStandardError(); + return parseCodeSignOutput(output); +} + TestBlackboxApple::TestBlackboxApple() : TestBlackboxBase (SRCDIR "/testdata-apple", "blackbox-apple") { @@ -680,6 +713,72 @@ void TestBlackboxApple::bundleStructure_data() QTest::newRow("G") << "G" << "com.apple.product-type.in-app-purchase-content"; } +void TestBlackboxApple::codesign() +{ + QFETCH(bool, isBundle); + QFETCH(bool, enableSigning); + + QDir::setCurrent(testDataDir + "/codesign"); + QbsRunParameters params(QStringList{"qbs.installPrefix:''"}); + params.arguments + << QStringLiteral("project.isBundle:%1").arg(isBundle ? "true" : "false"); + params.arguments + << QStringLiteral("project.enableSigning:%1").arg(enableSigning ? "true" : "false"); + + rmDirR(relativeBuildDir()); + QCOMPARE(runQbs(params), 0); + + const auto appName = isBundle ? QStringLiteral("A.app") : QStringLiteral("A"); + const auto appPath = defaultInstallRoot + "/" + appName; + QVERIFY(QFileInfo(appPath).exists()); + auto codeSignInfo = getCodeSignInfo(appPath); + QVERIFY(codeSignInfo.first != CodeSignResult::Failed); + QCOMPARE(codeSignInfo.first == CodeSignResult::Signed, enableSigning); + QCOMPARE(codeSignInfo.second.isEmpty(), !enableSigning); + if (!codeSignInfo.second.isEmpty()) { + QVERIFY(codeSignInfo.second.contains(QByteArrayLiteral("Executable"))); + QVERIFY(codeSignInfo.second.contains(QByteArrayLiteral("Identifier"))); + QCOMPARE(codeSignInfo.second.value(QByteArrayLiteral("Signature")), "adhoc"); + } + + const auto libName = isBundle ? QStringLiteral("B.framework") : QStringLiteral("libB.dylib"); + const auto libPath = defaultInstallRoot + "/" + libName; + QVERIFY(QFileInfo(libPath).exists()); + codeSignInfo = getCodeSignInfo(libPath); + QVERIFY(codeSignInfo.first != CodeSignResult::Failed); + QCOMPARE(codeSignInfo.first == CodeSignResult::Signed, enableSigning); + QCOMPARE(codeSignInfo.second.isEmpty(), !enableSigning); + if (!codeSignInfo.second.isEmpty()) { + QVERIFY(codeSignInfo.second.contains(QByteArrayLiteral("Executable"))); + QVERIFY(codeSignInfo.second.contains(QByteArrayLiteral("Identifier"))); + QCOMPARE(codeSignInfo.second.value(QByteArrayLiteral("Signature")), "adhoc"); + } + + const auto pluginPath = defaultInstallRoot + "/" + QStringLiteral("C.bundle"); + QVERIFY(QFileInfo(pluginPath).exists()); + QVERIFY(QFileInfo(pluginPath).isDir() == isBundle); + codeSignInfo = getCodeSignInfo(pluginPath); + QVERIFY(codeSignInfo.first != CodeSignResult::Failed); + QCOMPARE(codeSignInfo.first == CodeSignResult::Signed, enableSigning); + QCOMPARE(codeSignInfo.second.isEmpty(), !enableSigning); + if (!codeSignInfo.second.isEmpty()) { + QVERIFY(codeSignInfo.second.contains(QByteArrayLiteral("Executable"))); + QVERIFY(codeSignInfo.second.contains(QByteArrayLiteral("Identifier"))); + QCOMPARE(codeSignInfo.second.value(QByteArrayLiteral("Signature")), "adhoc"); + } +} + +void TestBlackboxApple::codesign_data() +{ + QTest::addColumn("isBundle"); + QTest::addColumn("enableSigning"); + + QTest::newRow("bundle, unsigned") << true << false; + QTest::newRow("standalone, unsigned") << false << false; + QTest::newRow("bundle, signed") << true << true; + QTest::newRow("standalone, signed") << false << true; +} + void TestBlackboxApple::deploymentTarget() { QFETCH(QString, sdk); diff --git a/tests/auto/blackbox/tst_blackboxapple.h b/tests/auto/blackbox/tst_blackboxapple.h index eeaa28d2f..32eee2432 100644 --- a/tests/auto/blackbox/tst_blackboxapple.h +++ b/tests/auto/blackbox/tst_blackboxapple.h @@ -55,6 +55,8 @@ private slots: void assetCatalogsMultiple(); void bundleStructure(); void bundleStructure_data(); + void codesign(); + void codesign_data(); void deploymentTarget(); void deploymentTarget_data(); void dmg(); diff --git a/tests/auto/blackbox/tst_blackboxbase.h b/tests/auto/blackbox/tst_blackboxbase.h index ed9a233de..d020b7cd9 100644 --- a/tests/auto/blackbox/tst_blackboxbase.h +++ b/tests/auto/blackbox/tst_blackboxbase.h @@ -60,7 +60,14 @@ public: expectCrash = false; profile = profileName(); settingsDir = settings()->baseDirectory(); - environment = QProcessEnvironment::systemEnvironment(); + environment = defaultEnvironment(); + } + + static QProcessEnvironment defaultEnvironment() + { + auto result = QProcessEnvironment::systemEnvironment(); + result.insert(QStringLiteral("QBS_AUTOTEST_CODE_SIGNING_REQUIRED"), QStringLiteral("0")); + return result; } QString command; -- cgit v1.2.3