import Foundation import XcodeProj import PathKit func isUserScriptSandboxingEnabled(project: PBXProj) -> Bool { let targetConfigs = project.nativeTargets.first?.buildConfigurationList?.buildConfigurations ?? [] let rootConfigs = project.projects.first?.buildConfigurationList?.buildConfigurations ?? [] let buildConfigs = targetConfigs + rootConfigs var hasExplicitSetting = false for configuration in buildConfigs { if let userSandbox = configuration.buildSettings["ENABLE_USER_SCRIPT_SANDBOXING"] as? String { hasExplicitSetting = true if userSandbox.uppercased() == "YES" { return true } } } // If the value is absent, assume it is the default "YES" return !hasExplicitSetting } 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) { let targetConfigs = project.nativeTargets.flatMap { $0.buildConfigurationList?.buildConfigurations ?? [] } let rootConfigs = project.projects.first?.buildConfigurationList?.buildConfigurations ?? [] for configuration in targetConfigs + rootConfigs { 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 xcode_spm_setup [--plist ] [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()