8
8
9
9
import Foundation
10
10
import XcodeEdit
11
+ import RswiftResources
11
12
12
13
public struct Xcodeproj : SupportedExtensions {
13
14
static public let supportedExtensions : Set < String > = [ " xcodeproj " ]
@@ -76,6 +77,97 @@ public struct Xcodeproj: SupportedExtensions {
76
77
return fileRefPaths + variantGroupPaths
77
78
}
78
79
80
+ // Returns extra resource URLs by extracting fileSystemSynchronizedGroups and scanning file system recursively.
81
+ // Handles exceptions configured in fileSystemSynchronizedGroups
82
+ func extraResourceURLs( forTarget targetName: String , sourceTreeURLs: SourceTreeURLs ) throws -> [ URL ] {
83
+ var resultURLs : [ URL ] = [ ]
84
+
85
+ let ( dirs, extraFiles, extraLocalizedFiles, exceptionPaths) = try fileSystemSynchronizedGroups ( forTarget: targetName)
86
+
87
+ for dir in dirs {
88
+ let url = dir. url ( with: sourceTreeURLs. url ( for: ) )
89
+ resultURLs. append ( contentsOf: recursiveContentsOf ( url: url) )
90
+ }
91
+
92
+ let extraURLs = extraFiles. map { $0. url ( with: sourceTreeURLs. url ( for: ) ) }
93
+ resultURLs. append ( contentsOf: extraURLs)
94
+
95
+ let extraLocalizedURLs = try extraLocalizedFiles
96
+ . map { $0. url ( with: sourceTreeURLs. url ( for: ) ) }
97
+ . flatMap { try expandLocalizedFileURL ( $0) }
98
+ resultURLs. append ( contentsOf: extraLocalizedURLs)
99
+
100
+ let exceptionURLs = exceptionPaths. map { $0. url ( with: sourceTreeURLs. url ( for: ) ) }
101
+ resultURLs. removeAll ( where: { exceptionURLs. contains ( $0) } )
102
+
103
+ let xcodeFilenames = [ " Info.plist " ]
104
+ resultURLs. removeAll ( where: { xcodeFilenames. contains ( $0. lastPathComponent) } )
105
+
106
+ return resultURLs
107
+ }
108
+
109
+ // For target, extract file system groups.
110
+ // Returns:
111
+ // - directories to scan
112
+ // - known files (based on exceptions of other targets)
113
+ // - known files that are localized (inside .lproj directory) (based on exceptions of other targets)
114
+ // - known exception files (based on exceptions of this target)
115
+ func fileSystemSynchronizedGroups( forTarget targetName: String ) throws -> ( dirs: [ Path ] , extraFiles: [ Path ] , extraLocalizedFiles: [ Path ] , exceptionPaths: [ Path ] ) {
116
+ var dirs : [ Path ] = [ ]
117
+ var extraFiles : [ Path ] = [ ]
118
+ var extraLocalizedFiles : [ Path ] = [ ]
119
+ var exceptionPaths : [ Path ] = [ ]
120
+
121
+ let target = try findTarget ( name: targetName)
122
+
123
+ guard let mainGroup = projectFile. project. mainGroup. value else {
124
+ throw ResourceParsingError ( " Project file is missing mainGroup " )
125
+ }
126
+
127
+ let targetFileSystemSynchronizedGroups = target. fileSystemSynchronizedGroups? . compactMap ( \. value? . id) ?? [ ]
128
+
129
+ let allFileSystemSynchronizedGroups = mainGroup. fileSystemSynchronizedGroups ( )
130
+
131
+ for synchronizedGroup in allFileSystemSynchronizedGroups {
132
+ guard let path = synchronizedGroup. fullPath else { continue }
133
+
134
+ let exceptions = ( synchronizedGroup. exceptions ?? [ ] ) . compactMap ( \. value)
135
+
136
+ if targetFileSystemSynchronizedGroups. contains ( synchronizedGroup. id) {
137
+ dirs. append ( path)
138
+
139
+ for exception in exceptions {
140
+ guard exception. target. id == target. id else { continue }
141
+
142
+ let files = exception. membershipExceptions ?? [ ]
143
+ let exPaths = files. map { file in path. map { dir in " \( dir) / \( file) " } }
144
+
145
+ exceptionPaths. append ( contentsOf: exPaths)
146
+ }
147
+ } else {
148
+ for exception in exceptions {
149
+ guard exception. target. id == target. id else { continue }
150
+
151
+ let files = exception. membershipExceptions ?? [ ]
152
+
153
+ let localized = " /Localized/ "
154
+ for file in files {
155
+ if file. hasPrefix ( localized) {
156
+ let cleanFile = String ( file. dropFirst ( localized. count) )
157
+ let exPath = path. map { dir in " \( dir) / \( cleanFile) " }
158
+ extraLocalizedFiles. append ( exPath)
159
+ } else {
160
+ let exPath = path. map { dir in " \( dir) / \( file) " }
161
+ extraFiles. append ( exPath)
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ return ( dirs: dirs, extraFiles: extraFiles, extraLocalizedFiles: extraLocalizedFiles, exceptionPaths: exceptionPaths)
169
+ }
170
+
79
171
public func buildConfigurations( forTarget targetName: String ) throws -> [ XCBuildConfiguration ] {
80
172
let target = try findTarget ( name: targetName)
81
173
@@ -87,3 +179,104 @@ public struct Xcodeproj: SupportedExtensions {
87
179
return buildConfigurations
88
180
}
89
181
}
182
+
183
+ extension PBXReference {
184
+ func fileSystemSynchronizedGroups( ) -> [ PBXFileSystemSynchronizedRootGroup ] {
185
+ if let root = self as? PBXFileSystemSynchronizedRootGroup {
186
+ return [ root]
187
+ } else if let group = self as? PBXGroup {
188
+ let children = group. children. compactMap ( \. value)
189
+
190
+ return children. flatMap { $0. fileSystemSynchronizedGroups ( ) }
191
+ } else {
192
+ return [ ]
193
+ }
194
+ }
195
+ }
196
+
197
+ extension Path {
198
+ func map( _ transform: ( String ) -> String ) -> Path {
199
+ switch self {
200
+ case let . absolute( str) :
201
+ return . absolute( transform ( str) )
202
+ case let . relativeTo( folder, str) :
203
+ return . relativeTo( folder, transform ( str) )
204
+ }
205
+ }
206
+ }
207
+
208
+ // Returns all(*) recursive files/directories that that are found on file system in specified directory.
209
+ // (*): xcassets are returned once, no deeper contents.
210
+ private func recursiveContentsOf( url: URL ) -> [ URL ] {
211
+ var resultURLs : [ URL ] = [ ]
212
+
213
+ var excludedExtensions = AssetCatalog . supportedExtensions
214
+ excludedExtensions. insert ( " bundle " )
215
+
216
+ if excludedExtensions. contains ( url. pathExtension) {
217
+ return [ ]
218
+ }
219
+
220
+ let enumerator = FileManager . default. enumerator ( at: url, includingPropertiesForKeys: [ . isRegularFileKey] , options: [ . skipsHiddenFiles] )
221
+
222
+ // Enumerator gives directories in hierarchical order (I assume/hope).
223
+ // If we hit a directory that is an .xcassets, we don't want to scan deeper, so we add it to the skipDirectories.
224
+ // Subsequent files/directories that have a skipDirectory as prefix are ignored.
225
+ var skipDirectories : [ URL ] = [ ]
226
+
227
+ guard let enumerator else { return [ ] }
228
+
229
+ for case let contentURL as URL in enumerator {
230
+ let shouldSkip = skipDirectories. contains { skip in contentURL. path. hasPrefix ( skip. path) }
231
+ if shouldSkip {
232
+ continue
233
+ }
234
+
235
+ if excludedExtensions. contains ( contentURL. pathExtension) {
236
+ resultURLs. append ( contentURL)
237
+ skipDirectories. append ( contentURL)
238
+ continue
239
+ }
240
+
241
+ if contentURL. hasDirectoryPath {
242
+ if excludedExtensions. contains ( contentURL. pathExtension) {
243
+ resultURLs. append ( contentURL)
244
+ skipDirectories. append ( contentURL)
245
+ }
246
+ } else {
247
+ resultURLs. append ( contentURL)
248
+ }
249
+ }
250
+
251
+ return resultURLs
252
+ }
253
+
254
+ // Returns the localized versions of an input URL
255
+ // Example: some-dir/Home.strings
256
+ // Becomes: some-dir/Base.lproj/Home.strings, some-dir/nl.lproj/Home.strings
257
+ private func expandLocalizedFileURL( _ url: URL ) throws -> [ URL ] {
258
+ let fileManager = FileManager . default
259
+ var localizedURLs : [ URL ] = [ ]
260
+
261
+ // Get the directory path and filename from the input URL
262
+ let directory = url. deletingLastPathComponent ( )
263
+ let filename = url. lastPathComponent
264
+
265
+ // Scan the directory for contents
266
+ let contents = try fileManager. contentsOfDirectory ( at: directory, includingPropertiesForKeys: nil )
267
+
268
+ // Filter the contents to find directories with the ".lproj" suffix
269
+ for item in contents {
270
+ if item. pathExtension == " lproj " {
271
+ // Construct the localized file path by appending the filename to the `.lproj` folder path
272
+ let localizedFileURL = item. appendingPathComponent ( filename)
273
+
274
+ // Check if the localized file exists
275
+ if fileManager. fileExists ( atPath: localizedFileURL. path) {
276
+ localizedURLs. append ( localizedFileURL)
277
+ }
278
+ }
279
+ }
280
+
281
+ return localizedURLs
282
+ }
0 commit comments