@@ -53,17 +53,14 @@ public struct ProjectResources {
5353
5454 let buildConfigurations = try xcodeproj. buildConfigurations ( forTarget: targetName)
5555
56- let paths = try xcodeproj. resourcePaths ( forTarget: targetName)
57- let urls = paths
58- . map { $0. url ( with: sourceTreeURLs. url ( for: ) ) }
59- . filter { !ignoreFile. matches ( url: $0) }
60-
56+ var excludeURLs : [ URL ] = [ ]
6157 let infoPlists : [ PropertyListResource ]
6258 let entitlements : [ PropertyListResource ]
6359
6460 if resourceTypes. contains ( . info) {
6561 infoPlists = try buildConfigurations. compactMap { config -> PropertyListResource ? in
6662 guard let url = infoPlistFile else { return nil }
63+ excludeURLs. append ( url)
6764 return try parse ( with: warning) {
6865 try PropertyListResource . parse ( url: url, buildConfigurationName: config. name)
6966 }
@@ -75,12 +72,39 @@ public struct ProjectResources {
7572 if resourceTypes. contains ( . entitlements) {
7673 entitlements = try buildConfigurations. compactMap { config -> PropertyListResource ? in
7774 guard let url = codeSignEntitlements else { return nil }
75+ excludeURLs. append ( url)
7876 return try parse ( with: warning) { try PropertyListResource . parse ( url: url, buildConfigurationName: config. name) }
7977 }
8078 } else {
8179 entitlements = [ ]
8280 }
8381
82+ let paths = try xcodeproj. resourcePaths ( forTarget: targetName)
83+ let pathURLs = paths. map { $0. url ( with: sourceTreeURLs. url ( for: ) ) }
84+
85+ let extraURLs = try xcodeproj. extraResourceURLs ( forTarget: targetName, sourceTreeURLs: sourceTreeURLs)
86+
87+ // Combine URLs from Xcode project file with extra URLs found by scanning file system
88+ var pathAndExtraURLs = Array ( Set ( pathURLs + extraURLs) )
89+
90+ // Find all localized strings files for ignore extension so that those can be removed
91+ let localizedExtensions = [ " xib " , " storyboard " , " intentdefinition " ]
92+ let localizedStringURLs = findLocalizedStrings ( inputURLs: pathAndExtraURLs, ignoreExtensions: localizedExtensions)
93+
94+ // These file types are compiled, and shouldn't be included as resources
95+ // Note that this should be done after finding localized files
96+ let sourceCodeExtensions = [
97+ " swift " , " h " , " m " , " mm " , " c " , " cpp " , " metal " ,
98+ " xcdatamodeld " , " entitlements " , " intentdefinition " ,
99+ ]
100+ pathAndExtraURLs. removeAll ( where: { sourceCodeExtensions. contains ( $0. pathExtension) } )
101+
102+ // Remove all ignored files, excluded files and localized strings files
103+ let urls = pathAndExtraURLs
104+ . filter { !ignoreFile. matches ( url: $0) }
105+ . filter { !excludeURLs. contains ( $0) }
106+ . filter { !localizedStringURLs. contains ( $0) }
107+
84108 return try parseURLs (
85109 urls: urls,
86110 infoPlists: infoPlists,
@@ -184,6 +208,71 @@ public struct ProjectResources {
184208 }
185209}
186210
211+ // Finds strings files for Xcode generated files
212+ //
213+ // Example 1:
214+ // some-dir/Base.lproj/MyIntents.intentdefinition
215+ // some-dir/nl.lproj/MyIntents.string
216+ //
217+ // Example 2:
218+ // some-dir/Base.lproj/Main.storyboard
219+ // some-dir/nl.lproj/Main.string
220+ private func findLocalizedStrings( inputURLs: [ URL ] , ignoreExtensions: [ String ] ) -> [ URL ] {
221+ // Dictionary to map each parent directory to its `.lproj` subdirectories
222+ var parentToLprojDirectories = [ URL: [ URL] ] ( )
223+
224+ // Dictionary to keep track of files in each `.lproj` directory
225+ var directoryContents = [ URL: [ URL] ] ( )
226+
227+ // Populate the dictionaries
228+ for url in inputURLs {
229+ let directoryURL = url. deletingLastPathComponent ( )
230+ let parentDirectory = directoryURL. deletingLastPathComponent ( )
231+ if directoryURL. lastPathComponent. hasSuffix ( " .lproj " ) {
232+ parentToLprojDirectories [ parentDirectory, default: [ ] ] . append ( directoryURL)
233+ directoryContents [ directoryURL, default: [ ] ] . append ( url)
234+ }
235+ }
236+
237+ // Set of URLs to remove
238+ var urlsToRemove = Set < URL > ( )
239+
240+ // Analyze each group of sibling `.lproj` directories under the same parent
241+ for (_, lprojDirectories) in parentToLprojDirectories {
242+ var baseFilenameToFileUrls = [ String: [ URL] ] ( )
243+ var baseFilenamesWithIgnoreExtension = Set < String > ( )
244+
245+ // Collect all files by base filename and check for files with an ignoreExtension
246+ for directory in lprojDirectories {
247+ guard let files = directoryContents [ directory] else { continue }
248+ for file in files {
249+ let baseFilename = file. deletingPathExtension ( ) . lastPathComponent
250+ let fileExtension = file. pathExtension
251+
252+ baseFilenameToFileUrls [ baseFilename, default: [ ] ] . append ( file)
253+
254+ if ignoreExtensions. contains ( fileExtension) {
255+ baseFilenamesWithIgnoreExtension. insert ( baseFilename)
256+ }
257+ }
258+ }
259+
260+ // Determine which files to remove based on the presence of files with an ignoreExtension
261+ for baseFilename in baseFilenamesWithIgnoreExtension {
262+ if let files = baseFilenameToFileUrls [ baseFilename] {
263+ for file in files {
264+ if file. pathExtension == " strings " {
265+ urlsToRemove. insert ( file)
266+ }
267+ }
268+ }
269+ }
270+ }
271+
272+ return Array ( urlsToRemove)
273+ }
274+
275+
187276private func parse< R> ( with warning: ( String ) -> Void , closure: ( ) throws -> R ) throws -> R ? {
188277 do {
189278 return try closure ( )
0 commit comments