diff options
author | Jake Petroules <jake.petroules@qt.io> | 2016-04-22 01:20:24 -0700 |
---|---|---|
committer | Ivan Komissarov <ABBAPOH@gmail.com> | 2021-02-18 00:10:00 +0000 |
commit | 2bc823ec00cec8a1d58981710eb50ba85b4f58d7 (patch) | |
tree | 4397fc51e963e00f8ab6dbf7686d5e28cda40c01 /share | |
parent | 05b74ff728e8c6bc975e8c55160e1feb137895a1 (diff) |
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 <christian.kandeler@qt.io>
Diffstat (limited to 'share')
-rw-r--r-- | share/qbs/imports/qbs/DarwinTools/darwin-tools.js | 16 | ||||
-rw-r--r-- | share/qbs/imports/qbs/Probes/XcodeProbe.qbs | 3 | ||||
-rw-r--r-- | share/qbs/modules/bundle/BundleModule.qbs | 71 | ||||
-rw-r--r-- | share/qbs/modules/codesign/CodeSignModule.qbs | 45 | ||||
-rw-r--r-- | share/qbs/modules/codesign/apple.qbs | 385 | ||||
-rw-r--r-- | share/qbs/modules/codesign/codesign.js | 279 | ||||
-rw-r--r-- | share/qbs/modules/codesign/noop.qbs | 37 | ||||
-rw-r--r-- | share/qbs/modules/cpp/GenericGCC.qbs | 20 | ||||
-rw-r--r-- | share/qbs/modules/cpp/gcc.js | 25 | ||||
-rw-r--r-- | share/qbs/modules/xcode/xcode.js | 21 | ||||
-rw-r--r-- | share/qbs/modules/xcode/xcode.qbs | 272 |
11 files changed, 826 insertions, 348 deletions
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]; - } - } } |