Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions Sources/XcodeGraph/Models/Target.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,21 @@ public struct Target: Equatable, Hashable, Comparable, Codable, Sendable {
// Note: The `.docc` file type is technically both a valid source extension and folder extension
// in order to compile the documentation archive (including Tutorials, Articles, etc.)
public static let validSourceCompatibleFolderExtensions: [String] = [
"playground", "rcproject", "mlpackage", "docc", "xcmappingmodel",
"playground", "rcproject", "mlpackage", "docc", "xcmappingmodel", "xcdatamodeld",
]
public static let validSourceExtensions: [String] = [
"m", "swift", "mm", "cpp", "c++", "cc", "c", "d", "s", "intentdefinition", "metal", "mlmodel",
"m", "swift", "mm", "cpp", "c++", "cc", "c", "d", "s", "intentdefinition", "metal", "mlmodel", "clp",
]
public static let validResourceExtensions: [String] = [
// Resource
"md", "xcstrings", "plist", "rtf", "tutorial", "sks", "xcprivacy", "gpx", "strings", "stringsdict", "geojson",
// User interface
"storyboard", "xib",
// Other
"xcfilelist", "xcconfig",
]
public static let validResourceCompatibleFolderExtensions: [String] = [
"xcassets", "scnassets", "bundle", "xcstickers", "app",
]
public static let validFolderExtensions: [String] = [
"framework", "bundle", "app", "xcassets", "appiconset", "scnassets",
Expand Down
208 changes: 203 additions & 5 deletions Sources/XcodeGraphMapper/Mappers/Targets/PBXTargetMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ protocol PBXTargetMapping {
}

// swiftlint:disable function_body_length
// swiftlint:disable type_body_length
/// A mapper that converts a `PBXTarget` into a domain `Target` model.
///
/// `PBXTargetMapper` orchestrates various specialized mappers (e.g., sources, resources, headers)
Expand All @@ -64,6 +65,7 @@ struct PBXTargetMapper: PBXTargetMapping {
private let frameworksMapper: PBXFrameworksBuildPhaseMapping
private let dependencyMapper: PBXTargetDependencyMapping
private let buildRuleMapper: BuildRuleMapping
private let fileSystem: FileSysteming

init(
settingsMapper: SettingsMapping = XCConfigurationMapper(),
Expand All @@ -75,7 +77,8 @@ struct PBXTargetMapper: PBXTargetMapping {
coreDataModelsMapper: PBXCoreDataModelsBuildPhaseMapping = PBXCoreDataModelsBuildPhaseMapper(),
frameworksMapper: PBXFrameworksBuildPhaseMapping = PBXFrameworksBuildPhaseMapper(),
dependencyMapper: PBXTargetDependencyMapping = PBXTargetDependencyMapper(),
buildRuleMapper: BuildRuleMapping = PBXBuildRuleMapper()
buildRuleMapper: BuildRuleMapping = PBXBuildRuleMapper(),
fileSystem: FileSysteming = FileSystem()
) {
self.settingsMapper = settingsMapper
self.sourcesMapper = sourcesMapper
Expand All @@ -87,6 +90,7 @@ struct PBXTargetMapper: PBXTargetMapping {
self.frameworksMapper = frameworksMapper
self.dependencyMapper = dependencyMapper
self.buildRuleMapper = buildRuleMapper
self.fileSystem = fileSystem
}

func map(pbxTarget: PBXTarget, xcodeProj: XcodeProj) async throws -> Target {
Expand All @@ -102,18 +106,32 @@ struct PBXTargetMapper: PBXTargetMapping {
)

// Build Phases
let sources = try pbxTarget.sourcesBuildPhase().map {
var sources = try pbxTarget.sourcesBuildPhase().map {
try sourcesMapper.map($0, xcodeProj: xcodeProj)
} ?? []
sources = try await fileSystemSynchronizedGroupsSources(
from: pbxTarget,
xcodeProj: xcodeProj
) + sources

let resources = try pbxTarget.resourcesBuildPhase().map {
var resources = try pbxTarget.resourcesBuildPhase().map {
try resourcesMapper.map($0, xcodeProj: xcodeProj)
} ?? []
resources = try await fileSystemSynchronizedGroupsResources(
from: pbxTarget,
xcodeProj: xcodeProj
) + resources

let headers = try pbxTarget.headersBuildPhase().map {
var headers = try pbxTarget.headersBuildPhase().map {
try headersMapper.map($0, xcodeProj: xcodeProj)
} ?? nil

headers = try await addHeadersFromFileSystemSynchronizedGroups(
from: pbxTarget,
xcodeProj: xcodeProj,
headers: headers
)

let runScriptPhases = pbxTarget.runScriptBuildPhases()
let scripts = try scriptsMapper.map(runScriptPhases, buildPhases: pbxTarget.buildPhases)
let rawScriptBuildPhases = scriptsMapper.mapRawScriptBuildPhases(runScriptPhases)
Expand All @@ -127,10 +145,15 @@ struct PBXTargetMapper: PBXTargetMapping {

// Frameworks & libraries
let frameworksPhase = try pbxTarget.frameworksBuildPhase()
let frameworks = try frameworksPhase.map {
var frameworks = try frameworksPhase.map {
try frameworksMapper.map($0, xcodeProj: xcodeProj)
} ?? []

frameworks = try await fileSystemSynchronizedGroupsFrameworks(
from: pbxTarget,
xcodeProj: xcodeProj
) + frameworks

// Additional files (not in build phases)
let additionalFiles = try mapAdditionalFiles(from: pbxTarget, xcodeProj: xcodeProj)

Expand Down Expand Up @@ -331,6 +354,181 @@ struct PBXTargetMapper: PBXTargetMapping {
return .string(String(describing: value))
}
}

private func fileSystemSynchronizedGroupsSources(
from pbxTarget: PBXTarget,
xcodeProj: XcodeProj
) async throws -> [SourceFile] {
guard let fileSystemSynchronizedGroups = pbxTarget.fileSystemSynchronizedGroups else { return [] }
var sources: [SourceFile] = []
for fileSystemSynchronizedGroup in fileSystemSynchronizedGroups {
if let path = fileSystemSynchronizedGroup.path {
let membershipExceptions = membershipExceptions(for: fileSystemSynchronizedGroup)
let additionalCompilerFlagsByRelativePath = fileSystemSynchronizedGroup.exceptions?
.reduce(into: [:]) { acc, element in
acc.merge(element.additionalCompilerFlagsByRelativePath ?? [:], uniquingKeysWith: { $1 })
}
let directory = xcodeProj.srcPath.appending(component: path)
let groupSources = try await globFiles(
directory: directory,
include: [
// Build glob patterns for source files and source-compatible folders.
// This creates patterns like "**/*.{m,swift,mm,...}".
"**/*.{\(Target.validSourceExtensions.joined(separator: ","))}",
"**/*.{\(Target.validSourceCompatibleFolderExtensions.joined(separator: ","))}",
],
membershipExceptions: membershipExceptions
)
.map {
SourceFile(
path: $0,
compilerFlags: additionalCompilerFlagsByRelativePath?[$0.relative(to: directory).pathString]
)
}
sources.append(contentsOf: groupSources)
}
}
return sources
}

private func fileSystemSynchronizedGroupsResources(
from pbxTarget: PBXTarget,
xcodeProj: XcodeProj
) async throws -> [ResourceFileElement] {
let fileSystemSynchronizedGroups = pbxTarget.fileSystemSynchronizedGroups ?? []
var resources: [ResourceFileElement] = []
for fileSystemSynchronizedGroup in fileSystemSynchronizedGroups {
guard let path = fileSystemSynchronizedGroup.path else { continue }
let directory = xcodeProj.srcPath.appending(component: path)
let membershipExceptions = Set(
fileSystemSynchronizedGroup.exceptions
.map { $0.compactMap(\.membershipExceptions).flatMap { $0 } } ?? []
)

let groupResources = try await globFiles(
directory: directory,
include: [
// Build glob patterns for resource files and resource-compatible folders.
// This creates patterns like "**/*.{xcassets,png,...}".
"**/*.{\(Target.validResourceExtensions.joined(separator: ","))}",
"**/*.{\(Target.validResourceCompatibleFolderExtensions.joined(separator: ","))}",
],
membershipExceptions: membershipExceptions
)
.map {
ResourceFileElement(path: $0)
}
resources.append(contentsOf: groupResources)
}

return resources
}

private func fileSystemSynchronizedGroupsFrameworks(
from pbxTarget: PBXTarget,
xcodeProj: XcodeProj
) async throws -> [TargetDependency] {
let fileSystemSynchronizedGroups = pbxTarget.fileSystemSynchronizedGroups ?? []
var frameworks: [TargetDependency] = []
for fileSystemSynchronizedGroup in fileSystemSynchronizedGroups {
guard let path = fileSystemSynchronizedGroup.path else { continue }
let directory = xcodeProj.srcPath.appending(component: path)
let membershipExceptions = membershipExceptions(for: fileSystemSynchronizedGroup)
let attributesByRelativePath = fileSystemSynchronizedGroup.exceptions?.reduce([:]) { acc, element in
acc.merging(element.attributesByRelativePath ?? [:], uniquingKeysWith: { $1 })
}

let groupFrameworks: [TargetDependency] = try await globFiles(
directory: directory,
include: [
"**/*.framework",
],
membershipExceptions: membershipExceptions
)
.map {
return .framework(
path: $0,
status: attributesByRelativePath?[$0.relative(to: directory).pathString]?
.contains("Weak") == true ? .optional : .required,
condition: nil
)
}
frameworks.append(contentsOf: groupFrameworks)
}

return frameworks
}

private func addHeadersFromFileSystemSynchronizedGroups(
from pbxTarget: PBXTarget,
xcodeProj: XcodeProj,
headers: Headers?
) async throws -> Headers? {
let fileSystemSynchronizedGroups = pbxTarget.fileSystemSynchronizedGroups ?? []
var publicHeaders = headers?.public ?? []
var privateHeaders = headers?.private ?? []
var projectHeaders = headers?.project ?? []
for fileSystemSynchronizedGroup in fileSystemSynchronizedGroups {
guard let path = fileSystemSynchronizedGroup.path else { continue }
let directory = xcodeProj.srcPath.appending(component: path)
for synchronizedBuildFileSystemExceptionSet in fileSystemSynchronizedGroup.exceptions ?? [] {
for publicHeader in synchronizedBuildFileSystemExceptionSet.publicHeaders ?? [] {
publicHeaders.append(directory.appending(component: publicHeader))
}
for privateHeader in synchronizedBuildFileSystemExceptionSet.privateHeaders ?? [] {
privateHeaders.append(directory.appending(component: privateHeader))
}
}
let membershipExceptions = membershipExceptions(for: fileSystemSynchronizedGroup)

let publicHeadersSet = Set(publicHeaders)
let privateHeadersSet = Set(privateHeaders)

let groupHeaders = try await globFiles(
directory: directory,
include: [
"**/*.{h,hpp}",
],
membershipExceptions: membershipExceptions
)
.filter {
!privateHeadersSet.contains($0) && !publicHeadersSet.contains($0)
}
projectHeaders.append(contentsOf: groupHeaders)
}
if !publicHeaders.isEmpty || !privateHeaders.isEmpty || !projectHeaders.isEmpty || headers != nil {
return Headers(
public: publicHeaders,
private: privateHeaders,
project: projectHeaders
)
} else {
return headers
}
}

private func membershipExceptions(for fileSystemSynchronizedGroup: PBXFileSystemSynchronizedRootGroup) -> Set<String> {
Set(
fileSystemSynchronizedGroup.exceptions
.map { $0.compactMap(\.membershipExceptions).flatMap { $0 } } ?? []
)
}

/// Performs a glob search in the given directory using specified patterns, filtering out paths
/// that appear in the membershipExceptions set.
private func globFiles(
directory: AbsolutePath,
include: [String],
membershipExceptions: Set<String>
) async throws -> [AbsolutePath] {
return try await fileSystem.glob(
directory: directory,
include: include
)
.collect()
.filter { !membershipExceptions.contains($0.relative(to: directory).pathString) }
}
}

// swiftlint:enable function_body_length
// swiftlint:enable type_body_length
Loading