1
+ // Copyright 2025 Google
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import Foundation
16
+ import ArgumentParser
17
+
18
+ struct SpmLocalizer : ParsableCommand {
19
+ static let configuration = CommandConfiguration (
20
+ abstract: " Updates an Xcode project's firebase-ios-sdk SPM dependency to point to a specific version, branch, or commit. "
21
+ )
22
+
23
+ @Argument ( help: " Path to the .xcodeproj file. " )
24
+ var projectPath : String
25
+
26
+ @Option ( name: . long, help: " The version to use for release testing (e.g., '10.24.0'). " )
27
+ var version : String ?
28
+
29
+ @Flag ( name: . long, help: " Flag to point to the latest commit on the main branch for prerelease testing. " )
30
+ var prerelease = false
31
+
32
+ @Option ( name: . long, help: " The commit hash to use for PR/branch testing. " )
33
+ var revision : String ?
34
+
35
+ func run( ) throws {
36
+ let pbxprojPath = " \( projectPath) /project.pbxproj "
37
+ var pbxprojContents : String
38
+ do {
39
+ pbxprojContents = try String ( contentsOfFile: pbxprojPath, encoding: . utf8)
40
+ } catch {
41
+ fatalError ( " Failed to read project.pbxproj file: \( error) " )
42
+ }
43
+
44
+ let requirement : String
45
+ let indent4 = " \t \t \t \t "
46
+ let indent3 = " \t \t \t "
47
+ if let version = version {
48
+ // Release testing: Point to CocoaPods-{VERSION} tag (as a branch)
49
+ requirement = " { \n \( indent4) kind = branch; \n \( indent4) branch = \" CocoaPods- \( version) \" ; \n \( indent3) } "
50
+ } else if prerelease {
51
+ // Prerelease testing: Point to the tip of the main branch
52
+ let commitHash = try getRemoteHeadRevision ( branch: " main " )
53
+ requirement = " { \n \( indent4) kind = revision; \n \( indent4) revision = \" \( commitHash) \" ; \n \( indent3) } "
54
+ } else if let revision = revision {
55
+ // PR testing: Point to the specific commit hash of the current branch
56
+ requirement = " { \n \( indent4) kind = revision; \n \( indent4) revision = \" \( revision) \" ; \n \( indent3) } "
57
+ } else {
58
+ fatalError ( " No dependency requirement specified. Please provide --version, --prerelease, or --revision. " )
59
+ }
60
+
61
+ let updatedContents = try replaceDependency ( in: pbxprojContents, with: requirement)
62
+
63
+ do {
64
+ try updatedContents. write ( toFile: pbxprojPath, atomically: true , encoding: . utf8)
65
+ print ( " Successfully updated SPM dependency in \( pbxprojPath) " )
66
+ } catch {
67
+ fatalError ( " Failed to write updated contents to project.pbxproj: \( error) " )
68
+ }
69
+ }
70
+
71
+ private func replaceDependency( in content: String , with requirement: String ) throws -> String {
72
+ let pattern = #"(repositoryURL = "https://github.com/firebase/firebase-ios-sdk";\s*requirement = )\{[^\}]+\};"#
73
+ let regex = try NSRegularExpression ( pattern: pattern, options: [ ] )
74
+
75
+ let range = NSRange ( content. startIndex..< content. endIndex, in: content)
76
+ let template = " $1 \( requirement) ; "
77
+
78
+ let modifiedContent = regex. stringByReplacingMatches ( in: content, options: [ ] , range: range, withTemplate: template)
79
+
80
+ if content == modifiedContent {
81
+ fatalError ( " Failed to find and replace the firebase-ios-sdk dependency. Check the regex pattern and project file structure. " )
82
+ }
83
+
84
+ return modifiedContent
85
+ }
86
+
87
+ private func getRemoteHeadRevision( branch: String ) throws -> String {
88
+ let process = Process ( )
89
+ process. executableURL = URL ( fileURLWithPath: " /usr/bin/git " )
90
+ process. arguments = [ " ls-remote " , " https://github.com/firebase/firebase-ios-sdk.git " , branch]
91
+
92
+ let pipe = Pipe ( )
93
+ process. standardOutput = pipe
94
+
95
+ try process. run ( )
96
+ process. waitUntilExit ( )
97
+
98
+ let data = pipe. fileHandleForReading. readDataToEndOfFile ( )
99
+ guard let output = String ( data: data, encoding: . utf8) , !output. isEmpty else {
100
+ fatalError ( " Failed to get remote revision for branch ' \( branch) '. No output from git. " )
101
+ }
102
+
103
+ // Output is in the format: <hash>\trefs/heads/<branch>
104
+ let components = output. components ( separatedBy: CharacterSet . whitespacesAndNewlines)
105
+ if components. isEmpty {
106
+ fatalError ( " Invalid output from git ls-remote: \( output) " )
107
+ }
108
+ return components [ 0 ]
109
+ }
110
+ }
111
+
112
+ SpmLocalizer . main ( )
0 commit comments