@@ -290,6 +290,207 @@ class SourceGeneratorTests: XCTestCase {
290290 try expect ( hasResourcesPhase) == true
291291 }
292292
293+ $0. it ( " deduplicates synced folders across targets " ) {
294+ let directories = """
295+ Sources:
296+ - a.swift
297+ """
298+ try createDirectories ( directories)
299+
300+ let source = TargetSource ( path: " Sources " , type: . syncedFolder)
301+ let target1 = Target ( name: " Target1 " , type: . application, platform: . iOS, sources: [ source] )
302+ let target2 = Target ( name: " Target2 " , type: . application, platform: . iOS, sources: [ source] )
303+ let project = Project ( basePath: directoryPath, name: " Test " , targets: [ target1, target2] )
304+
305+ let pbxProj = try project. generatePbxProj ( )
306+ let syncedFolders = try pbxProj. getMainGroup ( ) . children. compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
307+
308+ try expect ( syncedFolders. count) == 1
309+ }
310+
311+ $0. it ( " supports includes for synced folders " ) {
312+ let directories = """
313+ Sources:
314+ - included.swift
315+ - excluded.swift
316+ """
317+ try createDirectories ( directories)
318+
319+ let source = TargetSource ( path: " Sources " , includes: [ " included.swift " ] , type: . syncedFolder)
320+ let target = Target ( name: " Test " , type: . application, platform: . iOS, sources: [ source] )
321+ let project = Project ( basePath: directoryPath, name: " Test " , targets: [ target] )
322+
323+ let pbxProj = try project. generatePbxProj ( )
324+ let syncedFolders = try pbxProj. getMainGroup ( ) . children. compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
325+ let syncedFolder = try unwrap ( syncedFolders. first)
326+
327+ let exceptionSets = syncedFolder. exceptions? . compactMap { $0 as? PBXFileSystemSynchronizedBuildFileExceptionSet }
328+ let exceptionSet = try unwrap ( exceptionSets? . first)
329+ let exceptions = try unwrap ( exceptionSet. membershipExceptions)
330+
331+ try expect ( exceptions. contains ( " excluded.swift " ) ) == true
332+ try expect ( exceptions. contains ( " included.swift " ) ) == false
333+ }
334+
335+ $0. it ( " merges explicitFolders for synced folders across targets " ) {
336+ let directories = """
337+ Sources:
338+ - a.swift
339+ - FolderA:
340+ - b.swift
341+ - FolderB:
342+ - c.swift
343+ """
344+ try createDirectories ( directories)
345+
346+ let source1 = TargetSource ( path: " Sources " , explicitFolders: [ " FolderA " ] , type: . syncedFolder)
347+ let source2 = TargetSource ( path: " Sources " , explicitFolders: [ " FolderB " ] , type: . syncedFolder)
348+ let target1 = Target ( name: " Target1 " , type: . application, platform: . iOS, sources: [ source1] )
349+ let target2 = Target ( name: " Target2 " , type: . application, platform: . iOS, sources: [ source2] )
350+ let project = Project ( basePath: directoryPath, name: " Test " , targets: [ target1, target2] )
351+
352+ let pbxProj = try project. generatePbxProj ( )
353+ let syncedFolders = try pbxProj. getMainGroup ( ) . children. compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
354+ let syncedFolder = try unwrap ( syncedFolders. first)
355+
356+ try expect ( syncedFolder. explicitFolders? . sorted ( ) ) == [ " FolderA " , " FolderB " ]
357+ }
358+
359+ $0. it ( " supports different includes for the same synced folder across targets " ) {
360+ let directories = """
361+ Sources:
362+ - target1.swift
363+ - target2.swift
364+ - common.swift
365+ """
366+ try createDirectories ( directories)
367+
368+ let source1 = TargetSource ( path: " Sources " , includes: [ " target1.swift " , " common.swift " ] , type: . syncedFolder)
369+ let source2 = TargetSource ( path: " Sources " , includes: [ " target2.swift " , " common.swift " ] , type: . syncedFolder)
370+ let target1 = Target ( name: " Target1 " , type: . application, platform: . iOS, sources: [ source1] )
371+ let target2 = Target ( name: " Target2 " , type: . application, platform: . iOS, sources: [ source2] )
372+ let project = Project ( basePath: directoryPath, name: " Test " , targets: [ target1, target2] )
373+
374+ let pbxProj = try project. generatePbxProj ( )
375+ let syncedFolders = try pbxProj. getMainGroup ( ) . children. compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
376+ let syncedFolder = try unwrap ( syncedFolders. first)
377+
378+ let exceptionSets = syncedFolder. exceptions? . compactMap { $0 as? PBXFileSystemSynchronizedBuildFileExceptionSet }
379+ try expect ( exceptionSets? . count) == 2
380+
381+ let t1Exceptions = try unwrap ( exceptionSets? . first { $0. target? . name == " Target1 " } ? . membershipExceptions)
382+ try expect ( t1Exceptions. contains ( " target2.swift " ) ) == true
383+ try expect ( t1Exceptions. contains ( " target1.swift " ) ) == false
384+ try expect ( t1Exceptions. contains ( " common.swift " ) ) == false
385+
386+ let t2Exceptions = try unwrap ( exceptionSets? . first { $0. target? . name == " Target2 " } ? . membershipExceptions)
387+ try expect ( t2Exceptions. contains ( " target1.swift " ) ) == true
388+ try expect ( t2Exceptions. contains ( " target2.swift " ) ) == false
389+ try expect ( t2Exceptions. contains ( " common.swift " ) ) == false
390+ }
391+
392+ $0. it ( " correctly identifies exceptions for nested directories in includes " ) {
393+ let directories = """
394+ Sources:
395+ - a.swift
396+ - Nested:
397+ - b.swift
398+ - c.swift
399+ """
400+ try createDirectories ( directories)
401+
402+ let source = TargetSource ( path: " Sources " , includes: [ " Nested/b.swift " ] , type: . syncedFolder)
403+ let target = Target ( name: " Test " , type: . application, platform: . iOS, sources: [ source] )
404+ let project = Project ( basePath: directoryPath, name: " Test " , targets: [ target] )
405+
406+ let pbxProj = try project. generatePbxProj ( )
407+ let syncedFolders = try pbxProj. getMainGroup ( ) . children. compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
408+ let syncedFolder = try unwrap ( syncedFolders. first)
409+
410+ let exceptionSet = try unwrap ( syncedFolder. exceptions? . first as? PBXFileSystemSynchronizedBuildFileExceptionSet )
411+ let exceptions = try unwrap ( exceptionSet. membershipExceptions)
412+
413+ try expect ( exceptions. contains ( " a.swift " ) ) == true
414+ try expect ( exceptions. contains ( " Nested/c.swift " ) ) == true
415+ try expect ( exceptions. contains ( " Nested/b.swift " ) ) == false
416+ }
417+
418+ $0. it ( " excludes entire subdirectory as single exception when no files in it are included " ) {
419+ let directories = """
420+ Sources:
421+ - a.swift
422+ - ExcludedDir:
423+ - x.swift
424+ - y.swift
425+ """
426+ try createDirectories ( directories)
427+
428+ let source = TargetSource ( path: " Sources " , includes: [ " a.swift " ] , type: . syncedFolder)
429+ let target = Target ( name: " Test " , type: . application, platform: . iOS, sources: [ source] )
430+ let project = Project ( basePath: directoryPath, name: " Test " , targets: [ target] )
431+
432+ let pbxProj = try project. generatePbxProj ( )
433+ let syncedFolders = try pbxProj. getMainGroup ( ) . children. compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
434+ let syncedFolder = try unwrap ( syncedFolders. first)
435+
436+ let exceptionSet = try unwrap ( syncedFolder. exceptions? . first as? PBXFileSystemSynchronizedBuildFileExceptionSet )
437+ let exceptions = try unwrap ( exceptionSet. membershipExceptions)
438+
439+ // The whole directory should be a single exception entry, not each file within it
440+ try expect ( exceptions. contains ( " ExcludedDir " ) ) == true
441+ try expect ( exceptions. contains ( " ExcludedDir/x.swift " ) ) == false
442+ try expect ( exceptions. contains ( " ExcludedDir/y.swift " ) ) == false
443+ try expect ( exceptions. contains ( " a.swift " ) ) == false
444+ }
445+
446+ $0. it ( " respects excludes when includes are also specified " ) {
447+ let directories = """
448+ Sources:
449+ - a.swift
450+ - b.swift
451+ - c.swift
452+ """
453+ try createDirectories ( directories)
454+
455+ // includes a.swift and b.swift, but b.swift is also excluded → only a.swift is effectively included
456+ let source = TargetSource ( path: " Sources " , excludes: [ " b.swift " ] , includes: [ " a.swift " , " b.swift " ] , type: . syncedFolder)
457+ let target = Target ( name: " Test " , type: . application, platform: . iOS, sources: [ source] )
458+ let project = Project ( basePath: directoryPath, name: " Test " , targets: [ target] )
459+
460+ let pbxProj = try project. generatePbxProj ( )
461+ let syncedFolders = try pbxProj. getMainGroup ( ) . children. compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
462+ let syncedFolder = try unwrap ( syncedFolders. first)
463+
464+ let exceptionSet = try unwrap ( syncedFolder. exceptions? . first as? PBXFileSystemSynchronizedBuildFileExceptionSet )
465+ let exceptions = try unwrap ( exceptionSet. membershipExceptions)
466+
467+ try expect ( exceptions. contains ( " a.swift " ) ) == false
468+ try expect ( exceptions. contains ( " b.swift " ) ) == true
469+ try expect ( exceptions. contains ( " c.swift " ) ) == true
470+ }
471+
472+ $0. it ( " deduplicates synced folders and both targets reference the same group object " ) {
473+ let directories = """
474+ Sources:
475+ - a.swift
476+ """
477+ try createDirectories ( directories)
478+
479+ let source = TargetSource ( path: " Sources " , type: . syncedFolder)
480+ let target1 = Target ( name: " App " , type: . application, platform: . iOS, sources: [ source] )
481+ let target2 = Target ( name: " Tests " , type: . unitTestBundle, platform: . iOS, sources: [ source] )
482+ let project = Project ( basePath: directoryPath, name: " Test " , targets: [ target1, target2] )
483+
484+ let pbxProj = try project. generatePbxProj ( )
485+ let nativeTargets = pbxProj. nativeTargets
486+ let appTarget = try unwrap ( nativeTargets. first { $0. name == " App " } )
487+ let testsTarget = try unwrap ( nativeTargets. first { $0. name == " Tests " } )
488+
489+ let appGroup = try unwrap ( appTarget. fileSystemSynchronizedGroups? . first)
490+ let testsGroup = try unwrap ( testsTarget. fileSystemSynchronizedGroups? . first)
491+ try expect ( appGroup === testsGroup) == true
492+ }
493+
293494 $0. it ( " supports frameworks in sources " ) {
294495 let directories = """
295496 Sources:
0 commit comments