diff options
Diffstat (limited to 'share/qbs/modules/codesign')
-rw-r--r-- | share/qbs/modules/codesign/CodeSignModule.qbs | 47 | ||||
-rw-r--r-- | share/qbs/modules/codesign/android.qbs | 116 | ||||
-rw-r--r-- | share/qbs/modules/codesign/apple.qbs | 387 | ||||
-rw-r--r-- | share/qbs/modules/codesign/codesign.js | 351 | ||||
-rw-r--r-- | share/qbs/modules/codesign/noop.qbs | 37 |
5 files changed, 938 insertions, 0 deletions
diff --git a/share/qbs/modules/codesign/CodeSignModule.qbs b/share/qbs/modules/codesign/CodeSignModule.qbs new file mode 100644 index 000000000..1951ec374 --- /dev/null +++ b/share/qbs/modules/codesign/CodeSignModule.qbs @@ -0,0 +1,47 @@ +/**************************************************************************** +** +** 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 + + property bool _canSignArtifacts: false // whether can sign individual actifacts +} diff --git a/share/qbs/modules/codesign/android.qbs b/share/qbs/modules/codesign/android.qbs new file mode 100644 index 000000000..be96d42de --- /dev/null +++ b/share/qbs/modules/codesign/android.qbs @@ -0,0 +1,116 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Raphaël Cotty <raphael.cotty@gmail.com> +** Contact: http://www.qt.io/licensing +** +** This file is part of the 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.Environment +import qbs.File +import qbs.FileInfo +import qbs.ModUtils +import qbs.Probes +import "codesign.js" as CodeSign + +CodeSignModule { + condition: qbs.targetOS.contains("android") + priority: 1 + enableCodeSigning: true + + property bool useApksigner: true + property path apksignerFilePath + + Probes.JdkProbe { + id: jdk + environmentPaths: (jdkPath ? [jdkPath] : []).concat(base) + } + property string jdkPath: jdk.path + property string jarsignerFilePath: FileInfo.joinPaths(jdkPath, "bin", jarsignerName) + property string jarsignerName: "jarsigner" + property string keytoolFilePath: FileInfo.joinPaths(jdkPath, "bin", keytoolName) + property string keytoolName: "keytool" + + property string debugKeystorePath: FileInfo.joinPaths( + Environment.getEnv(qbs.hostOS.contains("windows") + ? "USERPROFILE" : "HOME"), + ".android", "debug.keystore") + readonly property string debugKeystorePassword: "android" + readonly property string debugPassword: "android" + readonly property string debugKeyAlias: "androiddebugkey" + + property string keystorePath: debugKeystorePath + property string keystorePassword: debugKeystorePassword + property string keyPassword: debugPassword + property string keyAlias: debugKeyAlias + + // Private property set by the Android.sdk module + property string _packageName + + Rule { + condition: useApksigner + inputs: ["android.package_unsigned"] + Artifact { + filePath: product.codesign._packageName + fileTags: "android.package" + } + prepare: CodeSign.signApkPackage.apply(this, arguments) + } + + Rule { + condition: !useApksigner + inputs: ["android.package_unsigned"] + Artifact { + filePath: product.codesign._packageName + fileTags: "android.package" + } + prepare: CodeSign.signAabPackage.apply(this, arguments) + } + + validate: { + // Typically there is a debug keystore in ~/.android/debug.keystore which gets created + // by the native build tools the first time a build is done. However, we don't want to + // create it ourselves, because writing to a location outside the qbs build directory is + // both polluting and has the potential for race conditions. So we'll instruct the user what + // to do. + if (keystorePath === debugKeystorePath && !File.exists(debugKeystorePath)) { + throw ModUtils.ModuleError("Could not find an Android debug keystore at " + + codesign.debugKeystorePath + ". " + + "If you are developing for Android on this machine for the first time and " + + "have never built an application using the native Gradle / Android Studio " + + "tooling, this is normal. You must create the debug keystore now using the " + + "following command, in order to continue:\n\n" + + CodeSign.createDebugKeyStoreCommandString(codesign.keytoolFilePath, + codesign.debugKeystorePath, + codesign.debugKeystorePassword, + codesign.debugPassword, + codesign.debugKeyAlias) + + "\n\n" + + "See the following URL for more information: " + + "https://developer.android.com/studio/publish/app-signing.html#debug-mode"); + } + } +} diff --git a/share/qbs/modules/codesign/apple.qbs b/share/qbs/modules/codesign/apple.qbs new file mode 100644 index 000000000..31e2c366d --- /dev/null +++ b/share/qbs/modules/codesign/apple.qbs @@ -0,0 +1,387 @@ +/**************************************************************************** +** +** 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 + + _canSignArtifacts: true + + 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..bf7e95224 --- /dev/null +++ b/share/qbs/modules/codesign/codesign.js @@ -0,0 +1,351 @@ +/**************************************************************************** +** +** 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 Process = require("qbs.Process"); +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; +} + +function signApkPackage(project, product, inputs, outputs, input, output, explicitlyDependsOn) { + var apkInput = inputs["android.package_unsigned"][0]; + var apkOutput = outputs["android.package"][0]; + var cmd; + if (product.codesign.enableCodeSigning) { + var args = ["sign", + "--ks", product.codesign.keystorePath, + "--ks-pass", "pass:" + product.codesign.keystorePassword, + "--ks-key-alias", product.codesign.keyAlias, + "--key-pass", "pass:" + product.codesign.keyPassword, + "--out", apkOutput.filePath, + apkInput.filePath]; + cmd = new Command(product.codesign.apksignerFilePath, args); + cmd.description = "signing " + apkOutput.fileName; + } else { + cmd = new JavaScriptCommand(); + cmd.description = "copying without signing " + apkOutput.fileName; + cmd.source = apkInput.filePath; + cmd.target = apkOutput.filePath; + cmd.silent = true; + cmd.sourceCode = function() { + // If enableCodeSigning is changed to false without any change to unsigned package then + // the copy won't happen because of timestamps. So the target file needs file needs to + // be removed to avoid it. + File.remove(target); + File.copy(source, target); + } + } + return cmd; +} + +function signAabPackage(project, product, inputs, outputs, input, output, explicitlyDependsOn) { + var aabInput = inputs["android.package_unsigned"][0]; + var aabOutput = outputs["android.package"][0]; + var cmd; + if (product.codesign.enableCodeSigning) { + args = ["-sigalg", "SHA1withRSA", "-digestalg", "SHA1", + "-keystore", product.codesign.keystorePath, + "-storepass", product.codesign.keystorePassword, + "-keypass", product.codesign.keyPassword, + "-signedjar", aabOutput.filePath, + aabInput.filePath, + product.codesign.keyAlias]; + cmd = new Command(product.codesign.jarsignerFilePath, args); + cmd.description = "signing " + aabOutput.fileName; + } else { + cmd = new JavaScriptCommand(); + cmd.description = "copying without signing " + aabOutput.fileName; + cmd.source = aabInput.filePath; + cmd.target = aabOutput.filePath; + cmd.silent = true; + cmd.sourceCode = function() { + // If enableCodeSigning is changed to false without any change to unsigned package then + // the copy won't happen because of timestamps. So the target file needs file needs to + // be removed to avoid it. + File.remove(target); + File.copy(source, target); + } + } + return cmd; +} + +function createDebugKeyStoreCommandString(keytoolFilePath, keystoreFilePath, keystorePassword, + keyPassword, keyAlias) { + var args = ["-genkey", "-keystore", keystoreFilePath, "-alias", keyAlias, + "-storepass", keystorePassword, "-keypass", keyPassword, "-keyalg", "RSA", + "-keysize", "2048", "-validity", "10000", "-dname", + "CN=Android Debug,O=Android,C=US"]; + return Process.shellQuote(keytoolFilePath, args); +} 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 +} |