Skip to content

Commit

Permalink
Feat: Handling signing of other targets (extensions) (#250)
Browse files Browse the repository at this point in the history
* feat: include extensions to Matchfile signing

* fix: duplicating Variants folder at project

* feat: add unit tests

* chore: update docs

* feat: update signing for targets in xcode

* fix: linter and danger errors
  • Loading branch information
GMinucci authored Jan 27, 2025
1 parent 7e39737 commit 559a21f
Show file tree
Hide file tree
Showing 26 changed files with 450 additions and 214 deletions.
10 changes: 2 additions & 8 deletions Sources/VariantsCore/Custom Types/Project/AndroidProject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,7 @@ class AndroidProject: Project {
}

private func createVariants(with configuration: AndroidConfiguration, spec: String) throws {
guard let defaultVariant = configuration.variants
.first(where: { $0.name.lowercased() == "default" }) else {
throw ValidationError("Variant 'default' not found.")
}
let defaultVariant = try configuration.defaultVariant
try gradleFactory.createScript(with: configuration, variant: defaultVariant)
}

Expand Down Expand Up @@ -138,10 +135,7 @@ class AndroidProject: Project {
"""

if StaticPath.Fastlane.baseFolder.isDirectory {
guard let defaultVariant = configuration.variants
.first(where: { $0.name.lowercased() == "default" }) else {
throw ValidationError("Variant 'default' not found.")
}
let defaultVariant = try configuration.defaultVariant

// Create 'variants_params.rb' with parameters whose
// destination are set as '.fastlane'
Expand Down
39 changes: 15 additions & 24 deletions Sources/VariantsCore/Custom Types/Project/iOSProject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,23 +83,23 @@ class iOSProject: Project {

// Create 'variants.xcconfig' with parameters whose
// destination are set as '.project'
let configPath = Path(spec).absolute().parent()
do {
try configFactory.createConfig(
for: configuration.target,
variant: variant,
xcodeProj: configuration.xcodeproj,
configPath: Path(spec).absolute().parent())
for: variant,
configuration: configuration,
configPath: configPath)
} catch {
Logger.shared.logFatal(item: error.localizedDescription)
}

// Update `variants_params.rb` with custom fastlane properties
var customProperties: [CustomProperty] = (variant.custom ?? []) + (configuration.custom ?? [])
customProperties.append(variant.destinationProperty)
// Create 'variants_params.rb' with parameters whose
// destination are set as '.fastlane'
try? storeFastlaneParams(customProperties)
try storeFastlaneParams(customProperties)

try parametersFactory.createMatchFile(for: variant, target: configuration.target)
// Update `Matchfile` with signing configurations
try parametersFactory.createMatchFile(for: variant, configuration: configuration)
}

private func runPostSwitchScript(_ script: String) throws {
Expand All @@ -108,19 +108,15 @@ class iOSProject: Project {
}

private func createVariants(with configuration: iOSConfiguration, spec: String) throws {
guard let defaultVariant = configuration.variants
.first(where: { $0.name.lowercased() == "default" }) else {
throw ValidationError("Variant 'default' not found.")
}
let defaultVariant = try configuration.defaultVariant

// Create 'variants.xcconfig' with parameters whose
// destination are set as '.project'
let configPath = Path(spec).absolute().parent()
do {
try configFactory.createConfig(
for: configuration.target,
variant: defaultVariant,
xcodeProj: configuration.xcodeproj,
for: defaultVariant,
configuration: configuration,
configPath: configPath)
} catch {
Logger.shared.logFatal(item: error.localizedDescription)
Expand Down Expand Up @@ -166,20 +162,15 @@ class iOSProject: Project {
"""

if StaticPath.Fastlane.baseFolder.isDirectory {
let defaultVariant = try configuration.defaultVariant

guard let defaultVariant = configuration.variants
.first(where: { $0.name.lowercased() == "default" })
else {
throw ValidationError("Variant 'default' not found.")
}
// Update `variants_params.rb` with custom fastlane properties
var customProperties: [CustomProperty] = (defaultVariant.custom ?? []) + (configuration.custom ?? [])
customProperties.append(defaultVariant.destinationProperty)

// Create 'variants_params.rb' with parameters whose
// destination are set as '.fastlane'
try storeFastlaneParams(customProperties)

try parametersFactory.createMatchFile(for: defaultVariant, target: configuration.target)
// Update `Matchfile` with signing configurations
try parametersFactory.createMatchFile(for: defaultVariant, configuration: configuration)

setupCompleteMessage =
"""
Expand Down
13 changes: 9 additions & 4 deletions Sources/VariantsCore/Factory/FastlaneParametersFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import PathKit

protocol ParametersFactory {
func createParametersFile(in file: Path, renderTemplate: String, with parameters: [CustomProperty]) throws
func createMatchFile(for variant: iOSVariant, target: iOSTarget) throws
func createMatchFile(for variant: iOSVariant, configuration: iOSConfiguration) throws
func render(context: [String: Any], renderTemplate: String) throws -> Data?
func write(_ data: Data, using parametersFile: Path) throws
}
Expand All @@ -31,7 +31,7 @@ class FastlaneParametersFactory: ParametersFactory {
try write(data, using: file)
}

func createMatchFile(for variant: iOSVariant, target: iOSTarget) throws {
func createMatchFile(for variant: iOSVariant, configuration: iOSConfiguration) throws {
// Return immediately if folder 'fastlane/' doesn't exist.
guard StaticPath.Fastlane.baseFolder.exists && StaticPath.Fastlane.baseFolder.isDirectory
else { return }
Expand All @@ -43,9 +43,14 @@ class FastlaneParametersFactory: ParametersFactory {
with: parameters)

// Populate 'fastlane/Matchfile' from template
var context = [
let extensionBundleIDs = configuration.extensions
.filter { $0.signed }
.map { $0.makeBundleID(variant: variant, target: configuration.target) }
.reduce(into: [], { $0.append($1) })
let appBundleID = [variant.makeBundleID(for: configuration.target)]
var context: [String: Any] = [
"export_method": (variant.signing?.exportMethod ?? .appstore).rawValue,
"bundle_id": variant.makeBundleID(for: target)
"app_identifiers": appBundleID + extensionBundleIDs
]

if let matchURL = variant.signing?.matchURL {
Expand Down
120 changes: 76 additions & 44 deletions Sources/VariantsCore/Factory/iOS/XCConfigFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// Created by Arthur Alves
//

// swiftlint:disable file_length

import Foundation
import ArgumentParser
import PathKit
Expand All @@ -15,10 +17,7 @@ public typealias DoesFileExist = (exists: Bool, path: Path?)
protocol XCFactory {
func write(_ stringContent: String, toFile file: Path, force: Bool) -> (Bool, Path?)
func writeJSON<T>(_ encodableObject: T, toFile file: Path) -> (Bool, Path?) where T: Encodable
func createConfig(for target: iOSTarget,
variant: iOSVariant,
xcodeProj: String?,
configPath: Path) throws
func createConfig(for variant: iOSVariant, configuration: iOSConfiguration, configPath: Path) throws
}

class XCConfigFactory: XCFactory {
Expand Down Expand Up @@ -62,19 +61,11 @@ class XCConfigFactory: XCFactory {
}
}

func createConfig(for target: iOSTarget,
variant: iOSVariant,
xcodeProj: String?,
configPath: Path) throws {

func createConfig(for variant: iOSVariant, configuration: iOSConfiguration, configPath: Path) throws {
let logger = Logger.shared
guard let xcodeProj = xcodeProj
else {
throw RuntimeError("Attempting to create \(xcconfigFileName) - Path to Xcode Project not found")
}
let xcodeProjPath = Path(xcodeProj)
let configString = target.source.config

let xcodeProjPath = Path(configuration.xcodeproj)
let configString = configuration.target.source.config

logger.logInfo("Checking if \(xcconfigFileName) exists", item: "")
let xcodeConfigFolder = Path("\(configPath)/\(configString)")
guard xcodeConfigFolder.isDirectory else {
Expand All @@ -89,24 +80,25 @@ class XCConfigFactory: XCFactory {

_ = write("", toFile: xcodeConfigPath, force: true)
logger.logInfo("Created file: ", item: "'\(xcconfigFileName)' at \(xcodeConfigPath.parent().abbreviate().description)")
populateConfig(for: target, configFile: xcodeConfigPath, variant: variant)
populateConfig(for: configuration.target, configFile: xcodeConfigPath, variant: variant)

/*
* If template files should be added to Xcode Project
*/
addToXcode(xcodeConfigPath, toProject: xcodeProjPath, sourceRoot: configPath, target: target, variant: variant)
addToXcode(xcodeConfigPath, toProject: xcodeProjPath, sourceRoot: configPath, variant: variant, configuration: configuration)

/*
* Adjust signing configuration in project.pbxproj
*/
updateSigningConfig(for: target, variant: variant, projectPath: xcodeProjPath)
updateSigningConfig(for: variant, configuration: configuration, projectPath: xcodeProjPath)
updateSigningConfigForExtensions(for: variant, configuration: configuration, projectPath: xcodeProjPath)

/*
* INFO.plist
*/
let infoPath = target.source.info
let infoPath = configuration.target.source.info
let infoPlistPath = Path("\(configPath)/\(infoPath)")
updateInfoPlist(with: target, configFile: infoPlistPath, variant: variant)
updateInfoPlist(with: configuration.target, configFile: infoPlistPath, variant: variant)

/*
* Add custom properties whose values should be read from environment variables
Expand All @@ -121,8 +113,8 @@ class XCConfigFactory: XCFactory {
private func addToXcode(_ xcConfigFile: Path,
toProject projectPath: Path,
sourceRoot: Path,
target: iOSTarget,
variant: iOSVariant) {
variant: iOSVariant,
configuration: iOSConfiguration) {
let variantsFile = Path("\(xcConfigFile.parent().absolute().description)/Variants.swift")
do {
let path = try TemplateDirectory().path
Expand All @@ -132,22 +124,31 @@ class XCConfigFactory: XCFactory {
).run()

let xcodeFactory = XcodeProjFactory()
xcodeFactory.add([xcConfigFile, variantsFile], toProject: projectPath, sourceRoot: sourceRoot, target: target)

xcodeFactory.add([xcConfigFile, variantsFile], toProject: projectPath, sourceRoot: sourceRoot, target: configuration.target)

// Update main target
let mainTargetSettings = [
"PRODUCT_BUNDLE_IDENTIFIER": "$(V_BUNDLE_ID)",
"PRODUCT_NAME": "$(V_APP_NAME)",
"ASSETCATALOG_COMPILER_APPICON_NAME": "$(V_APP_ICON)"
]
xcodeFactory.modify(mainTargetSettings, in: projectPath, target: target)

xcodeFactory.modify(
[
"TEST_HOST": "$(BUILT_PRODUCTS_DIR)/$(V_APP_NAME).app/$(V_APP_NAME)"
],
in: projectPath,
target: target,
asTestSettings: true)
xcodeFactory.modify(mainTargetSettings, in: projectPath, targetName: configuration.target.source.info)

// Update test target
let testTargetSettings = [
"TEST_HOST": "$(BUILT_PRODUCTS_DIR)/$(V_APP_NAME).app/$(V_APP_NAME)"
]
xcodeFactory.modify(testTargetSettings, in: projectPath, targetName: configuration.target.testTarget)

// Update extensions
for targetExtension in configuration.extensions.filter({ $0.signed }) {
let bundleID = targetExtension.makeBundleID(variant: variant, target: configuration.target)
let extensionSettings = [
"PRODUCT_BUNDLE_IDENTIFIER": "\(bundleID)"
]
xcodeFactory.modify(extensionSettings, in: projectPath, targetName: targetExtension.name)
}

} catch {
logger.logError("", item: "Failed to add Variants.swift to Xcode Project")
}
Expand All @@ -167,8 +168,8 @@ class XCConfigFactory: XCFactory {
}

private func updateSigningConfig(
for target: iOSTarget,
variant: iOSVariant,
for variant: iOSVariant,
configuration: iOSConfiguration,
projectPath: Path
) {
guard
Expand All @@ -179,19 +180,48 @@ class XCConfigFactory: XCFactory {
!teamName.isEmpty
else { return }

let xcodeFactory = XcodeProjFactory()
var certType = "Development"
if exportMethod == .appstore || exportMethod == .enterprise {
certType = "Distribution"
}
let mainTargetSettings = [
let isDistribution = exportMethod == .appstore || exportMethod == .enterprise
let certType = isDistribution ? "Distribution" : "Development"
let signingSettings = [
"PROVISIONING_PROFILE_SPECIFIER": "$(V_MATCH_PROFILE)",
"CODE_SIGN_STYLE": "Manual",
"CODE_SIGN_IDENTITY": "Apple \(certType): \(teamName) (\(teamID))"
]
xcodeFactory.modify(mainTargetSettings, in: projectPath, target: target)

let xcodeFactory = XcodeProjFactory()
xcodeFactory.modify(signingSettings, in: projectPath, targetName: configuration.target.source.info)
}


private func updateSigningConfigForExtensions(
for variant: iOSVariant,
configuration: iOSConfiguration,
projectPath: Path
) {
let targetExtensions = configuration.extensions.filter({ $0.signed })
guard
!targetExtensions.isEmpty,
let exportMethod = variant.signing?.exportMethod,
let teamName = variant.signing?.teamName,
let teamID = variant.signing?.teamID,
!teamID.isEmpty,
!teamName.isEmpty
else { return }

let isDistribution = exportMethod == .appstore || exportMethod == .enterprise
let certType = isDistribution ? "Distribution" : "Development"

let xcodeFactory = XcodeProjFactory()
for targetExtension in targetExtensions {
let bundleID = targetExtension.makeBundleID(variant: variant, target: configuration.target)
let signingSettings = [
"PROVISIONING_PROFILE_SPECIFIER": "\(exportMethod.prefix) \(bundleID)",
"CODE_SIGN_STYLE": "Manual",
"CODE_SIGN_IDENTITY": "Apple \(certType): \(teamName) (\(teamID))"
]
xcodeFactory.modify(signingSettings, in: projectPath, targetName: targetExtension.name)
}
}

private func updateInfoPlist(with target: iOSTarget, configFile: Path, variant: iOSVariant) {
let configFilePath = configFile.absolute().description
do {
Expand Down Expand Up @@ -227,3 +257,5 @@ class XCConfigFactory: XCFactory {
let xcconfigFileName: String = "variants.xcconfig"
let logger: Logger
}

// swiftlint:enable file_length
11 changes: 5 additions & 6 deletions Sources/VariantsCore/Factory/iOS/XcodeProjFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,21 +151,20 @@ struct XcodeProjFactory {
/// - Parameters:
/// - keyValue: Key/value pair to be modified
/// - projectPath: Path to Xcode project
/// - target: iOSTarget on which the `buildSettings` should be changed.
/// - targetName: Name of the target on which the `buildSettings` should be changed.
/// - asTestSettings: If true, add configuraiton to test/non-host targets.
/// - silent: Flag to determine if final logs are necessary
func modify(_ keyValue: [String: String],
in projectPath: Path,
target: iOSTarget,
targetName: String,
asTestSettings: Bool = false,
silent: Bool = false) {
do {
let project = try XcodeProj(path: projectPath)
logger.logInfo("Updating: ", item: projectPath)

let matchingKey = asTestSettings ? target.testTarget : target.source.info

project.pbxproj.buildConfigurations
.filter({ ($0.buildSettings["INFOPLIST_FILE"] as? String)?.contains(matchingKey) ?? false })
.filter({ ($0.buildSettings["INFOPLIST_FILE"] as? String)?.contains(targetName) ?? false })
.forEach { conf in
keyValue.forEach { (key, value) in
Logger.shared.logDebug("Item: ", item: "\(key) = \(value)",
Expand All @@ -191,7 +190,7 @@ private extension XcodeProjFactory {
target: iOSTarget
) throws -> PBXGroup? {
let groupName = "Variants"
let currentVariantsGroup = project.pbxproj.groups.first(where: { $0.name == groupName })
let currentVariantsGroup = project.pbxproj.groups.first(where: { $0.path == groupName || $0.name == groupName })

guard currentVariantsGroup == nil else { return currentVariantsGroup }
let sourceGroup = project.pbxproj.groups.first(where: { $0.path == target.name })
Expand Down
Loading

0 comments on commit 559a21f

Please sign in to comment.