← Files FirebaseARCHIVED FILE
skills/xcode-project-setup/scripts/xcode_spm_setup/Sources/main.swift
8.92 KB · Sep 30, 2026 · 23:01 UTC
import Foundation
import XcodeProj
import PathKit
func isUserScriptSandboxingEnabled(project: PBXProj) -> Bool {
guard let target = project.projects.first else {
print("Error: No project targets found")
return false
}
for configuration in target.buildConfigurationList?.buildConfigurations ?? [] {
if let userSandbox = configuration.buildSettings["ENABLE_USER_SCRIPT_SANDBOXING"] as? String {
return userSandbox.uppercased() == "YES"
}
}
// If the value is absent, assume it is the default "YES"
return true
}
func hasCrashlyticsRunScriptBuildPhase(project: PBXProj) -> Bool {
guard let nativeTargets = project.nativeTargets.first else {
return false
}
for phase in nativeTargets.buildPhases {
if phase.buildPhase == BuildPhase.runScript, let scriptPhase = phase as? PBXShellScriptBuildPhase {
if let script = scriptPhase.shellScript, script.contains("Crashlytics") {
return true
}
}
}
return false
}
func addCrashlyticsRunScriptBuildPhase(project: PBXProj) {
guard let nativeTarget = project.nativeTargets.first else {
print("Error: couldn't add the Crashlytics Run Script Build phase automatically, please add it manually")
return
}
var inputPaths = [
"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}",
"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}",
"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist",
"$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist",
"$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)"
]
if isUserScriptSandboxingEnabled(project: project) {
inputPaths.append("${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}.debug.dylib")
}
let phase = PBXShellScriptBuildPhase(
files: [],
inputPaths: inputPaths,
outputPaths: [],
shellPath: "/bin/sh",
shellScript: "\"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\n",
runOnlyForDeploymentPostprocessing: false
)
project.add(object: phase)
nativeTarget.buildPhases.append(phase)
}
func setDwarfWithDsymDebugInformationFormat(project: PBXProj) {
guard let target = project.projects.first else {
print("Error: No project targets found")
return
}
for configuration in target.buildConfigurationList?.buildConfigurations ?? [] {
// Set debug format for all configs
configuration.buildSettings["DEBUG_INFORMATION_FORMAT"] = "dwarf-with-dsym"
}
}
func main() {
let args = CommandLine.arguments
guard args.count >= 5 else {
print("Usage: swift run --package-path <path> xcode_spm_setup <Path/To/Project.xcodeproj> <RepoURL> <VersionRequirement> [--plist <Path/To/Plist>] <Product1> [Product2 ...]")
exit(1)
}
var arguments = args
_ = arguments.removeFirst() // executable name
let projectPath = Path(arguments.removeFirst())
let repoURL = arguments.removeFirst()
let versionRequirementString = arguments.removeFirst()
var plistPath: Path? = nil
if let plistIndex = arguments.firstIndex(of: "--plist"), plistIndex + 1 < arguments.count {
plistPath = Path(arguments[plistIndex + 1])
arguments.remove(at: plistIndex + 1)
arguments.remove(at: plistIndex)
}
let products = arguments
guard !products.isEmpty else {
print("Error: No products specified to link.")
exit(1)
}
do {
let xcodeproj = try XcodeProj(path: projectPath)
let pbxproj = xcodeproj.pbxproj
guard let rootObject = try pbxproj.rootProject() else {
print("Error: Could not find root project")
exit(1)
}
guard let target = pbxproj.nativeTargets.first else {
print("Error: No native targets found")
exit(1)
}
// 1. Add Plist to the project (Optional)
if let plistPath = plistPath {
print("Adding \(plistPath.lastComponent) to project...")
let mainGroup = rootObject.mainGroup
let appName = target.name
let groupToAddTo = mainGroup?.children.first(where: { $0.path == appName }) as? PBXGroup ?? mainGroup
// Only add if it doesn't already exist
if groupToAddTo?.children.contains(where: { $0.path == plistPath.lastComponent || $0.name == plistPath.lastComponent }) == false {
let fileRef = try groupToAddTo?.addFile(at: plistPath, sourceRoot: projectPath.parent())
if let fileRef = fileRef, let buildPhase = target.buildPhases.first(where: { $0.buildPhase == .resources }) as? PBXResourcesBuildPhase {
_ = try buildPhase.add(file: fileRef)
print("Successfully added \(plistPath.lastComponent) to resources build phase.")
}
} else {
print("\(plistPath.lastComponent) already exists in project.")
}
}
// 2. Add Swift Package Dependency
print("Adding Swift Package Dependency: \(repoURL)")
// Check if package already exists
let packageRef: XCRemoteSwiftPackageReference
if let existingPkg = rootObject.remotePackages.first(where: { $0.repositoryURL == repoURL }) {
packageRef = existingPkg
print("Package already present.")
} else {
packageRef = try rootObject.addSwiftPackage(
repositoryURL: repoURL,
productName: products.first!,
versionRequirement: .upToNextMajorVersion(versionRequirementString),
targetName: target.name
)
}
// 3. Link requested products
print("Linking products: \(products.joined(separator: ", "))")
var frameworksBuildPhase = target.buildPhases.compactMap { $0 as? PBXFrameworksBuildPhase }.first
if frameworksBuildPhase == nil {
let newPhase = PBXFrameworksBuildPhase()
pbxproj.add(object: newPhase)
target.buildPhases.append(newPhase)
frameworksBuildPhase = newPhase
}
for product in products {
// Check if product is already linked
if target.packageProductDependencies?.contains(where: { $0.productName == product }) == true {
print("Product \(product) is already linked.")
continue
}
let dependency = XCSwiftPackageProductDependency(productName: product, package: packageRef)
pbxproj.add(object: dependency)
if target.packageProductDependencies == nil { target.packageProductDependencies = [] }
target.packageProductDependencies?.append(dependency)
let buildFile = PBXBuildFile(product: dependency)
pbxproj.add(object: buildFile)
if frameworksBuildPhase?.files == nil { frameworksBuildPhase?.files = [] }
frameworksBuildPhase?.files?.append(buildFile)
}
// 4. Add -ObjC linker flag if adding Firebase
if products.contains(where: { $0.contains("Firebase") }) {
print("Adding -ObjC to OTHER_LDFLAGS...")
for configuration in target.buildConfigurationList?.buildConfigurations ?? [] {
var otherLdFlags: [String] = []
if let current = configuration.buildSettings["OTHER_LDFLAGS"] {
if let currentArray = current as? [String] {
otherLdFlags = currentArray
} else if let currentString = current as? String {
otherLdFlags = [currentString]
}
}
if !otherLdFlags.contains("-ObjC") {
otherLdFlags.append("-ObjC")
configuration.buildSettings["OTHER_LDFLAGS"] = otherLdFlags
print("Updated OTHER_LDFLAGS for configuration: \(configuration.name)")
}
}
}
if products.contains(where: { $0.contains("FirebaseCrashlytics")}) {
print("Setting the debug format to DWARF with dSYMs")
setDwarfWithDsymDebugInformationFormat(project: pbxproj)
print("Adding the Crashlytics Run Script Build phase")
if !hasCrashlyticsRunScriptBuildPhase(project: pbxproj) {
addCrashlyticsRunScriptBuildPhase(project: pbxproj)
} else {
print("Crashlytics Run Script Build phase already exists")
}
}
// Write changes
try xcodeproj.write(path: projectPath)
print("Successfully updated Xcode project!")
} catch {
print("Error: \(error)")
exit(1)
}
}
main()
SHA-256: ee7c6f5495c2ed32a600c237acb8ebd9d5ece1a48153a704c0b3bcf4fb4a2e45