diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1ed45f5b --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +fixtures/* binary diff --git a/.github/workflows/XcodeGraph.yml b/.github/workflows/XcodeGraph.yml index 576ef25f..2c3b8f06 100644 --- a/.github/workflows/XcodeGraph.yml +++ b/.github/workflows/XcodeGraph.yml @@ -30,7 +30,7 @@ jobs: - ubuntu-22.04 - macos-15 swift-version: - - "5.9" + - "6.0.3" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -48,7 +48,12 @@ jobs: if: steps.debug_build.outcome == 'failure' run: | swift build --configuration debug + - name: Test on Linux + if: matrix.os == 'ubuntu-22.04' + # These momdules rely on some Xcode utilities like xcode-select + run: swift test --skip XcodeProjMapperTests --skip XcodeMetadataTests - name: Test + if: matrix.os != 'ubuntu-22.04' run: swift test spm_build: name: SPM Build @@ -58,7 +63,7 @@ jobs: - ubuntu-22.04 - macos-15 swift-version: - - "5.9" + - "6.0.3" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -85,38 +90,6 @@ jobs: if: steps.release_build.outcome == 'failure' run: | swift build --configuration release - tuist_test: - name: Tuist Test - strategy: - matrix: - os: - - macos-15 - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: jdx/mise-action@v2 - with: - version: 2024.11.8 - - name: Install dependencies - run: tuist install - - name: Test - run: tuist test - tuist_build: - name: Tuist Build - strategy: - matrix: - os: - - macos-15 - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: jdx/mise-action@v2 - with: - version: 2024.11.8 - - name: Install dependencies - run: tuist install - - name: Build - run: tuist build lint: name: Lint strategy: diff --git a/.github/workflows/cache.yml b/.github/workflows/cache.yml deleted file mode 100644 index 00dd8252..00000000 --- a/.github/workflows/cache.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Cache -on: - push: - branches: - - main - paths: - - "**/*.swift" - - ".github/workflows/*.yml" - pull_request: - paths: - - "**/*.swift" - - ".github/workflows/*.yml" - -concurrency: - group: Cache-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - MISE_EXPERIMENTAL: 1 - TUIST_CONFIG_TOKEN: ${{ secrets.TUIST_CONFIG_CLOUD_TOKEN }} - -jobs: - warm: - name: Warm - strategy: - matrix: - os: - - macos-15 - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: jdx/mise-action@v2 - with: - version: 2024.11.8 - - name: Install dependencies - run: tuist install - - name: Build - run: tuist cache diff --git a/.gitignore b/.gitignore index 6f032e45..3083e85c 100644 --- a/.gitignore +++ b/.gitignore @@ -102,6 +102,7 @@ Tuist/Dependencies/graph.json # VSCode Settings .vscode/launch.json +.vscode # Release artifacts .bundle \ No newline at end of file diff --git a/.swiftformat b/.swiftformat index 6ef4b1cc..4fa95466 100644 --- a/.swiftformat +++ b/.swiftformat @@ -7,7 +7,7 @@ --disable hoistAwait --disable hoistTry --disable redundantReturn ---swiftversion 5.9 +--swiftversion 5.10 --minversion 0.53.0 # format options diff --git a/.swiftlint.yml b/.swiftlint.yml index 42dbd48f..80604831 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -8,6 +8,7 @@ disabled_rules: - function_parameter_count - opening_brace - line_length + - large_tuple identifier_name: min_length: error: 1 @@ -21,4 +22,10 @@ inclusive_language: type_name: min_length: error: 1 - warning: 1 \ No newline at end of file + warning: 1 +custom_rules: + error_must_conform_to_localizederror: + name: "Error must conform to LocalizedError" + regex: '(struct|class|enum)\s+\w+Error:\s*Error\s*(?!,\s*LocalizedError)' + message: "Errors must conform to LocalizedError to provide user-friendly descriptions." + severity: error diff --git a/Package.resolved b/Package.resolved index c1487b6c..89c23aa7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,15 @@ { + "originHash" : "e0b3d78a18232f4e8d0bd5c09f120c7920018f6a53c16bc16ff38be99be54dbe", "pins" : [ + { + "identity" : "aexml", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tadija/AEXML.git", + "state" : { + "revision" : "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3", + "version" : "4.6.1" + } + }, { "identity" : "anycodable", "kind" : "remoteSourceControl", @@ -9,6 +19,42 @@ "version" : "0.6.7" } }, + { + "identity" : "command", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/Command.git", + "state" : { + "revision" : "437e0c0ca18d1a16194c55b4690971b5bfb1f185", + "version" : "0.12.0" + } + }, + { + "identity" : "filesystem", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/FileSystem.git", + "state" : { + "revision" : "267329f17e523575162f0887d20f2b78f609585a", + "version" : "0.7.6" + } + }, + { + "identity" : "machokit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/p-x9/MachOKit", + "state" : { + "revision" : "518e8e1aca7ee64b87b08ecec5f7cad2a63b8efd", + "version" : "0.28.0" + } + }, + { + "identity" : "mockable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Kolos65/Mockable.git", + "state" : { + "revision" : "e1b311b01c11415099341eee49769185e965ac4c", + "version" : "0.2.0" + } + }, { "identity" : "path", "kind" : "remoteSourceControl", @@ -17,7 +63,115 @@ "revision" : "7c74ac435e03a927c3a73134c48b61e60221abcb", "version" : "0.3.8" } + }, + { + "identity" : "pathkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kylef/PathKit.git", + "state" : { + "revision" : "3bfd2737b700b9a36565a8c94f4ad2b050a5e574", + "version" : "1.0.1" + } + }, + { + "identity" : "spectre", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kylef/Spectre.git", + "state" : { + "revision" : "26cc5e9ae0947092c7139ef7ba612e34646086c7", + "version" : "0.10.1" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log", + "state" : { + "revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91", + "version" : "1.6.2" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio", + "state" : { + "revision" : "dff45738d84a53dbc8ee899c306b3a7227f54f89", + "version" : "2.80.0" + } + }, + { + "identity" : "swift-service-context", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-service-context", + "state" : { + "revision" : "0c62c5b4601d6c125050b5c3a97f20cce881d32b", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "c8a44d836fe7913603e246acab7c528c2e780168", + "version" : "1.4.0" + } + }, + { + "identity" : "xcodeproj", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/XcodeProj", + "state" : { + "revision" : "647cba2719e85748ec82d0720ee7afe5b7a6421e", + "version" : "8.26.3" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1", + "version" : "1.4.3" + } + }, + { + "identity" : "zipfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/ZIPFoundation", + "state" : { + "revision" : "e9b1917bd4d7d050e0ff4ec157b5d6e253c84385", + "version" : "0.9.20" + } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index 781feed7..2bc8b861 100644 --- a/Package.swift +++ b/Package.swift @@ -1,8 +1,7 @@ -// swift-tools-version:5.9 - +// swift-tools-version:5.10 import PackageDescription -var targets: [Target] = [ +let targets: [Target] = [ .target( name: "XcodeGraph", dependencies: [ @@ -13,6 +12,39 @@ var targets: [Target] = [ .enableExperimentalFeature("StrictConcurrency"), ] ), + .target( + name: "XcodeMetadata", + dependencies: [ + .product(name: "ServiceContextModule", package: "swift-service-context"), + .product(name: "FileSystem", package: "FileSystem"), + .product(name: "Mockable", package: "Mockable"), + .product(name: "MachOKitC", package: "MachOKit"), + ], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency"), + .define("MOCKING", .when(configuration: .debug)), + ] + ), + .testTarget( + name: "XcodeMetadataTests", + dependencies: ["XcodeMetadata", "XcodeGraph"], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency"), + ] + ), + .target( + name: "XcodeProjMapper", + dependencies: [ + "XcodeGraph", + "XcodeMetadata", + .product(name: "Command", package: "Command"), + .product(name: "Path", package: "Path"), + .product(name: "XcodeProj", package: "XcodeProj"), + ], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency"), + ] + ), .testTarget( name: "XcodeGraphTests", dependencies: [.target(name: "XcodeGraph")], @@ -20,20 +52,37 @@ var targets: [Target] = [ .enableExperimentalFeature("StrictConcurrency"), ] ), + .testTarget( + name: "XcodeProjMapperTests", + dependencies: [ + "XcodeProjMapper", + .product(name: "FileSystem", package: "FileSystem"), + ], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency"), + ] + ), ] let package = Package( name: "XcodeGraph", - platforms: [.macOS(.v12)], + platforms: [.macOS(.v13)], products: [ .library( name: "XcodeGraph", targets: ["XcodeGraph"] ), + .library(name: "XcodeProjMapper", targets: ["XcodeProjMapper"]), ], dependencies: [ .package(url: "https://github.com/Flight-School/AnyCodable", .upToNextMajor(from: "0.6.7")), .package(url: "https://github.com/tuist/Path.git", .upToNextMajor(from: "0.3.8")), + .package(url: "https://github.com/tuist/XcodeProj", from: "8.26.0"), + .package(url: "https://github.com/tuist/Command.git", from: "0.11.0"), + .package(url: "https://github.com/tuist/FileSystem.git", .upToNextMajor(from: "0.6.17")), + .package(url: "https://github.com/apple/swift-service-context", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/Kolos65/Mockable.git", .upToNextMajor(from: "0.0.11")), + .package(url: "https://github.com/p-x9/MachOKit", .upToNextMajor(from: "0.28.0")), ], targets: targets ) diff --git a/Project.swift b/Project.swift deleted file mode 100644 index 4b4cd374..00000000 --- a/Project.swift +++ /dev/null @@ -1,107 +0,0 @@ -import ProjectDescription -import ProjectDescriptionHelpers - -let baseSettings: SettingsDictionary = [ - "SWIFT_STRICT_CONCURRENCY": "complete", -] - -func debugSettings() -> SettingsDictionary { - var settings = baseSettings - settings["ENABLE_TESTABILITY"] = "YES" - return settings -} - -func releaseSettings() -> SettingsDictionary { - baseSettings -} - -func schemes() -> [Scheme] { - var schemes: [Scheme] = [ - .scheme( - name: "XcodeGraph-Workspace", - buildAction: .buildAction(targets: Module.allCases.flatMap(\.targets).map(\.name).sorted().map { .target($0) }), - testAction: .targets( - Module.allCases.flatMap(\.testTargets).map { .testableTarget(target: .target($0.name)) } - ), - runAction: .runAction( - arguments: .arguments( - environmentVariables: [ - "TUIST_CONFIG_SRCROOT": "$(SRCROOT)", - "TUIST_FRAMEWORK_SEARCH_PATHS": "$(FRAMEWORK_SEARCH_PATHS)", - ] - ) - ) - ), - .scheme( - name: "TuistAcceptanceTests", - buildAction: .buildAction( - targets: Module.allCases.flatMap(\.acceptanceTestTargets).map(\.name).sorted() - .map { .target($0) } - ), - testAction: .targets( - Module.allCases.flatMap(\.acceptanceTestTargets).map { .testableTarget(target: .target($0.name)) } - ), - runAction: .runAction( - arguments: .arguments( - environmentVariables: [ - "TUIST_CONFIG_SRCROOT": "$(SRCROOT)", - "TUIST_FRAMEWORK_SEARCH_PATHS": "$(FRAMEWORK_SEARCH_PATHS)", - ] - ) - ) - ), - ] - schemes.append(contentsOf: Module.allCases.filter(\.isRunnable).map { - .scheme( - name: $0.targetName, - buildAction: .buildAction(targets: [.target($0.targetName)]), - runAction: .runAction( - executable: .target($0.targetName), - arguments: .arguments( - environmentVariables: [ - "TUIST_CONFIG_SRCROOT": "$(SRCROOT)", - "TUIST_FRAMEWORK_SEARCH_PATHS": "$(FRAMEWORK_SEARCH_PATHS)", - ] - ) - ) - ) - }) - - schemes.append(contentsOf: Module.allCases.compactMap(\.acceptanceTestsTargetName).map { - .scheme( - name: $0, - hidden: true, - buildAction: .buildAction(targets: [.target($0)]), - testAction: .targets([.testableTarget(target: .target($0))]), - runAction: .runAction( - arguments: .arguments( - environmentVariables: [ - "TUIST_CONFIG_SRCROOT": "$(SRCROOT)", - "TUIST_FRAMEWORK_SEARCH_PATHS": "$(FRAMEWORK_SEARCH_PATHS)", - ] - ) - ) - ) - }) - - return schemes -} - -let project = Project( - name: "XcodeGraph", - options: .options( - automaticSchemesOptions: .disabled, - textSettings: .textSettings(usesTabs: false, indentWidth: 4, tabWidth: 4) - ), - settings: .settings( - configurations: [ - .debug(name: "Debug", settings: debugSettings(), xcconfig: nil), - .release(name: "Release", settings: releaseSettings(), xcconfig: nil), - ] - ), - targets: Module.allCases.flatMap(\.targets), - schemes: schemes(), - additionalFiles: [ - "README.md", - ] -) diff --git a/README.md b/README.md index c36d4eb0..1ee6be33 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,29 @@ let package = Package( ], ) ``` + +## XcodeGraphMapper + +XcodeGraphMapper parses `.xcworkspace` or `.xcodeproj` files using `XcodeProj` and constructs a `XcodeGraph.Graph` representing their projects, targets, and dependencies: + +### Usage + +```swift +import XcodeGraphMapper +let mapper: XcodeGraphMapping = XcodeGraphMapper() +let path = try AbsolutePath(validating: "/path/to/MyProjectOrWorkspace") +let graph = try await mapper.map(at: path) +// You now have a Graph containing projects, targets, packages, and dependencies.* +// Example: print all target names across all projects* +for project in graph.projects.values { + for (targetName, _) in project.targets { + print("Found target: \(targetName)") + } +} +``` + +Once you have the Graph, you can explore or transform it as needed—printing targets, analyzing dependencies, generating reports, or integrating into other build tools. + ## Contributors ✨ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): @@ -45,4 +68,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! \ No newline at end of file +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! diff --git a/Sources/XcodeGraph/DependenciesGraph/DependenciesGraph.swift b/Sources/XcodeGraph/DependenciesGraph/DependenciesGraph.swift index 62972a92..639bd16b 100644 --- a/Sources/XcodeGraph/DependenciesGraph/DependenciesGraph.swift +++ b/Sources/XcodeGraph/DependenciesGraph/DependenciesGraph.swift @@ -22,7 +22,7 @@ public struct DependenciesGraph: Equatable, Codable, Sendable { #if DEBUG extension DependenciesGraph { - public static func test( + static func test( externalDependencies: [String: [TargetDependency]] = [:], externalProjects: [AbsolutePath: Project] = [:] ) -> Self { @@ -43,7 +43,7 @@ public struct DependenciesGraph: Equatable, Codable, Sendable { ) } - public static func test( + static func test( packageFolder: AbsolutePath ) -> Self { let externalDependencies = [ diff --git a/Sources/XcodeGraph/Graph/Graph.swift b/Sources/XcodeGraph/Graph/Graph.swift index 8cdf72d7..5f749c82 100644 --- a/Sources/XcodeGraph/Graph/Graph.swift +++ b/Sources/XcodeGraph/Graph/Graph.swift @@ -68,7 +68,7 @@ extension [GraphEdge: PlatformCondition] { #if DEBUG extension Graph { - public static func test( + static func test( name: String = "graph", path: AbsolutePath = .root, workspace: Workspace = .test(), diff --git a/Sources/XcodeGraph/Graph/GraphDependency.swift b/Sources/XcodeGraph/Graph/GraphDependency.swift index 005cc1e8..37988426 100644 --- a/Sources/XcodeGraph/Graph/GraphDependency.swift +++ b/Sources/XcodeGraph/Graph/GraphDependency.swift @@ -1,8 +1,8 @@ import Foundation import Path -public enum GraphDependency: Hashable, CustomStringConvertible, Comparable, Codable { - public struct XCFramework: Hashable, CustomStringConvertible, Comparable, Codable { +public enum GraphDependency: Hashable, CustomStringConvertible, Comparable, Codable, Sendable { + public struct XCFramework: Hashable, CustomStringConvertible, Comparable, Codable, Sendable { public let path: AbsolutePath public let infoPlist: XCFrameworkInfoPlist public let linking: BinaryLinking @@ -39,7 +39,7 @@ public enum GraphDependency: Hashable, CustomStringConvertible, Comparable, Coda } } - public enum PackageProductType: String, Hashable, CustomStringConvertible, Comparable, Codable { + public enum PackageProductType: String, Hashable, CustomStringConvertible, Comparable, Codable, Sendable { public var description: String { rawValue } diff --git a/Sources/XcodeGraph/Graph/GraphEdge.swift b/Sources/XcodeGraph/Graph/GraphEdge.swift index da6a3975..2ad7e166 100644 --- a/Sources/XcodeGraph/Graph/GraphEdge.swift +++ b/Sources/XcodeGraph/Graph/GraphEdge.swift @@ -2,7 +2,7 @@ import Foundation /// A directed edge linking representing a dependent relationship /// e.g. `from` (MainApp) depends on `to` (UIKit) -public struct GraphEdge: Hashable, Codable { +public struct GraphEdge: Hashable, Codable, Sendable { public let from: GraphDependency public let to: GraphDependency public init(from: GraphDependency, to: GraphDependency) { diff --git a/Sources/XcodeGraph/Models/BinaryArchitecture.swift b/Sources/XcodeGraph/Models/BinaryArchitecture.swift index ce2321a3..c731c5fe 100644 --- a/Sources/XcodeGraph/Models/BinaryArchitecture.swift +++ b/Sources/XcodeGraph/Models/BinaryArchitecture.swift @@ -1,6 +1,6 @@ import Foundation -public enum BinaryArchitecture: String, Codable { +public enum BinaryArchitecture: String, CaseIterable, Codable, Sendable { case x8664 = "x86_64" case i386 case armv7 @@ -11,7 +11,7 @@ public enum BinaryArchitecture: String, Codable { case arm64e } -public enum BinaryLinking: String, Hashable, Codable { +public enum BinaryLinking: String, Hashable, Codable, Sendable { case `static`, dynamic } diff --git a/Sources/XcodeGraph/Models/Platform.swift b/Sources/XcodeGraph/Models/Platform.swift index 7bc512f6..b0102d21 100644 --- a/Sources/XcodeGraph/Models/Platform.swift +++ b/Sources/XcodeGraph/Models/Platform.swift @@ -1,13 +1,17 @@ import Foundation -public struct UnsupportedPlatformError: Error, CustomStringConvertible, Equatable { +public struct UnsupportedPlatformError: LocalizedError, CustomStringConvertible, Equatable { let input: String public var description: String { "Specified platform \(input) does not map to any of these supported platforms: \(Platform.allCases.map(\.caseValue).joined(separator: ", ")) " } + + public var errorDescription: String? { + description + } } -public enum Platform: String, CaseIterable, Codable, Comparable { +public enum Platform: String, CaseIterable, Codable, Comparable, Sendable { case iOS = "ios" case macOS = "macos" case tvOS = "tvos" diff --git a/Sources/XcodeGraph/Models/PlatformFilter.swift b/Sources/XcodeGraph/Models/PlatformFilter.swift index a7bff439..419e126d 100644 --- a/Sources/XcodeGraph/Models/PlatformFilter.swift +++ b/Sources/XcodeGraph/Models/PlatformFilter.swift @@ -3,7 +3,7 @@ import Foundation /// Convenience typealias to be used to ensure unique filters are applied public typealias PlatformFilters = Set -extension PlatformFilters: Comparable { +extension PlatformFilters: @retroactive Comparable { public static func < (lhs: Set, rhs: Set) -> Bool { lhs.map(\.xcodeprojValue).sorted().joined() < rhs.map(\.xcodeprojValue).sorted().joined() } diff --git a/Sources/XcodeGraph/Models/Plist.swift b/Sources/XcodeGraph/Models/Plist.swift index 5f1d743b..30e95550 100644 --- a/Sources/XcodeGraph/Models/Plist.swift +++ b/Sources/XcodeGraph/Models/Plist.swift @@ -94,24 +94,24 @@ extension Dictionary where Value == Plist.Value { public enum InfoPlist: Equatable, Codable, Sendable { // Path to a user defined info.plist file (already exists on disk). - case file(path: AbsolutePath) + case file(path: AbsolutePath, configuration: BuildConfiguration? = nil) // Path to a generated info.plist file (may not exist on disk at the time of project generation). // Data of the generated file - case generatedFile(path: AbsolutePath, data: Data) + case generatedFile(path: AbsolutePath, data: Data, configuration: BuildConfiguration? = nil) // User defined dictionary of keys/values for an info.plist file. - case dictionary([String: Plist.Value]) + case dictionary([String: Plist.Value], configuration: BuildConfiguration? = nil) // User defined dictionary of keys/values for an info.plist file extending the default set of keys/values // for the target type. - case extendingDefault(with: [String: Plist.Value]) + case extendingDefault(with: [String: Plist.Value], configuration: BuildConfiguration? = nil) // MARK: - Public public var path: AbsolutePath? { switch self { - case let .file(path), let .generatedFile(path: path, data: _): + case let .file(path, _), let .generatedFile(path: path, data: _, configuration: _): return path default: return nil @@ -131,23 +131,23 @@ extension InfoPlist: ExpressibleByStringLiteral { public enum Entitlements: Equatable, Codable, Sendable { // Path to a user defined .entitlements file (already exists on disk). - case file(path: AbsolutePath) + case file(path: AbsolutePath, configuration: BuildConfiguration? = nil) // Path to a generated .entitlements file (may not exist on disk at the time of project generation). // Data of the generated file - case generatedFile(path: AbsolutePath, data: Data) + case generatedFile(path: AbsolutePath, data: Data, configuration: BuildConfiguration? = nil) // User defined dictionary of keys/values for an .entitlements file. - case dictionary([String: Plist.Value]) + case dictionary([String: Plist.Value], configuration: BuildConfiguration? = nil) // A user defined xcconfig variable map to .entitlements file - case variable(String) + case variable(String, configuration: BuildConfiguration? = nil) // MARK: - Public public var path: AbsolutePath? { switch self { - case let .file(path), let .generatedFile(path: path, data: _): + case let .file(path, _), let .generatedFile(path: path, data: _, _): return path default: return nil diff --git a/Sources/XcodeGraph/Models/ResourceSynthesizer.swift b/Sources/XcodeGraph/Models/ResourceSynthesizer.swift index 9b8880e2..23748225 100644 --- a/Sources/XcodeGraph/Models/ResourceSynthesizer.swift +++ b/Sources/XcodeGraph/Models/ResourceSynthesizer.swift @@ -13,7 +13,7 @@ public struct ResourceSynthesizer: Equatable, Hashable, Codable, Sendable { case defaultTemplate(String) } - public enum Parser: String, Equatable, Hashable, Codable, Sendable { + public enum Parser: String, CaseIterable, Equatable, Hashable, Codable, Sendable { case strings case stringsCatalog case assets diff --git a/Sources/XcodeGraph/Models/SDKSource.swift b/Sources/XcodeGraph/Models/SDKSource.swift index 2c52a66e..b0d12fce 100644 --- a/Sources/XcodeGraph/Models/SDKSource.swift +++ b/Sources/XcodeGraph/Models/SDKSource.swift @@ -1,6 +1,6 @@ import Foundation -public enum SDKSource: String, Equatable, Codable { +public enum SDKSource: String, Equatable, Codable, Sendable { case developer // Platforms/iPhoneOS.platform/Developer/Library case system // Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library diff --git a/Sources/XcodeGraph/Models/TestableTarget.swift b/Sources/XcodeGraph/Models/TestableTarget.swift index aa01d488..dfb21aa6 100644 --- a/Sources/XcodeGraph/Models/TestableTarget.swift +++ b/Sources/XcodeGraph/Models/TestableTarget.swift @@ -76,7 +76,7 @@ public struct TestableTarget: Equatable, Hashable, Codable, Sendable { TestableTarget( target: target, skipped: skipped, - parallelizable: parallelizable, + parallelization: parallelizable ? .all : .none, randomExecutionOrdering: randomExecutionOrdering, simulatedLocation: simulatedLocation ) diff --git a/Sources/XcodeGraph/Models/XCFrameworkInfoPlist.swift b/Sources/XcodeGraph/Models/XCFrameworkInfoPlist.swift index dadd42f3..5d0377b2 100644 --- a/Sources/XcodeGraph/Models/XCFrameworkInfoPlist.swift +++ b/Sources/XcodeGraph/Models/XCFrameworkInfoPlist.swift @@ -2,14 +2,14 @@ import Foundation import Path /// It represents th Info.plist contained in an .xcframework bundle. -public struct XCFrameworkInfoPlist: Codable, Hashable, Equatable { +public struct XCFrameworkInfoPlist: Codable, Hashable, Equatable, Sendable { private enum CodingKeys: String, CodingKey { case libraries = "AvailableLibraries" } /// It represents a library inside an .xcframework - public struct Library: Codable, Hashable, Equatable { - public enum Platform: String, CaseIterable, Codable { + public struct Library: Codable, Hashable, Equatable, Sendable { + public enum Platform: String, CaseIterable, Codable, Sendable { case iOS = "ios" case macOS = "macos" case tvOS = "tvos" @@ -78,6 +78,10 @@ public struct XCFrameworkInfoPlist: Codable, Hashable, Equatable { /// List of libraries that are part of the .xcframework. public let libraries: [Library] + + public init(libraries: [Library]) { + self.libraries = libraries + } } #if DEBUG diff --git a/Sources/XcodeMetadata/Extensions/BinaryArchitecture+Extension.swift b/Sources/XcodeMetadata/Extensions/BinaryArchitecture+Extension.swift new file mode 100644 index 00000000..513f75c3 --- /dev/null +++ b/Sources/XcodeMetadata/Extensions/BinaryArchitecture+Extension.swift @@ -0,0 +1,87 @@ +import Foundation +#if canImport(MachO) + import MachO +#else + import MachOKitC +#endif +import XcodeGraph + +extension BinaryArchitecture { + /// An array of `(cpu_type_t, cpu_subtype_t)` pairs representing this architecture. + private var pairs: [(cpu_type_t, cpu_subtype_t)] { + switch self { + case .x8664: + return [ + (CPU_TYPE_X86_64, CPU_SUBTYPE_X86_64_ALL), + (CPU_TYPE_X86_64, CPU_SUBTYPE_X86_64_H), + ] + case .i386: + return [ + (CPU_TYPE_X86, CPU_SUBTYPE_X86_ALL), + ] + case .armv7: + return [ + (CPU_TYPE_ARM, CPU_SUBTYPE_ARM_V7), + ] + case .armv7s: + return [ + (CPU_TYPE_ARM, CPU_SUBTYPE_ARM_V7S), + ] + case .armv7k: + return [ + (CPU_TYPE_ARM, CPU_SUBTYPE_ARM_V7K), + ] + case .arm64: + return [ + (CPU_TYPE_ARM64, CPU_SUBTYPE_ARM64_ALL), + ] + case .arm64e: + return [ + (CPU_TYPE_ARM64, CPU_SUBTYPE_ARM64E), + ] + case .arm6432: + return [ + (CPU_TYPE_ARM64_32, CPU_SUBTYPE_ARM64_32_ALL), + (CPU_TYPE_ARM64_32, CPU_SUBTYPE_ARM64_32_V8), + ] + } + } + + /// Builds a single dictionary mapping `(cputype, cpusubtype)` to `BinaryArchitecture`. + /// + /// This is computed once by enumerating all enum cases and collecting their pairs. + private static let architectureMap: [CPUIdentifier: BinaryArchitecture] = { + var map = [CPUIdentifier: BinaryArchitecture]() + for arch in Self.allCases { + for (cputype, subtype) in arch.pairs { + map[CPUIdentifier(cputype: cputype, cpusubtype: subtype)] = arch + } + } + return map + }() + + /// Initializes a `BinaryArchitecture` from `(cputype, cpusubtype)`. + /// + /// If not found in `architectureMap`, returns `nil`. + public init?(cputype: cpu_type_t, subtype: cpu_subtype_t) { + let key = CPUIdentifier(cputype: cputype, cpusubtype: subtype) + // CPU_SUBTYPE_ANY is not available in MachOKitC + #if canImport(MachO) + let fallbackKey = CPUIdentifier(cputype: cputype, cpusubtype: CPU_SUBTYPE_ANY) + guard let architecture = Self.architectureMap[key] ?? Self.architectureMap[fallbackKey] else { + return nil + } + #else + guard let architecture = Self.architectureMap[key] else { + return nil + } + #endif + self = architecture + } +} + +/// A small Hashable struct to store `(cputype, cpusubtype)` pairs as dictionary keys. +private struct CPUIdentifier: Hashable, Sendable { + let cputype: cpu_type_t + let cpusubtype: cpu_subtype_t +} diff --git a/Sources/XcodeMetadata/Extensions/FileHandle+ReadHelpers.swift b/Sources/XcodeMetadata/Extensions/FileHandle+ReadHelpers.swift new file mode 100644 index 00000000..d44a6ed9 --- /dev/null +++ b/Sources/XcodeMetadata/Extensions/FileHandle+ReadHelpers.swift @@ -0,0 +1,28 @@ +import Foundation + +extension FileHandle { + /// Returns the current offset in the file. + var currentOffset: UInt64 { + offsetInFile + } + + /// Seeks to a specific file offset. + func seek(to offset: UInt64) { + seek(toFileOffset: offset) + } + + /// Reads a value of type `T` from the file handle. + /// - Returns: The value `T` loaded from the next `MemoryLayout.size` bytes. + func read() -> T { + let data = readData(ofLength: MemoryLayout.size) + return data.withUnsafeBytes { $0.load(as: T.self) } + } + + /// Reads a string of a specified length using ASCII encoding. + /// - Parameter length: The number of bytes to read. + /// - Returns: A `String` if decoding succeeds, otherwise `nil`. + func readString(ofLength length: Int) -> String? { + let data = readData(ofLength: length) + return String(data: data, encoding: .ascii) + } +} diff --git a/Sources/XcodeMetadata/Extensions/MachOByteSwapExtensions.swift b/Sources/XcodeMetadata/Extensions/MachOByteSwapExtensions.swift new file mode 100644 index 00000000..17f632c9 --- /dev/null +++ b/Sources/XcodeMetadata/Extensions/MachOByteSwapExtensions.swift @@ -0,0 +1,69 @@ +import Foundation +#if canImport(MachO) + import MachO +#else + import MachOKitC +#endif + +extension fat_header { + mutating func swapIfNeeded(_ shouldSwap: Bool) { + guard shouldSwap else { return } + magic = magic.byteSwapped + nfat_arch = nfat_arch.byteSwapped + } +} + +extension fat_arch { + mutating func swapIfNeeded(_ shouldSwap: Bool) { + guard shouldSwap else { return } + cputype = cputype.byteSwapped + cpusubtype = cpusubtype.byteSwapped + offset = offset.byteSwapped + size = size.byteSwapped + align = align.byteSwapped + } +} + +extension mach_header { + mutating func swapIfNeeded(_ shouldSwap: Bool) { + guard shouldSwap else { return } + magic = magic.byteSwapped + cputype = cputype.byteSwapped + cpusubtype = cpusubtype.byteSwapped + filetype = filetype.byteSwapped + ncmds = ncmds.byteSwapped + sizeofcmds = sizeofcmds.byteSwapped + flags = flags.byteSwapped + } +} + +extension mach_header_64 { + mutating func swapIfNeeded(_ shouldSwap: Bool) { + guard shouldSwap else { return } + magic = magic.byteSwapped + cputype = cputype.byteSwapped + cpusubtype = cpusubtype.byteSwapped + filetype = filetype.byteSwapped + ncmds = ncmds.byteSwapped + sizeofcmds = sizeofcmds.byteSwapped + flags = flags.byteSwapped + reserved = reserved.byteSwapped + } +} + +extension load_command { + mutating func swapIfNeeded(_ shouldSwap: Bool) { + guard shouldSwap else { return } + cmd = cmd.byteSwapped + cmdsize = cmdsize.byteSwapped + } +} + +extension uuid_command { + mutating func swapIfNeeded(_ shouldSwap: Bool) { + guard shouldSwap else { return } + cmd = cmd.byteSwapped + cmdsize = cmdsize.byteSwapped + // The uuid field is 16 raw bytes; no integer fields to swap for endianness. + } +} diff --git a/Sources/XcodeMetadata/Extensions/Sequence+ExecutionContext.swift b/Sources/XcodeMetadata/Extensions/Sequence+ExecutionContext.swift new file mode 100644 index 00000000..f8cb22b9 --- /dev/null +++ b/Sources/XcodeMetadata/Extensions/Sequence+ExecutionContext.swift @@ -0,0 +1,63 @@ +import Foundation + +extension Sequence where Element: Sendable { + /// Taken from: https://github.com/JohnSundell/CollectionConcurrencyKit/blob/b4f23e24b5a1bff301efc5e70871083ca029ff95/Sources/CollectionConcurrencyKit.swift + /// Transform the sequence into an array of new values using + /// an async closure that returns optional values. Only the + /// non-`nil` return values will be included in the new array. + /// + /// The closure calls will be performed in order, by waiting for + /// each call to complete before proceeding with the next one. If + /// any of the closure calls throw an error, then the iteration + /// will be terminated and the error rethrown. + /// + /// - parameter transform: The transform to run on each element. + /// - returns: The transformed values as an array. The order of + /// the transformed values will match the original sequence, + /// except for the values that were transformed into `nil`. + /// - throws: Rethrows any error thrown by the passed closure. + public func serialCompactMap( + _ transform: (Element) async throws -> T? + ) async rethrows -> [T] { + var values = [T]() + + for element in self { + guard let value = try await transform(element) else { + continue + } + + values.append(value) + } + + return values + } + + /// Filter (with execution context) + /// + /// - Parameters: + /// - context: The execution context to perform the `perform` operation with + /// - perform: The perform closure to call on each element in the array + func concurrentFilter(_ filter: @Sendable @escaping (Element) async throws -> Bool) async rethrows -> [Element] { + return try await concurrentCompactMap { + try await filter($0) ? $0 : nil + } + } + + /// Async concurrent compact map + /// + /// - Parameters: + /// - transform: The transformation closure to apply to the array + func concurrentCompactMap(_ transform: @Sendable @escaping (Element) async throws -> B?) async rethrows + -> [B] + { + let tasks = map { element in + Task { + try await transform(element) + } + } + + return try await tasks.serialCompactMap { task in + try await task.value + } + } +} diff --git a/Sources/XcodeMetadata/LoggerServiceContextKey.swift b/Sources/XcodeMetadata/LoggerServiceContextKey.swift new file mode 100644 index 00000000..6d17f7f3 --- /dev/null +++ b/Sources/XcodeMetadata/LoggerServiceContextKey.swift @@ -0,0 +1,16 @@ +import Logging +import ServiceContextModule + +private enum LoggerServiceContextKey: ServiceContextKey { + typealias Value = Logger +} + +extension ServiceContext { + var logger: Logger? { + get { + self[LoggerServiceContextKey.self] + } set { + self[LoggerServiceContextKey.self] = newValue + } + } +} diff --git a/Sources/XcodeMetadata/Providers/FrameworkMetadataProvider.swift b/Sources/XcodeMetadata/Providers/FrameworkMetadataProvider.swift new file mode 100644 index 00000000..acba747d --- /dev/null +++ b/Sources/XcodeMetadata/Providers/FrameworkMetadataProvider.swift @@ -0,0 +1,104 @@ +@preconcurrency import FileSystem +import Foundation +import Path +import XcodeGraph + +// MARK: - Provider Errors + +enum FrameworkMetadataProviderError: LocalizedError, Equatable { + case frameworkNotFound(AbsolutePath) + + // MARK: - FatalError + + var errorDescription: String? { + switch self { + case let .frameworkNotFound(path): + return "Couldn't find framework at \(path.pathString)" + } + } +} + +// MARK: - Provider + +public protocol FrameworkMetadataProviding: PrecompiledMetadataProviding { + /// Loads all the metadata associated with a framework at the specified path + /// - Note: This performs various shell calls and disk operations + func loadMetadata(at path: AbsolutePath, status: LinkingStatus) async throws -> FrameworkMetadata + + /// Given the path to a framework, it returns the path to its dSYMs if they exist + /// in the same framework directory. + /// - Parameter frameworkPath: Path to the .framework directory. + func dsymPath(frameworkPath: AbsolutePath) async throws -> AbsolutePath? + + /// Given the path to a framework, it returns the list of .bcsymbolmap files that + /// are associated to the framework and that are present in the same directory. + /// - Parameter frameworkPath: Path to the .framework directory. + func bcsymbolmapPaths(frameworkPath: AbsolutePath) async throws -> [AbsolutePath] + + /// Returns the product for the framework at the given path. + /// - Parameter frameworkPath: Path to the .framework directory. + func product(frameworkPath: AbsolutePath) throws -> Product +} + +// MARK: - Default Implementation + +public final class FrameworkMetadataProvider: PrecompiledMetadataProvider, FrameworkMetadataProviding { + private let fileSystem: FileSysteming + + public init( + fileSystem: FileSysteming = FileSystem() + ) { + self.fileSystem = fileSystem + super.init() + } + + public func loadMetadata(at path: AbsolutePath, status: LinkingStatus) async throws -> FrameworkMetadata { + guard try await fileSystem.exists(path) else { + throw FrameworkMetadataProviderError.frameworkNotFound(path) + } + let binaryPath = binaryPath(frameworkPath: path) + let dsymPath = try await dsymPath(frameworkPath: path) + let bcsymbolmapPaths = try await bcsymbolmapPaths(frameworkPath: path) + let linking = try linking(binaryPath: binaryPath) + let architectures = try architectures(binaryPath: binaryPath) + return FrameworkMetadata( + path: path, + binaryPath: binaryPath, + dsymPath: dsymPath, + bcsymbolmapPaths: bcsymbolmapPaths, + linking: linking, + architectures: architectures, + status: status + ) + } + + public func dsymPath(frameworkPath: AbsolutePath) async throws -> AbsolutePath? { + let path = try AbsolutePath(validating: "\(frameworkPath.pathString).dSYM") + if try await fileSystem.exists(path) { return path } + return nil + } + + public func bcsymbolmapPaths(frameworkPath: AbsolutePath) async throws -> [AbsolutePath] { + let binaryPath = binaryPath(frameworkPath: frameworkPath) + let uuids = try uuids(binaryPath: binaryPath) + let fileSystem = fileSystem + return try await uuids + .map { frameworkPath.parentDirectory.appending(component: "\($0).bcsymbolmap") } + .concurrentFilter { try await fileSystem.exists($0) } + .sorted() + } + + public func product(frameworkPath: AbsolutePath) throws -> Product { + let binaryPath = binaryPath(frameworkPath: frameworkPath) + switch try linking(binaryPath: binaryPath) { + case .dynamic: + return .framework + case .static: + return .staticFramework + } + } + + private func binaryPath(frameworkPath: AbsolutePath) -> AbsolutePath { + frameworkPath.appending(component: frameworkPath.basenameWithoutExt) + } +} diff --git a/Sources/XcodeMetadata/Providers/LibraryMetadataProvider.swift b/Sources/XcodeMetadata/Providers/LibraryMetadataProvider.swift new file mode 100644 index 00000000..984e42ee --- /dev/null +++ b/Sources/XcodeMetadata/Providers/LibraryMetadataProvider.swift @@ -0,0 +1,77 @@ +import FileSystem +import Foundation +import Path +import XcodeGraph + +// MARK: - Provider Errors + +enum LibraryMetadataProviderError: LocalizedError, Equatable { + case libraryNotFound(AbsolutePath) + case publicHeadersNotFound(libraryPath: AbsolutePath, headersPath: AbsolutePath) + case swiftModuleMapNotFound(libraryPath: AbsolutePath, moduleMapPath: AbsolutePath) + + // MARK: - FatalError + + var errorDescription: String? { + switch self { + case let .libraryNotFound(path): + return "Couldn't find library at \(path.pathString)" + case let .publicHeadersNotFound(libraryPath: libraryPath, headersPath: headersPath): + return "Couldn't find the public headers at \(headersPath.pathString) for library \(libraryPath.pathString)" + case let .swiftModuleMapNotFound(libraryPath: libraryPath, moduleMapPath: moduleMapPath): + return "Couldn't find the public headers at \(moduleMapPath.pathString) for library \(libraryPath.pathString)" + } + } +} + +// MARK: - Provider + +public protocol LibraryMetadataProviding: PrecompiledMetadataProviding { + /// Loads all the metadata associated with a library (.a / .dylib) at the specified path + /// - Note: This performs various shell calls and disk operations + func loadMetadata( + at path: AbsolutePath, + publicHeaders: AbsolutePath, + swiftModuleMap: AbsolutePath? + ) async throws -> LibraryMetadata +} + +// MARK: - Default Implementation + +public final class LibraryMetadataProvider: PrecompiledMetadataProvider, LibraryMetadataProviding { + private let fileSystem: FileSysteming + + public init( + fileSystem: FileSysteming = FileSystem() + ) { + self.fileSystem = fileSystem + } + + public func loadMetadata( + at path: AbsolutePath, + publicHeaders: AbsolutePath, + swiftModuleMap: AbsolutePath? + ) async throws -> LibraryMetadata { + guard try await fileSystem.exists(path) else { + throw LibraryMetadataProviderError.libraryNotFound(path) + } + guard try await fileSystem.exists(publicHeaders) else { + throw LibraryMetadataProviderError.publicHeadersNotFound(libraryPath: path, headersPath: publicHeaders) + } + if let swiftModuleMap { + guard try await fileSystem.exists(swiftModuleMap) else { + throw LibraryMetadataProviderError.swiftModuleMapNotFound(libraryPath: path, moduleMapPath: swiftModuleMap) + } + } + + let architectures = try architectures(binaryPath: path) + let linking = try linking(binaryPath: path) + return LibraryMetadata( + path: path, + publicHeaders: publicHeaders, + swiftModuleMap: swiftModuleMap, + architectures: architectures, + linking: linking + ) + } +} diff --git a/Sources/XcodeMetadata/Providers/PrecompiledMetadataProvider.swift b/Sources/XcodeMetadata/Providers/PrecompiledMetadataProvider.swift new file mode 100644 index 00000000..c5418478 --- /dev/null +++ b/Sources/XcodeMetadata/Providers/PrecompiledMetadataProvider.swift @@ -0,0 +1,241 @@ +import Foundation +#if canImport(MachO) + import MachO +#else + import MachOKitC +#endif +import Path +import XcodeGraph + +// swiftlint:disable identifier_name +private let CPU_SUBTYPE_MASK = Int32(bitPattern: 0xFF00_0000) + +// MARK: - Errors + +enum PrecompiledMetadataProviderError: LocalizedError, Equatable { + case architecturesNotFound(AbsolutePath) + case metadataNotFound(AbsolutePath) + + // MARK: - FatalError + + var errorDescription: String? { + switch self { + case let .architecturesNotFound(path): + return "Couldn't find architectures for binary at path \(path.pathString)" + case let .metadataNotFound(path): + return "Couldn't find metadata for binary at path \(path.pathString)" + } + } +} + +// MARK: - Protocol + +public protocol PrecompiledMetadataProviding { + /// Returns the supported architectures of the binary at the given path. + func architectures(binaryPath: AbsolutePath) throws -> [BinaryArchitecture] + + /// Returns how other binaries should link the binary at the given path (.dynamic or .static). + func linking(binaryPath: AbsolutePath) throws -> BinaryLinking + + /// Uses 'dwarfdump' logic to find UUIDs for each arch (helps match .bcsymbolmap files). + func uuids(binaryPath: AbsolutePath) throws -> Set +} + +// MARK: - PrecompiledMetadataProvider + +/// Reads Mach-O metadata (arches, linking type, UUIDs) without calling deprecated swap_* APIs. +public class PrecompiledMetadataProvider: PrecompiledMetadataProviding { + // A local struct for arch/linking/UUID data + typealias Metadata = (BinaryArchitecture, BinaryLinking, UUID?) + + public func architectures(binaryPath: AbsolutePath) throws -> [BinaryArchitecture] { + let metadata = try readMetadatas(binaryPath: binaryPath) + return metadata.map(\.0) + } + + public func linking(binaryPath: AbsolutePath) throws -> BinaryLinking { + let metadata = try readMetadatas(binaryPath: binaryPath) + // If *any* arch is dynamic, the overall binary is dynamic. + return metadata.contains(where: { $0.1 == .dynamic }) ? .dynamic : .static + } + + public func uuids(binaryPath: AbsolutePath) throws -> Set { + let metadata = try readMetadatas(binaryPath: binaryPath) + return Set(metadata.compactMap(\.2)) + } + + // MARK: - Internal + + private let sizeOfArchiveHeader: UInt64 = 60 + private let archiveHeaderSizeOffset: UInt64 = 56 + private let archiveFormatMagic = "!\n" + private let archiveExtendedFormat = "#1/" + + /// Reads all arch/linking/UUID info for the given binary (whether fat or thin). + func readMetadatas(binaryPath: AbsolutePath) throws -> [Metadata] { + guard let binary = FileHandle(forReadingAtPath: binaryPath.pathString) else { + throw PrecompiledMetadataProviderError.metadataNotFound(binaryPath) + } + defer { binary.closeFile() } + + // Peek at magic + let magic: UInt32 = binary.read() + // Reset to start + binary.seek(to: 0) + + if isFat(magic) { + return try readMetadatasFromFatHeader(binary: binary, magic: magic, binaryPath: binaryPath) + } else if let singleMetadata = try readMetadataFromMachHeaderIfAvailable(binary: binary) { + return [singleMetadata] + } else { + throw PrecompiledMetadataProviderError.metadataNotFound(binaryPath) + } + } + + private func readMetadatasFromFatHeader( + binary: FileHandle, + magic: UInt32, + binaryPath: AbsolutePath + ) throws -> [Metadata] { + var header: fat_header = binary.read() + header.swapIfNeeded(shouldSwap(magic)) + + return try (0 ..< header.nfat_arch).map { _ in + var fatArch: fat_arch = binary.read() + fatArch.swapIfNeeded(shouldSwap(magic)) + + let savedOffset = binary.currentOffset + // Jump to that arch offset + binary.seek(to: UInt64(fatArch.offset)) + + // Attempt to parse Mach-O data + let maybeMetadata = try readMetadataFromMachHeaderIfAvailable(binary: binary) + // Restore offset + binary.seek(to: savedOffset) + + if let metadata = maybeMetadata { + return metadata + } else { + let maskedSubtype = fatArch.cpusubtype & ~CPU_SUBTYPE_MASK // 0x00000002 + + // If we cannot parse Mach-O, fallback to static if cputype is known + guard let arch = readBinaryArchitecture( + cputype: fatArch.cputype, + cpusubtype: maskedSubtype + ) else { + throw PrecompiledMetadataProviderError.architecturesNotFound(binaryPath) + } + return (arch, .static, nil) + } + } + } + + private func readMetadataFromMachHeaderIfAvailable(binary: FileHandle) throws -> Metadata? { + readArchiveFormatIfAvailable(binary) + + let currentOffset = binary.currentOffset + let magic: UInt32 = binary.read() + binary.seek(to: currentOffset) + + guard isMagic(magic) else { + return nil + } + + let (cputype, cpusubtype, filetype, ncmds) = try readMachHeader(binary: binary, magic: magic) + guard let arch = readBinaryArchitecture(cputype: cputype, cpusubtype: cpusubtype) else { + return nil + } + + var foundUUID: UUID? + for _ in 0 ..< ncmds { + let cmdStart = binary.currentOffset + var loadCmd: load_command = binary.read() + loadCmd.swapIfNeeded(shouldSwap(magic)) + + guard loadCmd.cmd == LC_UUID else { + binary.seek(to: cmdStart + UInt64(loadCmd.cmdsize)) + continue + } + + // re-read the entire uuid_command + binary.seek(to: cmdStart) + var uuidCmd: uuid_command = binary.read() + uuidCmd.swapIfNeeded(shouldSwap(magic)) + + foundUUID = UUID(uuid: uuidCmd.uuid) + break + } + + let linking: BinaryLinking = (filetype == MH_DYLIB) ? .dynamic : .static + return (arch, linking, foundUUID) + } + + private func readMachHeader( + binary: FileHandle, + magic: UInt32 + ) throws -> (cpu_type_t, cpu_subtype_t, UInt32, UInt32) { + if is64(magic) { + var header64: mach_header_64 = binary.read() + header64.swapIfNeeded(shouldSwap(magic)) + return (header64.cputype, header64.cpusubtype, header64.filetype, header64.ncmds) + } else { + var header32: mach_header = binary.read() + header32.swapIfNeeded(shouldSwap(magic)) + return (header32.cputype, header32.cpusubtype, header32.filetype, header32.ncmds) + } + } + + private func readArchiveFormatIfAvailable(_ binary: FileHandle) { + let currentOffset = binary.currentOffset + let data = binary.readData(ofLength: 8) + binary.seek(to: currentOffset) + + guard let magicStr = String(data: data, encoding: .ascii), + magicStr == archiveFormatMagic + else { return } + + binary.seek(to: currentOffset + archiveHeaderSizeOffset) + guard let sizeString = binary.readString(ofLength: 10) else { return } + + let size = strtoul(sizeString, nil, 10) + // skip the archive header + binary.seek(to: 8 + sizeOfArchiveHeader + UInt64(size)) + + guard let name = binary.readString(ofLength: 16) else { return } + binary.seek(to: binary.currentOffset - 16) + + if name.hasPrefix(archiveExtendedFormat) { + let nameSize = strtoul(String(name.dropFirst(3)), nil, 10) + binary.seek(to: binary.currentOffset + sizeOfArchiveHeader + UInt64(nameSize)) + } else { + binary.seek(to: binary.currentOffset + sizeOfArchiveHeader) + } + } + + // MARK: - Architecture Mapping + + private func readBinaryArchitecture(cputype: cpu_type_t, cpusubtype: cpu_subtype_t) -> BinaryArchitecture? { + BinaryArchitecture(cputype: cputype, subtype: cpusubtype) + } + + // MARK: - Helpers + + private func isMagic(_ magic: UInt32) -> Bool { + [MH_MAGIC, MH_MAGIC_64, MH_CIGAM, MH_CIGAM_64, FAT_MAGIC, FAT_CIGAM].contains(magic) + } + + private func isFat(_ magic: UInt32) -> Bool { + [FAT_MAGIC, FAT_CIGAM].contains(magic) + } + + private func is64(_ magic: UInt32) -> Bool { + [MH_MAGIC_64, MH_CIGAM_64].contains(magic) + } + + /// If magic is CIGAM or FAT_CIGAM, it's big-endian -> we need to byte-swap + private func shouldSwap(_ magic: UInt32) -> Bool { + [MH_CIGAM, MH_CIGAM_64, FAT_CIGAM].contains(magic) + } +} + +// swiftlint:enable identifier_name diff --git a/Sources/XcodeMetadata/Providers/SystemFrameworkMetadataProvider.swift b/Sources/XcodeMetadata/Providers/SystemFrameworkMetadataProvider.swift new file mode 100644 index 00000000..f69144df --- /dev/null +++ b/Sources/XcodeMetadata/Providers/SystemFrameworkMetadataProvider.swift @@ -0,0 +1,96 @@ +import Foundation +import Path +import XcodeGraph + +// MARK: - Provider Errors + +public enum SystemFrameworkMetadataProviderError: LocalizedError, Equatable { + case unsupportedSDK(name: String) + + public var errorDescription: String? { + switch self { + case let .unsupportedSDK(sdk): + let supportedTypes = SDKType.supportedTypesDescription + return "The SDK type of \(sdk) is not currently supported - only \(supportedTypes) are supported." + } + } +} + +// MARK: - Provider + +public protocol SystemFrameworkMetadataProviding { + func loadMetadata(sdkName: String, status: LinkingStatus, platform: Platform, source: SDKSource) throws + -> SystemFrameworkMetadata +} + +extension SystemFrameworkMetadataProviding { + func loadXCTestMetadata(platform: Platform) throws -> SystemFrameworkMetadata { + try loadMetadata(sdkName: "XCTest.framework", status: .required, platform: platform, source: .developer) + } +} + +// MARK: - Default Implementation + +public final class SystemFrameworkMetadataProvider: SystemFrameworkMetadataProviding { + public init() {} + + public func loadMetadata( + sdkName: String, + status: LinkingStatus, + platform: Platform, + source: SDKSource + ) throws -> SystemFrameworkMetadata { + let sdkNamePath = try AbsolutePath(validating: "/\(sdkName)") + guard let sdkExtension = sdkNamePath.extension + else { throw SystemFrameworkMetadataProviderError.unsupportedSDK(name: sdkName) } + + let sdkType: SDKType + switch sdkExtension { + case "framework": + sdkType = .framework + case "tbd": + if sdkName.starts(with: "libswift") { + sdkType = .swiftLibrary + } else { + sdkType = .library + } + default: + throw SystemFrameworkMetadataProviderError.unsupportedSDK(name: sdkName) + } + + let path = try sdkPath(name: sdkName, platform: platform, type: sdkType, source: source) + return SystemFrameworkMetadata( + name: sdkName, + path: path, + status: status, + source: source + ) + } + + private func sdkPath(name: String, platform: Platform, type: SDKType, source: SDKSource) throws -> AbsolutePath { + switch source { + case .developer: + let xcodeDeveloperSdkRootPath = platform.xcodeDeveloperSdkRootPath + let sdkRootPath = try AbsolutePath(validating: "/\(xcodeDeveloperSdkRootPath)") + return sdkRootPath + .appending(try RelativePath(validating: "Frameworks")) + .appending(component: name) + + case .system: + let sdkRootPath = try AbsolutePath(validating: "/\(platform.xcodeSdkRootPath)") + switch type { + case .framework: + return sdkRootPath + .appending(try RelativePath(validating: "System/Library/Frameworks")) + .appending(component: name) + case .library: + return sdkRootPath + .appending(try RelativePath(validating: "usr/lib")) + .appending(component: name) + case .swiftLibrary: + return sdkRootPath + .appending(components: "usr", "lib", "swift", name) + } + } + } +} diff --git a/Sources/XcodeMetadata/Providers/XCFrameworkMetadataProvider.swift b/Sources/XcodeMetadata/Providers/XCFrameworkMetadataProvider.swift new file mode 100644 index 00000000..8cc8149e --- /dev/null +++ b/Sources/XcodeMetadata/Providers/XCFrameworkMetadataProvider.swift @@ -0,0 +1,196 @@ +import FileSystem +import Foundation +import Mockable +import Path +import ServiceContextModule +import XcodeGraph + +// MARK: - Provider Errors + +enum XCFrameworkMetadataProviderError: LocalizedError, Equatable { + case xcframeworkNotFound(AbsolutePath) + case missingRequiredFile(AbsolutePath) + case supportedArchitectureReferencesNotFound(AbsolutePath) + case fileTypeNotRecognised(file: RelativePath, frameworkName: String) + + // MARK: - FatalError + + var errorDescription: String? { + switch self { + case let .xcframeworkNotFound(path): + return "Couldn't find xcframework at \(path.pathString)" + case let .missingRequiredFile(path): + return + "The .xcframework at path \(path.pathString) doesn't contain an Info.plist. It's possible that the .xcframework was not generated properly or that got corrupted. Please, double check with the author of the framework." + case let .supportedArchitectureReferencesNotFound(path): + return + "Couldn't find any supported architecture references at \(path.pathString). It's possible that the .xcframework was not generated properly or that it got corrupted. Please, double check with the author of the framework." + case let .fileTypeNotRecognised(file, frameworkName): + return + "The extension of the file `\(file)`, which was found while parsing the xcframework `\(frameworkName)`, is not supported." + } + } +} + +// MARK: - Provider + +@Mockable +public protocol XCFrameworkMetadataProviding: PrecompiledMetadataProviding { + /// It returns the supported architectures of the binary at the given path. + /// - Parameter binaryPath: Binary path. + func architectures(binaryPath: AbsolutePath) throws -> [BinaryArchitecture] + + /// Return how other binaries should link the binary at the given path. + /// - Parameter binaryPath: Path to the binary. + func linking(binaryPath: AbsolutePath) throws -> BinaryLinking + + /// It uses 'dwarfdump' to dump the UUIDs of each architecture. + /// The UUIDs allows us to know which .bcsymbolmap files belong to this binary. + /// - Parameter binaryPath: Path to the binary. + func uuids(binaryPath: AbsolutePath) throws -> Set + + /// Loads all the metadata associated with an XCFramework at the specified path + /// - Note: This performs various shell calls and disk operations + func loadMetadata(at path: AbsolutePath, status: LinkingStatus) async throws + -> XCFrameworkMetadata + + /// Returns the info.plist of the xcframework at the given path. + /// - Parameter xcframeworkPath: Path to the xcframework. + func infoPlist(xcframeworkPath: AbsolutePath) async throws -> XCFrameworkInfoPlist +} + +// MARK: - Default Implementation + +public final class XCFrameworkMetadataProvider: PrecompiledMetadataProvider, + XCFrameworkMetadataProviding +{ + private let fileSystem: FileSysteming + + public init( + fileSystem: FileSysteming = FileSystem() + ) { + self.fileSystem = fileSystem + super.init() + } + + public func loadMetadata( + at path: AbsolutePath, + status: LinkingStatus + ) async throws -> XCFrameworkMetadata { + guard try await fileSystem.exists(path) else { + throw XCFrameworkMetadataProviderError.xcframeworkNotFound(path) + } + let infoPlist = try await infoPlist(xcframeworkPath: path) + let linking = try await linking( + xcframeworkPath: path, + libraries: infoPlist.libraries + ) + return XCFrameworkMetadata( + path: path, + infoPlist: infoPlist, + linking: linking, + mergeable: infoPlist.libraries.allSatisfy(\.mergeable), + status: status, + macroPath: try await macroPath(xcframeworkPath: path), + swiftModules: try await fileSystem.glob(directory: path, include: ["**/*.swiftmodule"]).collect().sorted(), + moduleMaps: try await fileSystem.glob(directory: path, include: ["**/*.modulemap"]).collect().sorted() + ) + } + + /** + An XCFramework that contains static frameworks that represent macros, those are frameworks with a Macros directory in them. + We assume that the Swift Macros, which are command line executables, are fat binaries for both architectures supported by macOS: + x86_64 and arm64. + */ + public func macroPath(xcframeworkPath: AbsolutePath) async throws -> AbsolutePath? { + guard let frameworkPath = try await fileSystem.glob(directory: xcframeworkPath, include: ["*/*.framework"]) + .collect() + .sorted() + .first + else { return nil } + guard let macroPath = try await fileSystem.glob(directory: frameworkPath, include: ["Macros/*"]).collect().first else { + return nil + } + return try AbsolutePath(validating: macroPath.pathString) + } + + public func infoPlist(xcframeworkPath: AbsolutePath) async throws -> XCFrameworkInfoPlist { + let infoPlist = xcframeworkPath.appending(component: "Info.plist") + guard try await fileSystem.exists(infoPlist) else { + throw XCFrameworkMetadataProviderError.missingRequiredFile(infoPlist) + } + + return try await fileSystem.readPlistFile(at: infoPlist) + } + + private func linking(xcframeworkPath: AbsolutePath, libraries: [XCFrameworkInfoPlist.Library]) + async throws -> BinaryLinking + { + let archs: [BinaryArchitecture] = [.arm64, .x8664] + + for library in libraries { + let hasValidArchitectures = !library.architectures.filter(archs.contains).isEmpty + guard hasValidArchitectures, + let linking = try? await linking(for: library, xcframeworkPath: xcframeworkPath) + else { + continue + } + + return linking + } + + throw XCFrameworkMetadataProviderError.supportedArchitectureReferencesNotFound( + xcframeworkPath + ) + } + + private func linking( + for library: XCFrameworkInfoPlist.Library, + xcframeworkPath: AbsolutePath + ) async throws -> BinaryLinking { + let (binaryPath, linking): (AbsolutePath, BinaryLinking?) + + switch library.path.extension { + case "framework": + binaryPath = try AbsolutePath( + validating: library.identifier, relativeTo: xcframeworkPath + ) + .appending(try RelativePath(validating: library.path.pathString)) + .appending(component: library.path.basenameWithoutExt) + linking = try? self.linking(binaryPath: binaryPath) + case "a": + binaryPath = try AbsolutePath( + validating: library.identifier, relativeTo: xcframeworkPath + ) + .appending(try RelativePath(validating: library.path.pathString)) + linking = .static + case "dylib": + binaryPath = try AbsolutePath( + validating: library.identifier, relativeTo: xcframeworkPath + ) + .appending(try RelativePath(validating: library.path.pathString)) + linking = .dynamic + default: + throw XCFrameworkMetadataProviderError.fileTypeNotRecognised( + file: library.path, + frameworkName: xcframeworkPath.basename + ) + } + + guard try await fileSystem.exists(binaryPath), let linking else { + // The missing slice relative to the XCFramework folder. e.g ios-x86_64-simulator/Alamofire.framework/Alamofire + let relativeArchitectureBinaryPath = binaryPath.components.suffix(3).joined( + separator: "/" + ) + ServiceContext.current?.logger? + .warning( + "\(xcframeworkPath.basename) is missing architecture \(relativeArchitectureBinaryPath) defined in the Info.plist" + ) + throw XCFrameworkMetadataProviderError.supportedArchitectureReferencesNotFound( + binaryPath + ) + } + + return linking + } +} diff --git a/Sources/XcodeProjMapper/Documentation.docc/XcodeProjMapper.md b/Sources/XcodeProjMapper/Documentation.docc/XcodeProjMapper.md new file mode 100644 index 00000000..0091e6da --- /dev/null +++ b/Sources/XcodeProjMapper/Documentation.docc/XcodeProjMapper.md @@ -0,0 +1,26 @@ +# ``XcodeProjMapper`` + +@Metadata { + @DisplayName("Xcode Project Mapper") + @TitleHeading("Documentation Portal") + @PageColor(purple) +} + +A tool that maps Xcode projects (`.xcodeproj` and `.xcworkspace`) into a structured, analyzable graph of projects, targets, dependencies, and build settings. This enables downstream tasks such as code generation, dependency analysis, and integration with custom tooling pipelines. + +## Overview + +``XcodeProjMapper`` leverages ``XcodeProj`` to parse and navigate Xcode project files, then translates them into a domain-specific graph model (``XcodeGraph/Graph``). This model captures all essential components—projects, targets, packages, dependencies, build settings, schemes, and more—providing a high-level, language-agnostic structure for further processing. + +By using this graph-based representation, you can easily analyze project configurations, visualize complex dependency graphs, or integrate advanced workflows into your build pipelines. For example, teams can leverage ``XcodeProjMapper`` to: + +- Generate code based on discovered resources and targets. +- Validate project configurations and detect missing bundle identifiers or invalid references. +- Explore dependencies between multiple projects and packages within a workspace. +- Automate repetitive tasks like scheme generation, resource synthesis, or compliance checks. + +## Topics + +### XcodeGraphMapper + +- ``XcodeGraphMapper`` diff --git a/Sources/XcodeProjMapper/Extensions/PBXProject+Extensions.swift b/Sources/XcodeProjMapper/Extensions/PBXProject+Extensions.swift new file mode 100644 index 00000000..c6cbe15e --- /dev/null +++ b/Sources/XcodeProjMapper/Extensions/PBXProject+Extensions.swift @@ -0,0 +1,11 @@ +import XcodeProj + +extension PBXProject { + /// Retrieves the value of a specific project attribute. + /// + /// - Parameter attr: The attribute key to look up. + /// - Returns: The value of the attribute if it exists, or `nil` if not found. + func attribute(for attr: ProjectAttribute) -> String? { + attributes[attr.rawValue] as? String + } +} diff --git a/Sources/XcodeProjMapper/Extensions/Package+Extensions.swift b/Sources/XcodeProjMapper/Extensions/Package+Extensions.swift new file mode 100644 index 00000000..e06e2354 --- /dev/null +++ b/Sources/XcodeProjMapper/Extensions/Package+Extensions.swift @@ -0,0 +1,13 @@ +import XcodeGraph + +extension Package { + /// Returns a URL or identifier for the package based on whether it's remote or local. + var url: String { + switch self { + case let .remote(url, _): + return url + case let .local(path): + return path.pathString + } + } +} diff --git a/Sources/XcodeProjMapper/Extensions/Platform+Extensions.swift b/Sources/XcodeProjMapper/Extensions/Platform+Extensions.swift new file mode 100644 index 00000000..14ef58c3 --- /dev/null +++ b/Sources/XcodeProjMapper/Extensions/Platform+Extensions.swift @@ -0,0 +1,28 @@ +import XcodeGraph + +extension Platform { + /// Initializes a `Platform` instance from an SDK root string (e.g., "iphoneos", "macosx"). + /// Returns `nil` if no matching platform is found. + init?(sdkroot: String) { + guard let platform = Platform.allCases.first(where: { $0.xcodeSdkRoot == sdkroot }) else { + return nil + } + self = platform + } + + /// Returns a set of `Destination` values supported by this platform. + var destinations: Destinations { + switch self { + case .iOS: + return [.iPad, .iPhone, .macCatalyst, .macWithiPadDesign, .appleVisionWithiPadDesign] + case .macOS: + return [.mac] + case .tvOS: + return [.appleTv] + case .watchOS: + return [.appleWatch] + case .visionOS: + return [.appleVision] + } + } +} diff --git a/Sources/XcodeProjMapper/Extensions/PlatformFilter+Extensions.swift b/Sources/XcodeProjMapper/Extensions/PlatformFilter+Extensions.swift new file mode 100644 index 00000000..d3906440 --- /dev/null +++ b/Sources/XcodeProjMapper/Extensions/PlatformFilter+Extensions.swift @@ -0,0 +1,25 @@ +import XcodeGraph + +extension PlatformFilter { + /// Initializes a `PlatformFilter` from a string that matches Xcodeproj values. + init?(rawValue: String) { + switch rawValue { + case PlatformFilter.ios.xcodeprojValue: + self = .ios + case PlatformFilter.macos.xcodeprojValue: + self = .macos + case PlatformFilter.tvos.xcodeprojValue: + self = .tvos + case PlatformFilter.catalyst.xcodeprojValue: + self = .catalyst + case PlatformFilter.driverkit.xcodeprojValue: + self = .driverkit + case PlatformFilter.watchos.xcodeprojValue: + self = .watchos + case PlatformFilter.visionos.xcodeprojValue: + self = .visionos + default: + return nil + } + } +} diff --git a/Sources/XcodeProjMapper/Extensions/TargetDependency+Extensions.swift b/Sources/XcodeProjMapper/Extensions/TargetDependency+Extensions.swift new file mode 100644 index 00000000..c92c1fce --- /dev/null +++ b/Sources/XcodeProjMapper/Extensions/TargetDependency+Extensions.swift @@ -0,0 +1,25 @@ +import XcodeGraph + +extension TargetDependency { + /// Extracts the name of the dependency for relevant cases, such as target, project, SDK, package, and libraries. + var name: String { + switch self { + case let .target(name, _, _): + return name + case let .project(target, _, _, _): + return target + case let .sdk(name, _, _): + return name + case let .package(product, _, _): + return product + case let .framework(path, _, _): + return path.basenameWithoutExt + case let .xcframework(path, _, _): + return path.basenameWithoutExt + case let .library(path, _, _, _): + return path.basenameWithoutExt + case .xctest: + return "xctest" + } + } +} diff --git a/Sources/XcodeProjMapper/Extensions/XCWorkspace+Extensions.swift b/Sources/XcodeProjMapper/Extensions/XCWorkspace+Extensions.swift new file mode 100644 index 00000000..e5811c85 --- /dev/null +++ b/Sources/XcodeProjMapper/Extensions/XCWorkspace+Extensions.swift @@ -0,0 +1,28 @@ +import Foundation +import Path +import XcodeProj + +// swiftlint:disable force_try +extension XCWorkspace { + /// A computed property that either returns the workspace’s `path` + public var workspacePath: AbsolutePath { + try! AbsolutePath(validating: path!.string) + } +} + +extension XcodeProj { + /// A computed property that either returns the project’s `path` + public var projectPath: AbsolutePath { + try! AbsolutePath(validating: path!.string) + } + + public var srcPath: AbsolutePath { + projectPath.parentDirectory + } + + public var srcPathString: String { + srcPath.pathString + } +} + +// swiftlint:enable force_try diff --git a/Sources/XcodeProjMapper/Extensions/XCWorkspaceDataFileRef+Extensions.swift b/Sources/XcodeProjMapper/Extensions/XCWorkspaceDataFileRef+Extensions.swift new file mode 100644 index 00000000..a525e9cc --- /dev/null +++ b/Sources/XcodeProjMapper/Extensions/XCWorkspaceDataFileRef+Extensions.swift @@ -0,0 +1,38 @@ +import Path +import XcodeProj + +extension XCWorkspaceDataFileRef { + /// Resolves the absolute path referenced by this `XCWorkspaceDataFileRef`. + /// + /// - Parameter srcPath: The workspace source root path. + /// - Returns: The resolved `AbsolutePath` of this file reference. + func path( + srcPath: AbsolutePath, + developerDirectoryProvider: DeveloperDirectoryProviding = DeveloperDirectoryProvider() + ) async throws -> AbsolutePath { + switch location { + case let .absolute(path): + return try AbsolutePath(validating: path) + case let .container(subPath): + let relativePath = try RelativePath(validating: subPath) + return srcPath.appending(relativePath) + case let .developer(subPath): + return try AbsolutePath( + validating: subPath, + relativeTo: try await developerDirectoryProvider.developerDirectory() + ) + case let .group(subPath): + // Group paths are relative to the workspace file itself + let relativePath = try RelativePath(validating: subPath) + return srcPath.appending(relativePath) + case let .current(subPath): + // Current paths are relative to the current directory + let relativePath = try RelativePath(validating: subPath) + return srcPath.appending(relativePath) + case let .other(type, subPath): + // Other path types: prefix with the type and append subpath + let relativePath = try RelativePath(validating: "\(type)/\(subPath)") + return srcPath.appending(relativePath) + } + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Graph/XcodeGraphMapper.swift b/Sources/XcodeProjMapper/Mappers/Graph/XcodeGraphMapper.swift new file mode 100644 index 00000000..8dc56b3b --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Graph/XcodeGraphMapper.swift @@ -0,0 +1,286 @@ +import FileSystem +import Foundation +import Path +import PathKit +import XcodeGraph +import XcodeProj + +/// A protocol defining how to map a given file path to a `Graph`. +public protocol XcodeGraphMapping { + /// Builds a `Graph` from the specified path. + /// - Parameter path: The absolute path to a `.xcworkspace`, `.xcodeproj`, or directory containing them. + /// - Returns: A `Graph` representing the projects, targets, and dependencies found at `pathString`. + /// - Throws: If the path doesn't exist or no projects are found. + func map(at path: AbsolutePath) async throws -> Graph +} + +/// An error type for `XcodeGraphMapper` when the path is invalid or no projects are found. +public enum XcodeGraphMapperError: LocalizedError { + case pathNotFound(String) + case noProjectsFound(String) + + public var errorDescription: String? { + switch self { + case let .pathNotFound(path): + return "The specified path does not exist: \(path)" + case let .noProjectsFound(path): + return "No `.xcworkspace` or `.xcodeproj` was found at: \(path)" + } + } +} + +/// Specifies whether we’re mapping a single `.xcodeproj` or an `.xcworkspace`. +enum XcodeMapperGraphType { + case workspace(XCWorkspace) + case project(XcodeProj) +} + +/// A unified entry point that locates `.xcworkspace` or `.xcodeproj` files—even within directories—and +/// constructs a comprehensive `Graph` of projects, targets, and dependencies. +/// +/// Specifically, this mapper: +/// 1. Detects whether the input path is a single project, a workspace, or a directory. +/// 2. Enumerates all discovered targets and dependencies to assemble the final `Graph`. +/// +/// This replaces old parsers/providers with a single approach. For example: +/// ```swift +/// let mapper: XcodeGraphMapping = XcodeGraphMapper() +/// let graph = try await mapper.map(at: "/path/to/MyApp") +/// ``` +public struct XcodeGraphMapper: XcodeGraphMapping { + private let fileSystem: FileSysteming + + // MARK: - Initialization + + public init(fileSystem: FileSysteming = FileSystem()) { + self.fileSystem = fileSystem + } + + // MARK: - Public API + + public func map(at path: AbsolutePath) async throws -> Graph { + guard try await fileSystem.exists(path) else { + throw XcodeGraphMapperError.pathNotFound(path.pathString) + } + + let graphType = try await determineGraphType(at: path) + return try await buildGraph(from: graphType) + } + + // MARK: - Determine Graph Type + + private func determineGraphType(at path: AbsolutePath) async throws -> XcodeMapperGraphType { + // Try a direct match for .xcworkspace / .xcodeproj + if let directType = try detectDirectGraphType(at: path) { + return directType + } + // Otherwise look inside the directory + return try await detectGraphTypeInDirectory(at: path) + } + + private func detectDirectGraphType(at path: AbsolutePath) throws -> XcodeMapperGraphType? { + guard let ext = path.extension?.lowercased() else { + return nil + } + + switch ext { + case "xcworkspace": + let xcworkspace = try XCWorkspace(path: Path(path.pathString)) + return .workspace(xcworkspace) + case "xcodeproj": + let xcodeProj = try XcodeProj(pathString: path.pathString) + return .project(xcodeProj) + default: + return nil + } + } + + private func detectGraphTypeInDirectory(at path: AbsolutePath) async throws -> XcodeMapperGraphType { + let patterns = ["**/*.xcworkspace", "**/*.xcodeproj"] + let contents = try fileSystem.glob(directory: path, include: patterns) + + if let workspacePath = try await contents.first(where: { $0.extension?.lowercased() == "xcworkspace" }) { + let xcworkspace = try XCWorkspace(path: Path(workspacePath.pathString)) + return .workspace(xcworkspace) + } + + if let projectPath = try await contents.first(where: { $0.extension?.lowercased() == "xcodeproj" }) { + let xcodeProj = try XcodeProj(pathString: projectPath.pathString) + return .project(xcodeProj) + } + + throw XcodeGraphMapperError.noProjectsFound(path.pathString) + } + + // MARK: - Build Graph + + func buildGraph(from graphType: XcodeMapperGraphType) async throws -> Graph { + let projectPaths = try await identifyProjectPaths(from: graphType) + let workspace = assembleWorkspace(graphType: graphType, projectPaths: projectPaths) + let projects = try await loadProjects(projectPaths) + let packages = extractPackages(from: projects) + let (dependencies, dependencyConditions) = try await resolveDependencies(for: projects) + + return assembleFinalGraph( + workspace: workspace, + projects: projects, + packages: packages, + dependencies: dependencies, + dependencyConditions: dependencyConditions + ) + } + + private func identifyProjectPaths(from graphType: XcodeMapperGraphType) async throws -> [AbsolutePath] { + switch graphType { + case let .workspace(xcworkspace): + return try await extractProjectPaths( + from: xcworkspace.data.children, + srcPath: xcworkspace.workspacePath.parentDirectory + ) + case let .project(xcodeProj): + return [xcodeProj.projectPath] + } + } + + private func assembleWorkspace( + graphType: XcodeMapperGraphType, + projectPaths: [AbsolutePath] + ) -> Workspace { + let workspacePath: AbsolutePath + let name: String + + switch graphType { + case let .workspace(xcworkspace): + workspacePath = xcworkspace.workspacePath + name = workspacePath.basenameWithoutExt + case let .project(xcodeProj): + workspacePath = xcodeProj.projectPath.parentDirectory + name = "Workspace" + } + + return Workspace( + path: workspacePath, + xcWorkspacePath: workspacePath, + name: name, + projects: projectPaths + ) + } + + private func loadProjects(_ projectPaths: [AbsolutePath]) async throws -> [AbsolutePath: Project] { + var projects = [AbsolutePath: Project]() + + for path in projectPaths { + let xcodeProj = try XcodeProj(pathString: path.pathString) + let projectMapper = PBXProjectMapper() + let project = try await projectMapper.map(xcodeProj: xcodeProj) + projects[path] = project + } + + return projects + } + + private func extractPackages( + from projects: [AbsolutePath: Project] + ) -> [AbsolutePath: [String: Package]] { + projects.compactMapValues { project in + guard !project.packages.isEmpty else { return nil } + return Dictionary( + uniqueKeysWithValues: project.packages.map { ($0.url, $0) } + ) + } + } + + private func resolveDependencies( + for projects: [AbsolutePath: Project] + ) async throws -> ([GraphDependency: Set], [GraphEdge: PlatformCondition]) { + let allTargetsMap = Dictionary( + projects.values.flatMap(\.targets), + uniquingKeysWith: { existing, _ in existing } + ) + return try await buildDependencies(for: projects, using: allTargetsMap) + } + + private func buildDependencies( + for projects: [AbsolutePath: Project], + using allTargetsMap: [String: Target] + ) async throws -> ([GraphDependency: Set], [GraphEdge: PlatformCondition]) { + var dependencies = [GraphDependency: Set]() + var dependencyConditions = [GraphEdge: PlatformCondition]() + + for (path, project) in projects { + for (name, target) in project.targets { + let sourceDependency = GraphDependency.target(name: name, path: path.parentDirectory) + + // Build edges for each target dependency + let edgesAndDeps = try await target.dependencies.serialCompactMap { (dep: TargetDependency) async throws -> ( + GraphEdge, + PlatformCondition?, + GraphDependency + ) in + let graphDep = try await dep.graphDependency( + sourceDirectory: path.parentDirectory, + allTargetsMap: allTargetsMap, + target: target + ) + return (GraphEdge(from: sourceDependency, to: graphDep), dep.condition, graphDep) + } + + // Update conditions dictionary + for (edge, condition, _) in edgesAndDeps { + if let condition { + dependencyConditions[edge] = condition + } + } + + // Update dependencies dictionary + let targetDeps = edgesAndDeps.map(\.2) + if !targetDeps.isEmpty { + dependencies[sourceDependency] = Set(targetDeps) + } + } + } + return (dependencies, dependencyConditions) + } + + private func assembleFinalGraph( + workspace: Workspace, + projects: [AbsolutePath: Project], + packages: [AbsolutePath: [String: Package]], + dependencies: [GraphDependency: Set], + dependencyConditions: [GraphEdge: PlatformCondition] + ) -> Graph { + Graph( + name: workspace.name, + path: workspace.path, + workspace: workspace, + projects: projects, + packages: packages, + dependencies: dependencies, + dependencyConditions: dependencyConditions + ) + } + + // MARK: - Project Path Extraction + + private func extractProjectPaths( + from elements: [XCWorkspaceDataElement], + srcPath: AbsolutePath + ) async throws -> [AbsolutePath] { + var paths: [AbsolutePath] = [] + + for element in elements { + switch element { + case let .file(ref): + let refPath = try await ref.path(srcPath: srcPath) + if refPath.extension == "xcodeproj" { + paths.append(refPath) + } + case let .group(group): + let nestedPaths = try await extractProjectPaths(from: group.children, srcPath: srcPath) + paths.append(contentsOf: nestedPaths) + } + } + + return paths + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Packages/XCPackageMapper.swift b/Sources/XcodeProjMapper/Mappers/Packages/XCPackageMapper.swift new file mode 100644 index 00000000..a3ddc4de --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Packages/XCPackageMapper.swift @@ -0,0 +1,78 @@ +import Foundation +import Path +import XcodeGraph +import XcodeProj + +/// Defines errors that may occur when mapping package references. +enum PackageMappingError: Error, LocalizedError, Equatable { + case missingRepositoryURL(packageName: String) + + var errorDescription: String? { + switch self { + case let .missingRepositoryURL(packageName): + return "The repository URL is missing for the package: \(packageName)." + } + } +} + +/// A protocol defining how to map remote and local Swift package references into `Package` models. +protocol PackageMapping { + /// Maps a remote Swift package reference to a `Package`. + /// + /// - Parameter package: The remote package reference. + /// - Returns: A `Package` representing the remote package. + /// - Throws: `PackageMappingError.missingRepositoryURL` if the package has no repository URL. + func map(package: XCRemoteSwiftPackageReference) throws -> Package + + /// Maps a local Swift package reference to a `Package`. + /// + /// - Parameters: + /// - package: The local Swift package reference. + /// - sourceDirectory: The project’s source directory used to resolve relative paths. + /// - Returns: A `Package` representing the local package. + /// - Throws: If the provided path is invalid and cannot be resolved. + func map(package: XCLocalSwiftPackageReference, sourceDirectory: AbsolutePath) throws -> Package +} + +/// A mapper that converts remote and local Swift package references into `Package` domain models. +struct XCPackageMapper: PackageMapping { + func map(package: XCRemoteSwiftPackageReference) throws -> Package { + guard let repositoryURL = package.repositoryURL else { + let name = package.name ?? "Unknown Package" + throw PackageMappingError.missingRepositoryURL(packageName: name) + } + let requirement = mapRequirement(package: package) + return .remote(url: repositoryURL, requirement: requirement) + } + + func map(package: XCLocalSwiftPackageReference, sourceDirectory: AbsolutePath) throws -> Package { + let relativePath = try RelativePath(validating: package.relativePath) + let path = sourceDirectory.appending(relativePath) + return .local(path: path) + } + + // MARK: - Private Helpers + + /// Determines the version requirement for a remote Swift package. + private func mapRequirement(package: XCRemoteSwiftPackageReference) -> Requirement { + guard let versionRequirement = package.versionRequirement else { + // Default to an all-zero version if none is specified + return .upToNextMajor("0.0.0") + } + + switch versionRequirement { + case let .upToNextMajorVersion(version): + return .upToNextMajor(version) + case let .upToNextMinorVersion(version): + return .upToNextMinor(version) + case let .exact(version): + return .exact(version) + case let .range(lowerBound, upperBound): + return .range(from: lowerBound, to: upperBound) + case let .branch(branch): + return .branch(branch) + case let .revision(revision): + return .revision(revision) + } + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Phases/BuildPhaseConstants.swift b/Sources/XcodeProjMapper/Mappers/Phases/BuildPhaseConstants.swift new file mode 100644 index 00000000..e51d16d0 --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Phases/BuildPhaseConstants.swift @@ -0,0 +1,46 @@ +import Foundation +import Path + +/// Constants related to various build phases and their default values. +enum BuildPhaseConstants { + /// The default name for a run script build phase if none is provided. + static let defaultScriptName = "Run Script" + /// The default shell path used by run script build phases. + static let defaultShellPath = "/bin/sh" + /// A placeholder name used when a shell script build phase has no name. + static let unnamedScriptPhase = "Unnamed Shell Script Phase" + /// The default name for a copy files build phase if none is provided. + static let copyFilesDefault = "Copy Files" +} + +/// Attributes indicating header visibility within a build target. +enum HeaderAttribute: String { + /// Indicates that a header is and can be exposed outside the module. + case `public` = "Public" + /// Indicates that a header is private and not exposed outside the module. + case `private` = "Private" +} + +/// Attributes related to code generation behavior for source files. +enum CodeGenAttribute: String { + /// Indicates that code generation is enabled publicly. + case `public` = "codegen" + /// Indicates that code generation is restricted to private scopes. + case `private` = "private_codegen" + /// Indicates that code generation is scoped to the project only. + case project = "project_codegen" + /// Indicates that code generation is disabled for the file. + case disabled = "no_codegen" +} + +/// Commonly referenced directory names within a project. +enum DirectoryName { + /// The directory used to store headers. + static let headers = "Headers" +} + +/// Attributes that can be assigned to build files in certain build phases. +enum BuildFileAttribute: String { + /// Indicates that the file should be code signed on copy during a copy files build phase. + case codeSignOnCopy = "CodeSignOnCopy" +} diff --git a/Sources/XcodeProjMapper/Mappers/Phases/PBXCopyFilesBuildPhaseMapper.swift b/Sources/XcodeProjMapper/Mappers/Phases/PBXCopyFilesBuildPhaseMapper.swift new file mode 100644 index 00000000..1c01c216 --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Phases/PBXCopyFilesBuildPhaseMapper.swift @@ -0,0 +1,80 @@ +import Foundation +import Path +import XcodeGraph +import XcodeProj + +/// A protocol for mapping `PBXCopyFilesBuildPhase` objects to `CopyFilesAction` models. +protocol PBXCopyFilesBuildPhaseMapping { + /// Maps the provided copy files phases to an array of `CopyFilesAction`. + /// - Parameters: + /// - copyFilesPhases: The build phases to map. + /// - xcodeProj: The `XcodeProj` containing project configuration and file references. + /// - Returns: An array of mapped `CopyFilesAction`s. + /// - Throws: If any file paths are invalid or cannot be resolved. + func map(_ copyFilesPhases: [PBXCopyFilesBuildPhase], xcodeProj: XcodeProj) throws -> [CopyFilesAction] +} + +/// A mapper that converts `PBXCopyFilesBuildPhase` objects into `CopyFilesAction` domain models. +struct PBXCopyFilesBuildPhaseMapper: PBXCopyFilesBuildPhaseMapping { + /// Maps the provided copy files phases to sorted `CopyFilesAction` models. + func map(_ copyFilesPhases: [PBXCopyFilesBuildPhase], xcodeProj: XcodeProj) throws -> [CopyFilesAction] { + try copyFilesPhases + .compactMap { try mapCopyFilesPhase($0, xcodeProj: xcodeProj) } + .sorted { $0.name < $1.name } + } + + // MARK: - Private Helpers + + /// Converts a single `PBXCopyFilesBuildPhase` to a `CopyFilesAction`. + /// - Parameters: + /// - phase: The `PBXCopyFilesBuildPhase` to convert. + /// - xcodeProj: The `XcodeProj` for path resolution. + /// - Returns: A `CopyFilesAction` if the phase could be mapped, otherwise `nil`. + /// - Throws: If file paths are invalid or unresolved. + private func mapCopyFilesPhase( + _ phase: PBXCopyFilesBuildPhase, + xcodeProj: XcodeProj + ) throws -> CopyFilesAction? { + let files = try (phase.files ?? []) + .compactMap { buildFile -> CopyFileElement? in + guard let fileRef = buildFile.file, + let pathString = try fileRef.fullPath(sourceRoot: xcodeProj.srcPathString) + else { + return nil + } + + let absolutePath = try AbsolutePath(validating: pathString) + let attributes = buildFile.settings?.stringArray(for: .attributes) + let codeSignOnCopy = attributes?.contains(BuildFileAttribute.codeSignOnCopy.rawValue) ?? false + + return .file(path: absolutePath, condition: nil, codeSignOnCopy: codeSignOnCopy) + } + .sorted { $0.path < $1.path } + + return CopyFilesAction( + name: phase.name ?? BuildPhaseConstants.copyFilesDefault, + destination: mapDstSubfolderSpec(phase.dstSubfolderSpec), + subpath: (phase.dstPath?.isEmpty == true) ? nil : phase.dstPath, + files: files + ) + } + + /// Maps a `PBXCopyFilesBuildPhase.SubFolder` to a `CopyFilesAction.Destination`. + private func mapDstSubfolderSpec( + _ subfolderSpec: PBXCopyFilesBuildPhase.SubFolder? + ) -> CopyFilesAction.Destination { + switch subfolderSpec { + case .absolutePath: return .absolutePath + case .productsDirectory: return .productsDirectory + case .wrapper: return .wrapper + case .executables: return .executables + case .resources: return .resources + case .javaResources: return .javaResources + case .frameworks: return .frameworks + case .sharedFrameworks: return .sharedFrameworks + case .sharedSupport: return .sharedSupport + case .plugins: return .plugins + default: return .productsDirectory + } + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Phases/PBXCoreDataModelsBuildPhaseMapper.swift b/Sources/XcodeProjMapper/Mappers/Phases/PBXCoreDataModelsBuildPhaseMapper.swift new file mode 100644 index 00000000..ac789586 --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Phases/PBXCoreDataModelsBuildPhaseMapper.swift @@ -0,0 +1,51 @@ +import Foundation +import Path +import XcodeGraph +import XcodeProj + +/// A protocol for mapping a set of resource files into `CoreDataModel` objects. +protocol PBXCoreDataModelsBuildPhaseMapping { + /// Maps the provided resource files into an array of `CoreDataModel`. + /// - Parameters: + /// - resourceFiles: The build files that might contain Core Data models. + /// - xcodeProj: The `XcodeProj` for resolving file paths. + /// - Returns: An array of `CoreDataModel` objects. + /// - Throws: If any paths are invalid. + func map(_ resourceFiles: [PBXBuildFile], xcodeProj: XcodeProj) throws -> [CoreDataModel] +} + +/// Maps `PBXBuildFile` objects to `CoreDataModel` domain models if they represent `.xcdatamodeld` files. +struct PBXCoreDataModelsBuildPhaseMapper: PBXCoreDataModelsBuildPhaseMapping { + func map(_ resourceFiles: [PBXBuildFile], xcodeProj: XcodeProj) throws -> [CoreDataModel] { + try resourceFiles.compactMap { try mapCoreDataModel($0, xcodeProj: xcodeProj) } + } + + // MARK: - Private Helpers + + /// Converts a single `PBXBuildFile` into a `CoreDataModel` if it references a `.xcdatamodeld` version group. + private func mapCoreDataModel(_ buildFile: PBXBuildFile, xcodeProj: XcodeProj) throws -> CoreDataModel? { + guard let versionGroup = buildFile.file as? XCVersionGroup, + versionGroup.path?.hasSuffix(FileExtension.coreData.rawValue) == true, + let modelPathString = try versionGroup.fullPath(sourceRoot: xcodeProj.srcPathString) + else { + return nil + } + + let modelPath = try AbsolutePath(validating: modelPathString) + + // Gather all child .xcdatamodel versions + let versionPaths = versionGroup.children.compactMap(\.path) + let resolvedVersions = try versionPaths.map { + try AbsolutePath(validating: $0, relativeTo: modelPath) + } + + // Current version defaults to the first if not explicitly set + let currentVersion = versionGroup.currentVersion?.path ?? resolvedVersions.first?.pathString ?? "" + + return CoreDataModel( + path: modelPath, + versions: resolvedVersions, + currentVersion: currentVersion + ) + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Phases/PBXFrameworksBuildPhaseMapper.swift b/Sources/XcodeProjMapper/Mappers/Phases/PBXFrameworksBuildPhaseMapper.swift new file mode 100644 index 00000000..6bd5b64b --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Phases/PBXFrameworksBuildPhaseMapper.swift @@ -0,0 +1,67 @@ +import Foundation +import Path +import XcodeGraph +import XcodeProj + +/// A protocol for mapping a `PBXFrameworksBuildPhase` into associated `TargetDependency`s. +protocol PBXFrameworksBuildPhaseMapping { + /// Maps the given frameworks build phase to a list of `TargetDependency` instances. + /// + /// - Parameters: + /// - frameworksBuildPhase: The `PBXFrameworksBuildPhase` to map. + /// - xcodeProj: The `XcodeProj` for path resolution. + /// - Returns: An array of `TargetDependency` objects representing the frameworks. + /// - Throws: If any file paths or references cannot be resolved. + func map( + _ frameworksBuildPhase: PBXFrameworksBuildPhase, + xcodeProj: XcodeProj + ) throws -> [TargetDependency] +} + +/// The default mapper that converts `PBXFrameworksBuildPhase` files into `TargetDependency` models. +struct PBXFrameworksBuildPhaseMapper: PBXFrameworksBuildPhaseMapping { + private let pathMapper: PathDependencyMapping + + init(pathMapper: PathDependencyMapping = PathDependencyMapper()) { + self.pathMapper = pathMapper + } + + func map( + _ frameworksBuildPhase: PBXFrameworksBuildPhase, + xcodeProj: XcodeProj + ) throws -> [TargetDependency] { + let files = frameworksBuildPhase.files ?? [] + return try files.map { try mapFrameworkDependency($0, xcodeProj: xcodeProj) } + } + + // MARK: - Private Helpers + + /// Maps a single PBXBuildFile from the frameworks build phase to a `TargetDependency`. + private func mapFrameworkDependency( + _ buildFile: PBXBuildFile, + xcodeProj: XcodeProj + ) throws -> TargetDependency { + let fileRef = try buildFile.file.throwing(PBXFrameworksBuildPhaseMappingError.missingFileReference) + let filePathString = try fileRef.fullPath(sourceRoot: xcodeProj.srcPathString) + .throwing(PBXFrameworksBuildPhaseMappingError.missingFilePath(name: fileRef.name)) + + let absolutePath = try AbsolutePath(validating: filePathString) + return try pathMapper.map(path: absolutePath, condition: nil) + } +} + +/// Errors that may occur when mapping framework build phase files. +enum PBXFrameworksBuildPhaseMappingError: Error, LocalizedError { + case missingFileReference + case missingFilePath(name: String?) + + var errorDescription: String? { + switch self { + case .missingFileReference: + return "Missing `PBXBuildFile.file` reference." + case let .missingFilePath(name): + let fileName = name ?? "Unknown" + return "Missing or invalid file path for `PBXBuildFile`: \(fileName)." + } + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Phases/PBXHeadersBuildPhaseMapper.swift b/Sources/XcodeProjMapper/Mappers/Phases/PBXHeadersBuildPhaseMapper.swift new file mode 100644 index 00000000..379b0515 --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Phases/PBXHeadersBuildPhaseMapper.swift @@ -0,0 +1,82 @@ +import Foundation +import Path +import XcodeGraph +import XcodeProj + +/// A protocol for mapping a PBXHeadersBuildPhase into a Headers model. +protocol PBXHeadersBuildPhaseMapping { + /// Converts the given headers build phase into a `Headers` object. + /// - Parameters: + /// - headersBuildPhase: The build phase containing header files. + /// - xcodeProj: The `XcodeProj` used for resolving file paths. + /// - Returns: A `Headers` object, or `nil` if there are no valid header files. + /// - Throws: If any file paths cannot be resolved. + func map(_ headersBuildPhase: PBXHeadersBuildPhase, xcodeProj: XcodeProj) throws -> Headers? +} + +/// Maps a `PBXHeadersBuildPhase` to a `Headers` domain model. +struct PBXHeadersBuildPhaseMapper: PBXHeadersBuildPhaseMapping { + func map(_ headersBuildPhase: PBXHeadersBuildPhase, xcodeProj: XcodeProj) throws -> Headers? { + // Gather all valid HeaderInfo objects + let headerInfos = try (headersBuildPhase.files ?? []).compactMap { + try mapHeaderFile($0, xcodeProj: xcodeProj) + } + + guard !headerInfos.isEmpty else { + return nil + } + + let publicHeaders = headerInfos + .filter { $0.visibility == .public } + .map(\.path) + let privateHeaders = headerInfos + .filter { $0.visibility == .private } + .map(\.path) + let projectHeaders = headerInfos + .filter { $0.visibility == .project } + .map(\.path) + + return Headers( + public: publicHeaders, + private: privateHeaders, + project: projectHeaders + ) + } + + // MARK: - Private Helpers + + /// Converts a single `PBXBuildFile` into a `HeaderInfo` if it's a valid header reference. + private func mapHeaderFile(_ buildFile: PBXBuildFile, xcodeProj: XcodeProj) throws -> HeaderInfo? { + guard let pbxElement = buildFile.file, + let pathString = try pbxElement.fullPath(sourceRoot: xcodeProj.srcPathString) + else { + return nil + } + + let absolutePath = try AbsolutePath(validating: pathString) + let attributes = buildFile.settings?.stringArray(for: .attributes) + + let visibility: HeaderInfo.HeaderVisibility + if attributes?.contains(HeaderAttribute.public.rawValue) == true { + visibility = .public + } else if attributes?.contains(HeaderAttribute.private.rawValue) == true { + visibility = .private + } else { + visibility = .project + } + + return HeaderInfo(path: absolutePath, visibility: visibility) + } +} + +/// Internal struct used to capture a single header file’s path and visibility. +private struct HeaderInfo { + let path: AbsolutePath + let visibility: HeaderVisibility + + enum HeaderVisibility { + case `public` + case `private` + case project + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Phases/PBXResourcesBuildPhaseMapper.swift b/Sources/XcodeProjMapper/Mappers/Phases/PBXResourcesBuildPhaseMapper.swift new file mode 100644 index 00000000..db89ce32 --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Phases/PBXResourcesBuildPhaseMapper.swift @@ -0,0 +1,86 @@ +import Foundation +import Path +import XcodeGraph +import XcodeProj + +/// A protocol for mapping a PBXResourcesBuildPhase into an array of ResourceFileElement. +protocol PBXResourcesBuildPhaseMapping { + /// Converts the given resources build phase to a list of `ResourceFileElement` models. + /// - Parameters: + /// - resourcesBuildPhase: The build phase that may contain resource files and variant groups. + /// - xcodeProj: The `XcodeProj` used for path resolution. + /// - Returns: An array of `ResourceFileElement`s, representing file paths or grouped variants. + /// - Throws: If any file references are missing or paths cannot be resolved. + func map(_ resourcesBuildPhase: PBXResourcesBuildPhase, xcodeProj: XcodeProj) throws -> [ResourceFileElement] +} + +/// A mapper that converts a `PBXResourcesBuildPhase` into a list of `ResourceFileElement`. +struct PBXResourcesBuildPhaseMapper: PBXResourcesBuildPhaseMapping { + func map( + _ resourcesBuildPhase: PBXResourcesBuildPhase, + xcodeProj: XcodeProj + ) throws -> [ResourceFileElement] { + let files = resourcesBuildPhase.files ?? [] + let elements = try files.flatMap { buildFile in + try mapResourceElement(buildFile, xcodeProj: xcodeProj) + } + return elements.sorted { $0.path < $1.path } + } + + // MARK: - Private Helpers + + /// Maps a single `PBXBuildFile` to one or more `ResourceFileElement`s. + private func mapResourceElement( + _ buildFile: PBXBuildFile, + xcodeProj: XcodeProj + ) throws -> [ResourceFileElement] { + let fileElement = try buildFile.file + .throwing(PBXResourcesMappingError.missingFileReference) + + // If it's a PBXVariantGroup, map each child within that group. + if let variantGroup = fileElement as? PBXVariantGroup { + return try mapVariantGroup(variantGroup, xcodeProj: xcodeProj) + } else { + // Otherwise, it's a straightforward file or reference. + return try mapFileElement(fileElement, xcodeProj: xcodeProj) + } + } + + /// Maps a simple (non-variant) file element to a list (usually a single entry) of `ResourceFileElement`. + private func mapFileElement( + _ fileElement: PBXFileElement, + xcodeProj: XcodeProj + ) throws -> [ResourceFileElement] { + let pathString = try fileElement + .fullPath(sourceRoot: xcodeProj.srcPathString) + .throwing(PBXResourcesMappingError.missingFullPath(fileElement.name ?? "Unknown")) + + let absolutePath = try AbsolutePath(validating: pathString) + return [.file(path: absolutePath)] + } + + /// Maps a PBXVariantGroup by expanding each child into a `ResourceFileElement`. + private func mapVariantGroup( + _ variantGroup: PBXVariantGroup, + xcodeProj: XcodeProj + ) throws -> [ResourceFileElement] { + try variantGroup.children.flatMap { child in + try mapFileElement(child, xcodeProj: xcodeProj) + } + } +} + +/// Example error types for resource mapping. +enum PBXResourcesMappingError: LocalizedError { + case missingFileReference + case missingFullPath(String) + + var errorDescription: String? { + switch self { + case .missingFileReference: + return "Missing file reference for resource." + case let .missingFullPath(name): + return "No valid path for resource file element: \(name)." + } + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Phases/PBXScriptsBuildPhaseMapper.swift b/Sources/XcodeProjMapper/Mappers/Phases/PBXScriptsBuildPhaseMapper.swift new file mode 100644 index 00000000..672ae6ba --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Phases/PBXScriptsBuildPhaseMapper.swift @@ -0,0 +1,116 @@ +import Foundation +import Path +import XcodeGraph +import XcodeProj + +/// A protocol for mapping PBXShellScriptBuildPhases into domain models. +protocol PBXScriptsBuildPhaseMapping { + /// Maps the given script phases into `TargetScript` models. + /// + /// - Parameters: + /// - scriptPhases: The shell script build phases to convert. + /// - buildPhases: The full list of a target's `PBXBuildPhase`s, used to determine script order. + /// - Returns: An array of `TargetScript` models representing each shell script build phase. + /// - Throws: If any file paths cannot be validated. + func map( + _ scriptPhases: [PBXShellScriptBuildPhase], + buildPhases: [PBXBuildPhase] + ) throws -> [TargetScript] + + /// Maps shell script build phases into a simpler, “raw” representation (`RawScriptBuildPhase`). + /// + /// - Parameter scriptPhases: The shell script build phases to map. + /// - Returns: A list of `RawScriptBuildPhase` models for each script. + func mapRawScriptBuildPhases(_ scriptPhases: [PBXShellScriptBuildPhase]) -> [RawScriptBuildPhase] +} + +/// Maps `PBXShellScriptBuildPhase` instances into `TargetScript` and `RawScriptBuildPhase` models. +struct PBXScriptsBuildPhaseMapper: PBXScriptsBuildPhaseMapping { + func map( + _ scriptPhases: [PBXShellScriptBuildPhase], + buildPhases: [PBXBuildPhase] + ) throws -> [TargetScript] { + try scriptPhases.compactMap { + try mapScriptPhase($0, buildPhases: buildPhases) + } + } + + func mapRawScriptBuildPhases(_ scriptPhases: [PBXShellScriptBuildPhase]) -> [RawScriptBuildPhase] { + scriptPhases.map { mapShellScriptBuildPhase($0) } + } + + // MARK: - Private Helpers + + /// Converts a single `PBXShellScriptBuildPhase` to a `TargetScript`, if valid. + private func mapScriptPhase( + _ scriptPhase: PBXShellScriptBuildPhase, + buildPhases: [PBXBuildPhase] + ) throws -> TargetScript? { + guard let shellScript = scriptPhase.shellScript else { + return nil + } + + let inputFileListPaths = try scriptPhase.inputFileListPaths?.compactMap { + try AbsolutePath(validating: $0) + } ?? [] + + let outputFileListPaths = try scriptPhase.outputFileListPaths?.compactMap { + try AbsolutePath(validating: $0) + } ?? [] + + let dependencyFile = try scriptPhase.dependencyFile.map { + try AbsolutePath(validating: $0) + } + + return TargetScript( + name: scriptPhase.name ?? BuildPhaseConstants.defaultScriptName, + order: determineScriptOrder(buildPhases: buildPhases, scriptPhase: scriptPhase), + script: .embedded(shellScript), + inputPaths: scriptPhase.inputPaths, + inputFileListPaths: inputFileListPaths, + outputPaths: scriptPhase.outputPaths, + outputFileListPaths: outputFileListPaths, + showEnvVarsInLog: scriptPhase.showEnvVarsInLog, + basedOnDependencyAnalysis: scriptPhase.alwaysOutOfDate ? false : nil, + runForInstallBuildsOnly: scriptPhase.runOnlyForDeploymentPostprocessing, + shellPath: scriptPhase.shellPath ?? BuildPhaseConstants.defaultShellPath, + dependencyFile: dependencyFile + ) + } + + /// Converts a single `PBXShellScriptBuildPhase` into a simpler `RawScriptBuildPhase`. + private func mapShellScriptBuildPhase( + _ buildPhase: PBXShellScriptBuildPhase + ) -> RawScriptBuildPhase { + let name = buildPhase.name() ?? BuildPhaseConstants.unnamedScriptPhase + let shellPath = buildPhase.shellPath ?? BuildPhaseConstants.defaultShellPath + let script = buildPhase.shellScript ?? "" + let showEnvVarsInLog = buildPhase.showEnvVarsInLog + + return RawScriptBuildPhase( + name: name, + script: script, + showEnvVarsInLog: showEnvVarsInLog, + hashable: false, + shellPath: shellPath + ) + } + + /// Determines the order of the script relative to other build phases (pre or post sources). + private func determineScriptOrder( + buildPhases: [PBXBuildPhase], + scriptPhase: PBXShellScriptBuildPhase + ) -> TargetScript.Order { + guard let scriptIndex = buildPhases.firstIndex(of: scriptPhase) else { + return .pre + } + + // If we have a Sources phase, check whether this script is above or below it. + if let sourcesIndex = buildPhases.firstIndex(where: { $0.buildPhase == .sources }) { + return scriptIndex > sourcesIndex ? .post : .pre + } + + // Fallback: if it's at index 0, consider it pre; otherwise post. + return scriptIndex == 0 ? .pre : .post + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Phases/PBXSourcesBuildPhaseMapper.swift b/Sources/XcodeProjMapper/Mappers/Phases/PBXSourcesBuildPhaseMapper.swift new file mode 100644 index 00000000..dc0e5745 --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Phases/PBXSourcesBuildPhaseMapper.swift @@ -0,0 +1,71 @@ +import Foundation +import Path +import XcodeGraph +import XcodeProj + +/// A protocol for mapping a PBXSourcesBuildPhase into an array of SourceFile models. +protocol PBXSourcesBuildPhaseMapping { + /// Converts the given sources build phase into a list of `SourceFile`s. + /// - Parameters: + /// - sourcesBuildPhase: The build phase that may contain source files. + /// - xcodeProj: The `XcodeProj` used for path resolution. + /// - Returns: A sorted list of `SourceFile`s by their path. + /// - Throws: If file paths are invalid or unavailable. + func map(_ sourcesBuildPhase: PBXSourcesBuildPhase, xcodeProj: XcodeProj) throws -> [SourceFile] +} + +/// The default mapper that converts a `PBXSourcesBuildPhase` to an array of `SourceFile`s. +struct PBXSourcesBuildPhaseMapper: PBXSourcesBuildPhaseMapping { + func map( + _ sourcesBuildPhase: PBXSourcesBuildPhase, + xcodeProj: XcodeProj + ) throws -> [SourceFile] { + let files = sourcesBuildPhase.files ?? [] + return try files + .compactMap { try mapSourceFile($0, xcodeProj: xcodeProj) } + .sorted { $0.path < $1.path } + } + + // MARK: - Private Helpers + + /// Maps a single `PBXBuildFile` into a `SourceFile` if valid. + private func mapSourceFile( + _ buildFile: PBXBuildFile, + xcodeProj: XcodeProj + ) throws -> SourceFile? { + guard let fileRef = buildFile.file, + let pathString = try fileRef.fullPath(sourceRoot: xcodeProj.srcPathString) + else { + return nil + } + + let path = try AbsolutePath(validating: pathString) + let settings = buildFile.settings ?? [:] + let compilerFlags: String? = settings.string(for: .compilerFlags) + let attributes: [String]? = settings.stringArray(for: .attributes) + + return SourceFile( + path: path, + compilerFlags: compilerFlags, + codeGen: mapCodeGenAttribute(attributes) + ) + } + + /// Translates file attributes into a `FileCodeGen`, if specified. + private func mapCodeGenAttribute(_ attributes: [String]?) -> FileCodeGen? { + guard let attributes else { return nil } + + switch true { + case attributes.contains(FileCodeGen.public.rawValue): + return .public + case attributes.contains(FileCodeGen.private.rawValue): + return .private + case attributes.contains(FileCodeGen.project.rawValue): + return .project + case attributes.contains(FileCodeGen.disabled.rawValue): + return .disabled + default: + return nil + } + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Project/PBXProjectMapper.swift b/Sources/XcodeProjMapper/Mappers/Project/PBXProjectMapper.swift new file mode 100644 index 00000000..e9687749 --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Project/PBXProjectMapper.swift @@ -0,0 +1,157 @@ +import Foundation +import Path +import PathKit +import XcodeGraph +import XcodeMetadata +@preconcurrency import XcodeProj + +// swiftlint:disable function_body_length + +/// A protocol for mapping an Xcode project (`.xcodeproj`) into a `Project` domain model. +protocol PBXProjectMapping { + /// Maps the given `XcodeProj` into a `Project` model. + /// + /// - Parameter xcodeProj: The Xcode project to be transformed. + /// - Returns: A fully constructed `Project` model. + /// - Throws: If reading or transforming project data fails. + func map(xcodeProj: XcodeProj) async throws -> Project +} + +/// A mapper that transforms a `.xcodeproj` into a `Project` domain model. +/// +/// This process involves: +/// - Mapping project-level settings. +/// - Converting `PBXTarget`s into `Target` models. +/// - Resolving both remote and local Swift packages. +/// - Identifying and integrating user and shared schemes. +/// - Providing resource synthesizers for code generation. +struct PBXProjectMapper: PBXProjectMapping { + /// Maps the given Xcode project into a `Project` model. + /// + /// - Parameter xcodeProj: The Xcode project reference containing `.pbxproj` data. + /// - Returns: A fully constructed `Project` model. + /// - Throws: If reading or transforming project data fails. + func map(xcodeProj: XcodeProj) async throws -> Project { + let settingsMapper = XCConfigurationMapper() + let pbxProject = try xcodeProj.mainPBXProject() + let xcodeProjPath = xcodeProj.projectPath + let sourceDirectory = xcodeProjPath.parentDirectory + + // Map the project-wide build settings + let settings = try settingsMapper.map( + xcodeProj: xcodeProj, + configurationList: pbxProject.buildConfigurationList + ) + + // Map PBXTargets to domain Targets + let targetMapper = PBXTargetMapper() + let targets = try await pbxProject.targets.serialCompactMap { + try await targetMapper.map(pbxTarget: $0, xcodeProj: xcodeProj) + } + .sorted() + + // Map remote and local packages + let packageMapper = XCPackageMapper() + let remotePackages = try pbxProject.remotePackages.compactMap { + try packageMapper.map(package: $0) + } + let localPackages = try pbxProject.localPackages.compactMap { + try packageMapper.map(package: $0, sourceDirectory: sourceDirectory) + } + + // Create a files group for the main group + let filesGroup = ProjectGroup.group(name: pbxProject.mainGroup?.name ?? "Project") + + // Map user and shared schemes + let schemeMapper = XCSchemeMapper() + let graphType: XcodeMapperGraphType = .project(xcodeProj) + let userSchemes = try xcodeProj.userData.flatMap(\.schemes).map { + try schemeMapper.map($0, shared: false, graphType: graphType) + } + let sharedSchemes = try xcodeProj.sharedData?.schemes.map { + try schemeMapper.map($0, shared: true, graphType: graphType) + } ?? [] + let schemes = userSchemes + sharedSchemes + + // Other project-level metadata + let lastUpgradeCheck = pbxProject.attribute(for: .lastUpgradeCheck).flatMap { Version(string: $0) } + let defaultKnownRegions = pbxProject.knownRegions.isEmpty ? nil : pbxProject.knownRegions + + // Construct the final `Project` + return Project( + path: sourceDirectory, + sourceRootPath: sourceDirectory, + xcodeProjPath: xcodeProjPath, + name: pbxProject.name, + organizationName: pbxProject.attribute(for: .organization), + classPrefix: pbxProject.attribute(for: .classPrefix), + defaultKnownRegions: defaultKnownRegions, + developmentRegion: pbxProject.developmentRegion, + options: .init( + automaticSchemesOptions: .disabled, + disableBundleAccessors: false, + disableShowEnvironmentVarsInScriptPhases: false, + disableSynthesizedResourceAccessors: false, + textSettings: .init( + usesTabs: nil, + indentWidth: nil, + tabWidth: nil, + wrapsLines: nil + ) + ), + settings: settings, + filesGroup: filesGroup, + targets: targets, + packages: remotePackages + localPackages, + schemes: schemes, + ideTemplateMacros: nil, + additionalFiles: [], + resourceSynthesizers: mapResourceSynthesizers(), + lastUpgradeCheck: lastUpgradeCheck, + type: .local + ) + } + + /// Returns a set of default resource synthesizers for common resource types. + private func mapResourceSynthesizers() -> [ResourceSynthesizer] { + ResourceSynthesizer.Parser.allCases.map { parser in + let (exts, template) = parser.resourceTypes() + return ResourceSynthesizer( + parser: parser, + parserOptions: [:], + extensions: Set(exts), + template: .defaultTemplate(template) + ) + } + } +} + +// MARK: - ResourceSynthesizer.Parser Helpers + +extension ResourceSynthesizer.Parser { + /// Defines resource extensions and default templates for each parser type. + fileprivate func resourceTypes() -> (exts: [String], template: String) { + switch self { + case .strings, .stringsCatalog: + return (["strings", "stringsdict"], "Strings") + case .assets: + return (["xcassets"], "Assets") + case .plists: + return (["plist"], "Plists") + case .fonts: + return (["ttf", "otf", "ttc"], "Fonts") + case .coreData: + return (["xcdatamodeld"], "CoreData") + case .interfaceBuilder: + return (["xib", "storyboard"], "InterfaceBuilder") + case .json: + return (["json"], "JSON") + case .yaml: + return (["yaml", "yml"], "YAML") + case .files: + return (["txt", "md"], "Files") + } + } +} + +// swiftlint:enable function_body_length diff --git a/Sources/XcodeProjMapper/Mappers/Project/ProjectAttribute.swift b/Sources/XcodeProjMapper/Mappers/Project/ProjectAttribute.swift new file mode 100644 index 00000000..ae33a42f --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Project/ProjectAttribute.swift @@ -0,0 +1,8 @@ +import Foundation + +/// Attributes for project settings that can be retrieved from a `PBXProject`. +enum ProjectAttribute: String { + case classPrefix = "CLASSPREFIX" + case organization = "ORGANIZATIONNAME" + case lastUpgradeCheck = "LastUpgradeCheck" +} diff --git a/Sources/XcodeProjMapper/Mappers/Project/XcodeProj+Extensions.swift b/Sources/XcodeProjMapper/Mappers/Project/XcodeProj+Extensions.swift new file mode 100644 index 00000000..b90eb076 --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Project/XcodeProj+Extensions.swift @@ -0,0 +1,23 @@ +import Foundation +import XcodeProj + +/// Errors that may occur while accessing main `PBXProject` information. +enum XcodeProjError: LocalizedError, Equatable { + case noProjectsFound + + var errorDescription: String? { + switch self { + case .noProjectsFound: + return "No `PBXProject` was found in the `.xcodeproj`" + } + } +} + +extension XcodeProj { + func mainPBXProject() throws -> PBXProject { + guard let pbxProject = pbxproj.projects.first else { + throw XcodeProjError.noProjectsFound + } + return pbxProject + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Schemes/SchemeDiagnosticsOptions+XCScheme.swift b/Sources/XcodeProjMapper/Mappers/Schemes/SchemeDiagnosticsOptions+XCScheme.swift new file mode 100644 index 00000000..ebbf3f2c --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Schemes/SchemeDiagnosticsOptions+XCScheme.swift @@ -0,0 +1,25 @@ +import XcodeGraph +import XcodeProj + +extension SchemeDiagnosticsOptions { + /// Creates a SchemeDiagnosticsOptions from a LaunchAction. + init(action: XCScheme.LaunchAction) { + self = SchemeDiagnosticsOptions( + addressSanitizerEnabled: action.enableAddressSanitizer, + detectStackUseAfterReturnEnabled: action.enableASanStackUseAfterReturn, + threadSanitizerEnabled: action.enableThreadSanitizer, + mainThreadCheckerEnabled: !action.disableMainThreadChecker, + performanceAntipatternCheckerEnabled: !action.disablePerformanceAntipatternChecker + ) + } + + /// Creates a SchemeDiagnosticsOptions from a TestAction. + init(action: XCScheme.TestAction) { + self = SchemeDiagnosticsOptions( + addressSanitizerEnabled: action.enableAddressSanitizer, + detectStackUseAfterReturnEnabled: action.enableASanStackUseAfterReturn, + threadSanitizerEnabled: action.enableThreadSanitizer, + mainThreadCheckerEnabled: !action.disableMainThreadChecker + ) + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Schemes/XCSchemeMapper.swift b/Sources/XcodeProjMapper/Mappers/Schemes/XCSchemeMapper.swift new file mode 100644 index 00000000..eeda5bdb --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Schemes/XCSchemeMapper.swift @@ -0,0 +1,224 @@ +import Foundation +import Path +import XcodeGraph +import XcodeProj + +/// A protocol defining how to map a single `XCScheme` object (and its actions) into a domain `Scheme` model. +/// +/// Conforming types translate a raw `XCScheme` instance, including its build, test, run, archive, profile, +/// and analyze actions, into a `Scheme` model ready for analysis, code generation, or tooling integration. +protocol SchemeMapping { + /// Maps a single `XCScheme` into a `Scheme` model. + /// + /// - Parameters: + /// - xcscheme: The `XCScheme` to map. + /// - shared: Indicates whether the scheme is shared. + /// - graphType: Specifies if we’re dealing with a workspace or project for path resolution. + /// - Returns: A `Scheme` model corresponding to the given `XCScheme`. + /// - Throws: If any of the scheme's actions (build, test, run, etc.) cannot be resolved. + func map( + _ xcscheme: XCScheme, + shared: Bool, + graphType: XcodeMapperGraphType + ) throws -> Scheme +} + +/// A mapper responsible for converting an `XCScheme` object into a `Scheme` model. +/// +/// `XCSchemeMapper` resolves references to targets, environment variables, and all scheme actions. +/// The resulting `Scheme` models enable analysis, code generation, or integration with custom tooling. +struct XCSchemeMapper: SchemeMapping { + // MARK: - Public API + + func map( + _ xcscheme: XCScheme, + shared: Bool, + graphType: XcodeMapperGraphType + ) throws -> Scheme { + Scheme( + name: xcscheme.name, + shared: shared, + hidden: false, + buildAction: try mapBuildAction(action: xcscheme.buildAction, graphType: graphType), + testAction: try mapTestAction(action: xcscheme.testAction, graphType: graphType), + runAction: try mapRunAction(action: xcscheme.launchAction, graphType: graphType), + archiveAction: try mapArchiveAction(action: xcscheme.archiveAction), + profileAction: try mapProfileAction(action: xcscheme.profileAction, graphType: graphType), + analyzeAction: try mapAnalyzeAction(action: xcscheme.analyzeAction) + ) + } + + // MARK: - Action Mappings + + /// Maps the optional build action into a domain `BuildAction`, or returns `nil` if not present. + private func mapBuildAction( + action: XCScheme.BuildAction?, + graphType: XcodeMapperGraphType + ) throws -> BuildAction? { + guard let action else { return nil } + + let targets = try action.buildActionEntries.compactMap { + try mapTargetReference(buildableReference: $0.buildableReference, graphType: graphType) + } + + return BuildAction( + targets: targets, + preActions: [], + postActions: [], + runPostActionsOnFailure: action.runPostActionsOnFailure ?? false, + findImplicitDependencies: action.buildImplicitDependencies + ) + } + + /// Maps the optional test action into a domain `TestAction`, or returns `nil` if not present. + private func mapTestAction( + action: XCScheme.TestAction?, + graphType: XcodeMapperGraphType + ) throws -> TestAction? { + guard let action else { return nil } + + let testTargets = try action.testables.compactMap { testable in + let targetRef = try mapTargetReference( + buildableReference: testable.buildableReference, + graphType: graphType + ) + return TestableTarget(target: targetRef, skipped: testable.skipped) + } + + let arguments = mapArguments( + environmentVariables: action.environmentVariables, + commandlineArguments: action.commandlineArguments + ) + let diagnosticsOptions = SchemeDiagnosticsOptions(action: action) + + return TestAction( + targets: testTargets, + arguments: arguments, + configurationName: action.buildConfiguration, + attachDebugger: true, + coverage: action.codeCoverageEnabled, + codeCoverageTargets: [], + expandVariableFromTarget: nil, + preActions: [], + postActions: [], + diagnosticsOptions: diagnosticsOptions, + language: action.language, + region: action.region + ) + } + + /// Maps the optional run (launch) action into a domain `RunAction`, or returns `nil` if not present. + private func mapRunAction( + action: XCScheme.LaunchAction?, + graphType: XcodeMapperGraphType + ) throws -> RunAction? { + guard let action else { return nil } + + let executable: TargetReference? = try { + if let buildableRef = action.runnable?.buildableReference { + return try mapTargetReference(buildableReference: buildableRef, graphType: graphType) + } + return nil + }() + + let arguments = mapArguments( + environmentVariables: action.environmentVariables, + commandlineArguments: action.commandlineArguments + ) + let diagnosticsOptions = SchemeDiagnosticsOptions(action: action) + // If no debugger is explicitly chosen, Xcode uses the default lldb (true). + let attachDebugger = action.selectedDebuggerIdentifier.isEmpty + + return RunAction( + configurationName: action.buildConfiguration, + attachDebugger: attachDebugger, + customLLDBInitFile: nil, + preActions: [], + postActions: [], + executable: executable, + filePath: nil, + arguments: arguments, + options: RunActionOptions(), + diagnosticsOptions: diagnosticsOptions + ) + } + + /// Maps the optional archive action into a domain `ArchiveAction`, or returns `nil` if not present. + private func mapArchiveAction( + action: XCScheme.ArchiveAction? + ) throws -> ArchiveAction? { + guard let action else { return nil } + return ArchiveAction( + configurationName: action.buildConfiguration, + revealArchiveInOrganizer: action.revealArchiveInOrganizer + ) + } + + /// Maps the optional profile action into a domain `ProfileAction`, or returns `nil` if not present. + func mapProfileAction( + action: XCScheme.ProfileAction?, + graphType: XcodeMapperGraphType + ) throws -> ProfileAction? { + guard let action else { return nil } + + let executable: TargetReference? = try { + if let buildableRef = action.buildableProductRunnable?.buildableReference { + return try mapTargetReference(buildableReference: buildableRef, graphType: graphType) + } + return nil + }() + + return ProfileAction( + configurationName: action.buildConfiguration, + executable: executable + ) + } + + /// Maps the optional analyze action into a domain `AnalyzeAction`, or returns `nil` if not present. + private func mapAnalyzeAction( + action: XCScheme.AnalyzeAction? + ) throws -> AnalyzeAction? { + guard let action else { return nil } + return AnalyzeAction(configurationName: action.buildConfiguration) + } + + // MARK: - Helper Methods + + /// Converts a buildable reference within a scheme to a `TargetReference`. + private func mapTargetReference( + buildableReference: XCScheme.BuildableReference, + graphType: XcodeMapperGraphType + ) throws -> TargetReference { + let targetName = buildableReference.blueprintName + let container = buildableReference.referencedContainer + + let projectPath: AbsolutePath + switch graphType { + case let .workspace(xcworkspace): + // Container is relative to the workspace’s parent directory + let relativeContainerPath = container.replacingOccurrences(of: "container:", with: "") + let relPath = try RelativePath(validating: relativeContainerPath) + projectPath = xcworkspace.workspacePath.parentDirectory.appending(relPath) + case let .project(xcodeProj): + projectPath = xcodeProj.projectPath + } + + return TargetReference(projectPath: projectPath, name: targetName) + } + + /// Converts environment variables and command-line arguments into a unified `Arguments` model. + private func mapArguments( + environmentVariables: [XCScheme.EnvironmentVariable]?, + commandlineArguments: XCScheme.CommandLineArguments? + ) -> Arguments { + let envVars = environmentVariables?.reduce(into: [String: EnvironmentVariable]()) { dict, variable in + dict[variable.variable] = EnvironmentVariable(value: variable.value, isEnabled: variable.enabled) + } ?? [:] + + let launchArgs = commandlineArguments?.arguments.map { + LaunchArgument(name: $0.name, isEnabled: $0.enabled) + } ?? [] + + return Arguments(environmentVariables: envVars, launchArguments: launchArgs) + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Settings/BuildSettings.swift b/Sources/XcodeProjMapper/Mappers/Settings/BuildSettings.swift new file mode 100644 index 00000000..e5652b26 --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Settings/BuildSettings.swift @@ -0,0 +1,100 @@ +import Foundation + +/// Keys representing various build settings that may appear in an Xcode project or workspace configuration. +enum BuildSettingKey: String { + case sdkroot = "SDKROOT" + case compilerFlags = "COMPILER_FLAGS" + case attributes = "ATTRIBUTES" + case environmentVariables = "ENVIRONMENT_VARIABLES" + case codeSignOnCopy = "CODE_SIGN_ON_COPY" + case dependencyFile = "DEPENDENCY_FILE" + case inputPaths = "INPUT_PATHS" + case outputPaths = "OUTPUT_PATHS" + case showEnvVarsInLog = "SHOW_ENV_VARS_IN_LOG" + case shellPath = "SHELL_PATH" + case launchArguments = "LAUNCH_ARGUMENTS" + case tags = "TAGS" + case mergedBinaryType = "MERGED_BINARY_TYPE" + case prune = "PRUNE" + case mergeable = "MERGEABLE" + case productBundleIdentifier = "PRODUCT_BUNDLE_IDENTIFIER" + case infoPlistFile = "INFOPLIST_FILE" + case codeSignEntitlements = "CODE_SIGN_ENTITLEMENTS" + case iPhoneOSDeploymentTarget = "IPHONEOS_DEPLOYMENT_TARGET" + case macOSDeploymentTarget = "MACOSX_DEPLOYMENT_TARGET" + case watchOSDeploymentTarget = "WATCHOS_DEPLOYMENT_TARGET" + case tvOSDeploymentTarget = "TVOS_DEPLOYMENT_TARGET" + case visionOSDeploymentTarget = "VISIONOS_DEPLOYMENT_TARGET" +} + +/// A protocol representing a type that can parse a build setting value from a generic `Any`. +protocol BuildSettingValue { + associatedtype Value + static func parse(_ any: Any) -> Value? +} + +/// A type that parses build settings as strings. +enum BuildSettingString: BuildSettingValue { + static func parse(_ any: Any) -> String? { + any as? String + } +} + +/// A type that parses build settings as arrays of strings. +enum BuildSettingStringArray: BuildSettingValue { + static func parse(_ any: Any) -> [String]? { + let arr = any as? [Any] + return arr?.compactMap { $0 as? String } + } +} + +/// A type that parses build settings as booleans. +enum BuildSettingBool: BuildSettingValue { + static func parse(_ any: Any) -> Bool? { + any as? Bool + } +} + +/// A type that parses build settings as dictionaries of strings to strings. +enum BuildSettingStringDict: BuildSettingValue { + static func parse(_ any: Any) -> [String: String]? { + any as? [String: String] + } +} + +extension [String: Any] { + /// Extracts a build setting value of a specified type from the dictionary. + /// + /// - Parameters: + /// - key: The `BuildSettingKey` to look up. + /// - type: The type conforming to `BuildSettingValue` indicating the expected value type. + /// - Returns: The parsed value if found and valid, or `nil` otherwise. + func extractBuildSetting(_ key: BuildSettingKey, as _: T.Type = T.self) + -> T.Value? + { + guard let value = self[key.rawValue] else { return nil } + return T.parse(value) + } +} + +extension [String: Any] { + /// Retrieves a string value for the given build setting key. + func string(for key: BuildSettingKey) -> String? { + extractBuildSetting(key, as: BuildSettingString.self) + } + + /// Retrieves an array of strings for the given build setting key. + func stringArray(for key: BuildSettingKey) -> [String]? { + extractBuildSetting(key, as: BuildSettingStringArray.self) + } + + /// Retrieves a boolean value for the given build setting key. + func bool(for key: BuildSettingKey) -> Bool? { + extractBuildSetting(key, as: BuildSettingBool.self) + } + + /// Retrieves a dictionary of strings for the given build setting key. + func stringDict(for key: BuildSettingKey) -> [String: String]? { + extractBuildSetting(key, as: BuildSettingStringDict.self) + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Settings/XCConfigurationList+Helpers.swift b/Sources/XcodeProjMapper/Mappers/Settings/XCConfigurationList+Helpers.swift new file mode 100644 index 00000000..8c429113 --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Settings/XCConfigurationList+Helpers.swift @@ -0,0 +1,41 @@ +import XcodeGraph +import XcodeProj + +extension XCConfigurationList { + /// Retrieves a build setting value from the first configuration in which it is found. + /// + /// - Parameter key: The `BuildSettingKey` to look up. + /// - Returns: The value as a `String` if found, otherwise `nil`. + func stringSettings(for key: BuildSettingKey) -> [BuildConfiguration: String] { + let configurationMatcher = ConfigurationMatcher() + var results = [BuildConfiguration: String]() + for config in buildConfigurations { + if let value = config.buildSettings.string(for: key) { + let variant = configurationMatcher.variant(for: config.name) + let buildConfig = BuildConfiguration(name: config.name, variant: variant) + results[buildConfig] = value + } + } + return results + } + + /// Retrieves all deployment target values from all configurations and aggregates them. + /// + /// - Parameters: + /// - keys: A list of keys to search (e.g., `.iPhoneOSDeploymentTarget`, `.macOSDeploymentTarget`) + /// - Returns: A dictionary mapping `BuildSettingKey` to the found value. + func allDeploymentTargets(keys: [BuildSettingKey]) -> [BuildSettingKey: String] { + var results = [BuildSettingKey: String]() + + for key in keys { + for config in buildConfigurations { + if let value = config.buildSettings.string(for: key) { + results[key] = value + break // Once found, move to the next key + } + } + } + + return results + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Settings/XCConfigurationMapper.swift b/Sources/XcodeProjMapper/Mappers/Settings/XCConfigurationMapper.swift new file mode 100644 index 00000000..5a5b683c --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Settings/XCConfigurationMapper.swift @@ -0,0 +1,128 @@ +import Foundation +import Path +import XcodeGraph +import XcodeProj + +/// A protocol that defines how to map an Xcode project's `XCConfigurationList` into a domain-specific `Settings` model. +/// +/// Conforming types provide an asynchronous mapping function that takes a `projectProvider` and an optional +/// `XCConfigurationList`, then returns a `Settings` model. If no configuration list is provided, they return default settings. +protocol SettingsMapping: Sendable { + /// Maps a given `XCConfigurationList` into a `Settings` model. + /// + /// This operation extracts build configurations and their associated build settings, translating them into a + /// domain-specific `Settings` representation. If the provided configuration list is `nil`, default settings are returned. + /// + /// - Parameters: + /// - projectProvider: A provider for project-related paths and files, used to resolve paths like `.xcconfig` files. + /// - configurationList: The `XCConfigurationList` from which to derive settings. If `nil`, defaults are returned. + /// - Returns: A `Settings` model derived from the configuration list, or default settings if none are found. + /// - Throws: If any build settings cannot be properly mapped into a `Settings` model. + func map( + xcodeProj: XcodeProj, + configurationList: XCConfigurationList? + ) throws -> Settings +} + +/// A mapper responsible for converting an Xcode project's configuration list into a `Settings` domain model. +/// +/// `SettingsMapper` reads through the project's `XCConfigurationList`, extracting each build configuration along with +/// its raw build settings. It then translates these settings into a structured `Settings` model, associating them with +/// corresponding `BuildConfiguration` variants (e.g., debug or release). Additionally, it attempts to resolve any +/// `.xcconfig` references into absolute paths. If no configuration list is provided, `SettingsMapper` returns default settings. +/// +/// Typical usage: +/// ```swift +/// let mapper = SettingsMapper() +/// let settings = try mapper.map(xcodeProj: provider.xcodeProj, configurationList: configurationList) +/// ``` +final class XCConfigurationMapper: SettingsMapping { + func map( + xcodeProj: XcodeProj, + configurationList: XCConfigurationList? + ) throws -> Settings { + guard let configurationList else { + return Settings.default + } + + var configurations: [BuildConfiguration: Configuration?] = [:] + for buildConfig in configurationList.buildConfigurations { + let buildSettings = buildConfig.buildSettings + let settingsDict = try mapBuildSettings(buildSettings) + + var xcconfigAbsolutePath: AbsolutePath? + if let baseConfigRef = buildConfig.baseConfiguration, + let xcconfigPath = try baseConfigRef.fullPath( + sourceRoot: xcodeProj.srcPathString + ) + { + xcconfigAbsolutePath = try AbsolutePath(validating: xcconfigPath) + } + + let variant = variant(for: buildConfig.name) + let buildConfiguration = BuildConfiguration(name: buildConfig.name, variant: variant) + configurations[buildConfiguration] = Configuration( + settings: settingsDict, + xcconfig: xcconfigAbsolutePath + ) + } + + return Settings( + base: [:], + baseDebug: [:], + configurations: configurations, + defaultSettings: .recommended + ) + } + + /// Converts a dictionary of raw build settings (`[String: Any]`) into a structured `SettingsDictionary`. + /// + /// Each raw setting value is mapped to a `SettingValue`. Strings and arrays of strings are preserved as-is; + /// other types are converted into strings as a fallback. This ensures that all settings are represented in a + /// uniform and easily processed manner. + /// + /// - Parameter buildSettings: A dictionary of raw build settings. + /// - Returns: A `SettingsDictionary` containing `SettingValue`-typed settings. + /// - Throws: If a setting value cannot be mapped (this is typically non-fatal; most values can be stringified). + func mapBuildSettings(_ buildSettings: [String: Any]) throws -> SettingsDictionary { + var settingsDict = SettingsDictionary() + for (key, value) in buildSettings { + settingsDict[key] = try mapSettingValue(value) + } + return settingsDict + } + + /// Maps a single raw setting value into a `SettingValue`. + /// + /// - If the value is a `String`, it becomes a `SettingValue.string`. + /// - If the value is an `Array`, each element is converted to a string if possible, resulting in `SettingValue.array`. + /// - Otherwise, the value is stringified using `String(describing:)` and returned as `SettingValue.string`. + /// + /// - Parameter value: A raw setting value from the build settings dictionary. + /// - Returns: A `SettingValue` representing the processed setting. + private func mapSettingValue(_ value: Any) throws -> SettingValue { + if let stringValue = value as? String { + return .string(stringValue) + } else if let arrayValue = value as? [Any] { + let stringArray = arrayValue.compactMap { $0 as? String } + return .array(stringArray) + } else { + // Fallback: convert unknown types to strings + let stringValue = String(describing: value) + return .string(stringValue) + } + } + + /// Determines a `BuildConfiguration.Variant` (e.g., `.debug` or `.release`) from a configuration name. + /// + /// Uses `ConfigurationMatcher` to infer the variant by analyzing the configuration name for known keywords. + /// + /// - Parameter name: The name of the build configuration (e.g., "Debug", "Release", "Development"). + /// - Returns: The corresponding `BuildConfiguration.Variant` inferred from the name. + private func variant( + for name: String, + configurationMatcher: ConfigurationMatching = ConfigurationMatcher() + ) -> BuildConfiguration.Variant { + configurationMatcher.variant(for: name) + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Targets/PBXBuildRuleMapper.swift b/Sources/XcodeProjMapper/Mappers/Targets/PBXBuildRuleMapper.swift new file mode 100644 index 00000000..2d559c61 --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Targets/PBXBuildRuleMapper.swift @@ -0,0 +1,67 @@ +import Foundation +import Path +import XcodeGraph +import XcodeProj + +/// A protocol defining how to map a single `PBXBuildRule` instance into a `BuildRule` domain model. +/// +/// Conforming types transform an individual build rule defined in an Xcode project into a +/// structured `BuildRule` model, enabling further analysis or code generation steps to operate on +/// well-defined representations of build rules. +protocol BuildRuleMapping { + /// Maps a single `PBXBuildRule` into a `BuildRule` model. + /// + /// - Parameter buildRule: The `PBXBuildRule` to map. + /// - Returns: A `BuildRule` model if the compiler spec and file type are recognized; otherwise, `nil`. + /// - Throws: If resolving or mapping the build rule fails. + func map(_ buildRule: PBXBuildRule) throws -> BuildRule? +} + +/// A mapper that converts a `PBXBuildRule` object into a `BuildRule` domain model. +/// +/// `BuildRuleMapper` extracts known compiler specs and file types from the provided build rule. +/// If the compiler spec or file type is unknown, the build rule is ignored (returning `nil`). +struct PBXBuildRuleMapper: BuildRuleMapping { + func map(_ buildRule: PBXBuildRule) throws -> BuildRule? { + let compilerSpec = try mapCompilerSpec(buildRule.compilerSpec) + let fileType = try mapFileType(buildRule.fileType) + + return BuildRule( + compilerSpec: compilerSpec, + fileType: fileType, + filePatterns: buildRule.filePatterns, + name: buildRule.name, + outputFiles: buildRule.outputFiles, + inputFiles: buildRule.inputFiles, + outputFilesCompilerFlags: buildRule.outputFilesCompilerFlags, + script: buildRule.script, + runOncePerArchitecture: buildRule.runOncePerArchitecture + ) + } + + // MARK: - Private Helpers + + private func mapCompilerSpec(_ compilerSpec: String) throws -> BuildRule.CompilerSpec { + try BuildRule.CompilerSpec(rawValue: compilerSpec) + .throwing(PBXBuildRuleMappingError.unknownCompilerSpec(compilerSpec)) + } + + private func mapFileType(_ fileType: String) throws -> BuildRule.FileType { + try BuildRule.FileType(rawValue: fileType) + .throwing(PBXBuildRuleMappingError.unknownFileType(fileType)) + } +} + +enum PBXBuildRuleMappingError: Error, LocalizedError, Equatable { + case unknownFileType(String) + case unknownCompilerSpec(String) + + var errorDescription: String? { + switch self { + case let .unknownFileType(fileType): + return "Unknown file type: \(fileType)" + case let .unknownCompilerSpec(compilerSpec): + return "Unknown compiler spec: \(compilerSpec)" + } + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Targets/PBXTarget+BuildHeaders.swift b/Sources/XcodeProjMapper/Mappers/Targets/PBXTarget+BuildHeaders.swift new file mode 100644 index 00000000..3708f285 --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Targets/PBXTarget+BuildHeaders.swift @@ -0,0 +1,8 @@ +import XcodeProj + +extension PBXTarget { + /// Returns the headers build phase, if any. + func headersBuildPhase() throws -> PBXHeadersBuildPhase? { + buildPhases.compactMap { $0 as? PBXHeadersBuildPhase }.first + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Targets/PBXTarget+BuildSettings.swift b/Sources/XcodeProjMapper/Mappers/Targets/PBXTarget+BuildSettings.swift new file mode 100644 index 00000000..88115db3 --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Targets/PBXTarget+BuildSettings.swift @@ -0,0 +1,88 @@ +import Foundation +import Path +import XcodeGraph +import XcodeProj + +extension PBXTarget { + enum EnvironmentExtractor { + static func extract(from buildSettings: BuildSettings) -> [String: EnvironmentVariable] { + guard let envVars = buildSettings.stringDict(for: .environmentVariables) else { + return [:] + } + return envVars.reduce(into: [:]) { result, pair in + result[pair.key] = EnvironmentVariable(value: pair.value, isEnabled: true) + } + } + } + + /// Retrieves the path to the Info.plist file from the target's build settings. + /// + /// - Returns: The `INFOPLIST_FILE` value if present, otherwise `nil`. + func infoPlistPath() -> [BuildConfiguration: String] { + buildConfigurationList?.stringSettings(for: .infoPlistFile) ?? [:] + } + + /// Retrieves the path to the entitlements file from the target's build settings. + /// + /// - Returns: The `CODE_SIGN_ENTITLEMENTS` value if present, otherwise `nil`. + func entitlementsPath() -> [BuildConfiguration: String] { + buildConfigurationList?.stringSettings(for: .codeSignEntitlements) ?? [:] + } + + func defaultBuildConfiguration( + configurationMatcher: ConfigurationMatching = ConfigurationMatcher() + ) -> BuildConfiguration? { + guard let defaultName = buildConfigurationList?.defaultConfigurationName else { return nil } + let variant = configurationMatcher.variant(for: defaultName) + return BuildConfiguration(name: defaultName, variant: variant) + } + + /// Retrieves deployment target versions for various platforms supported by this target. + /// + /// Checks build configurations for: + /// - `IPHONEOS_DEPLOYMENT_TARGET` + /// - `MACOSX_DEPLOYMENT_TARGET` + /// - `WATCHOS_DEPLOYMENT_TARGET` + /// - `TVOS_DEPLOYMENT_TARGET` + /// - `VISIONOS_DEPLOYMENT_TARGET` + /// + /// - Returns: A `DeploymentTargets` instance containing any discovered versions. + func deploymentTargets() -> DeploymentTargets { + guard let configList = buildConfigurationList else { + return DeploymentTargets(iOS: nil, macOS: nil, watchOS: nil, tvOS: nil, visionOS: nil) + } + + let keys: [BuildSettingKey] = [ + .iPhoneOSDeploymentTarget, + .macOSDeploymentTarget, + .watchOSDeploymentTarget, + .tvOSDeploymentTarget, + .visionOSDeploymentTarget, + ] + + let targets = configList.allDeploymentTargets(keys: keys) + return DeploymentTargets( + iOS: targets[.iPhoneOSDeploymentTarget], + macOS: targets[.macOSDeploymentTarget], + watchOS: targets[.watchOSDeploymentTarget], + tvOS: targets[.tvOSDeploymentTarget], + visionOS: targets[.visionOSDeploymentTarget] + ) + } + + /// Extracts environment variables from all build configurations of the target. + /// + /// If multiple configurations define the same environment variable, the last processed configuration takes precedence. + func extractEnvironmentVariables() -> [String: EnvironmentVariable] { + buildConfigurationList?.buildConfigurations.reduce(into: [:]) { result, config in + result.merge(EnvironmentExtractor.extract(from: config.buildSettings)) { current, _ in current + } + } ?? [:] + } + + /// Returns the build settings from the "Debug" build configuration, or an empty dictionary if not present. + var debugBuildSettings: [String: Any] { + buildConfigurationList?.buildConfigurations.first(where: { $0.name == "Debug" })?.buildSettings + ?? [:] + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Targets/PBXTarget+GraphMapping.swift b/Sources/XcodeProjMapper/Mappers/Targets/PBXTarget+GraphMapping.swift new file mode 100644 index 00000000..98d22726 --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Targets/PBXTarget+GraphMapping.swift @@ -0,0 +1,61 @@ +import Foundation +import Path +import XcodeGraph +import XcodeProj + +extension PBXTarget { + /// Attempts to retrieve the bundle identifier from the target's debug build settings, or throws an error if missing. + func bundleIdentifier() throws -> String { + if let bundleId = debugBuildSettings.string(for: .productBundleIdentifier) { + return bundleId + } + throw PBXTargetMappingError.missingBundleIdentifier(targetName: name) + } + + /// Returns an array of all `PBXCopyFilesBuildPhase` instances for this target. + func copyFilesBuildPhases() -> [PBXCopyFilesBuildPhase] { + buildPhases.compactMap { $0 as? PBXCopyFilesBuildPhase } + } + + func launchArguments() throws -> [LaunchArgument] { + guard let buildConfigList = buildConfigurationList else { return [] } + var launchArguments: [LaunchArgument] = [] + for buildConfig in buildConfigList.buildConfigurations { + if let args = buildConfig.buildSettings.stringArray(for: .launchArguments) { + launchArguments.append(contentsOf: args.map { LaunchArgument(name: $0, isEnabled: true) }) + } + } + return launchArguments.uniqued() + } + + func prune() throws -> Bool { + debugBuildSettings.bool(for: .prune) ?? false + } + + func mergedBinaryType() throws -> MergedBinaryType { + let mergedBinaryTypeString = debugBuildSettings.string(for: .mergedBinaryType) + return mergedBinaryTypeString == "automatic" ? .automatic : .disabled + } + + func mergeable() throws -> Bool { + debugBuildSettings.bool(for: .mergeable) ?? false + } + + func onDemandResourcesTags() throws -> OnDemandResourcesTags? { + // Currently returns nil, could be extended if needed + return nil + } + + func metadata() throws -> TargetMetadata { + var tags: Set = [] + for buildConfig in buildConfigurationList?.buildConfigurations ?? [] { + if let tagsString = buildConfig.buildSettings.string(for: .tags) { + let extractedTags = tagsString + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespaces) } + tags.formUnion(extractedTags) + } + } + return .metadata(tags: tags) + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Targets/PBXTarget+PlatformInference.swift b/Sources/XcodeProjMapper/Mappers/Targets/PBXTarget+PlatformInference.swift new file mode 100644 index 00000000..e67e33fd --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Targets/PBXTarget+PlatformInference.swift @@ -0,0 +1,90 @@ +import Foundation +import Path +import XcodeGraph +import XcodeProj + +enum PlatformInferenceError: LocalizedError, Equatable { + case noPlatformInferred(String) + + var errorDescription: String? { + switch self { + case let .noPlatformInferred(name): + return "No platform could be inferred from target '\(name)'." + } + } +} + +extension PBXTarget { + /// Determines the set of `Destinations` supported by this target. + /// + /// Attempts to identify platforms from: + /// 1. `SDKROOT` (if present) + /// 2. Deployment targets + /// 3. Product type as a final fallback + /// + /// Supports multi-platform scenarios by unioning destinations from all inferred platforms. + /// + /// - Returns: A `Destinations` set representing all supported destinations. + /// - Throws: If retrieving deployment targets fails. + func platform() throws -> Destinations { + if let sdkNames = buildConfigurationList?.stringSettings(for: .sdkroot), + let sdkName = sdkNames.values.first, + let root = Platform(sdkroot: sdkName) + { + return root.destinations + } else { + return try inferPlatformFromTarget() + } + } + + /// Infers the platform from deployment targets if `SDKROOT` is not set or recognized. + /// + /// Aggregates all platforms indicated by the deployment targets. If none are found, + /// picks a default based on product type: + /// - iOS for most apps, clips, and app extensions. + /// - macOS for frameworks, libraries, command line tools, macros, xpc, system extensions. + /// - tvOS for tvTopShelfExtension. + /// - iOS otherwise. + /// + /// - Returns: A `Destinations` set representing all inferred destinations. + /// - Throws: If retrieving deployment targets fails. + private func inferPlatformFromTarget() throws -> Destinations { + let deploymentTargets = deploymentTargets() + var result = Destinations() + + if deploymentTargets.iOS != nil { + result.formUnion(Platform.iOS.destinations) + } + if deploymentTargets.macOS != nil { + result.formUnion(Platform.macOS.destinations) + } + if deploymentTargets.watchOS != nil { + result.formUnion(Platform.watchOS.destinations) + } + if deploymentTargets.tvOS != nil { + result.formUnion(Platform.tvOS.destinations) + } + if deploymentTargets.visionOS != nil { + result.formUnion(Platform.visionOS.destinations) + } + + guard result.isEmpty else { return result } + + let productType = productType?.mapProductType() + let product = try productType.throwing(PlatformInferenceError.noPlatformInferred(name)) + + switch product { + case .app, .stickerPackExtension, .appClip, .appExtension: + return Platform.iOS.destinations + case .framework, .staticLibrary, .dynamicLibrary, .commandLineTool, .macro, .xpc, + .systemExtension: + return Platform.macOS.destinations + case .tvTopShelfExtension: + return Platform.tvOS.destinations + case .watch2App, .watch2Extension: + return Platform.watchOS.destinations + default: + return Platform.iOS.destinations + } + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Targets/PBXTargetDependency+PlatformCondition.swift b/Sources/XcodeProjMapper/Mappers/Targets/PBXTargetDependency+PlatformCondition.swift new file mode 100644 index 00000000..84bdfaaa --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Targets/PBXTargetDependency+PlatformCondition.swift @@ -0,0 +1,19 @@ +import XcodeGraph +import XcodeProj + +/// A mapper for platform-related conditions, extracting platform filters from `PBXTargetDependency`. +extension PBXTargetDependency { + /// Maps the platform filters on a given `PBXTargetDependency` into a `PlatformCondition`. + /// + /// Returns `nil` if no filters apply, meaning the dependency isn't restricted by platform and + /// should be considered available on all platforms. + func platformCondition() -> PlatformCondition? { + var filters = Set(platformFilters ?? []) + if let singleFilter = platformFilter { + filters.insert(singleFilter) + } + + let platformFilters = Set(filters.compactMap { PlatformFilter(rawValue: $0) }) + return PlatformCondition.when(platformFilters) + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Targets/PBXTargetDependencyMapper.swift b/Sources/XcodeProjMapper/Mappers/Targets/PBXTargetDependencyMapper.swift new file mode 100644 index 00000000..f06e4eb1 --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Targets/PBXTargetDependencyMapper.swift @@ -0,0 +1,181 @@ +import Foundation +import Path +import XcodeGraph +import XcodeProj + +/// A protocol defining how to map a single `PBXTargetDependency` into a `TargetDependency` model. +/// +/// Conforming types handle all known dependency types—direct targets, package products, +/// proxy references (which may point to other targets or external projects), and file-based dependencies. +protocol PBXTargetDependencyMapping { + /// Maps a single `PBXTargetDependency` into a `TargetDependency` model. + /// + /// - Parameters: + /// - dependency: The `PBXTargetDependency` to map. + /// - xcodeProj: Provides the `.xcodeproj` data and source directory paths. + /// - Returns: A `TargetDependency` if the dependency can be resolved. + /// - Throws: If the dependency references invalid paths or targets that cannot be resolved. + func map(_ dependency: PBXTargetDependency, xcodeProj: XcodeProj) throws -> TargetDependency +} + +/// A unified mapper that handles all types of `PBXTargetDependency` instances. +/// +/// `PBXTargetDependencyMapper` checks if the dependency references a direct target, a package product, +/// or a proxy. For proxy dependencies, it may resolve references to another target, a project, +/// or file-based dependencies (frameworks, libraries, etc.). If a dependency cannot be resolved +/// to a known domain model, it throws an error. +struct PBXTargetDependencyMapper: PBXTargetDependencyMapping { + private let pathMapper: PathDependencyMapping + + init(pathMapper: PathDependencyMapping = PathDependencyMapper()) { + self.pathMapper = pathMapper + } + + func map(_ dependency: PBXTargetDependency, xcodeProj: XcodeProj) throws -> TargetDependency { + let condition = dependency.platformCondition() + + // 1. Direct target dependency + if let target = dependency.target { + return .target(name: target.name, status: .required, condition: condition) + } + + // 2. Package product dependency + if let product = dependency.product { + return .package( + product: product.productName, + type: .runtime, + condition: condition + ) + } + + // 3. Proxy dependency + if let targetProxy = dependency.targetProxy { + switch targetProxy.proxyType { + case .nativeTarget: + return try mapNativeTargetProxy(targetProxy, condition: condition, xcodeProj: xcodeProj) + case .reference: + return try mapReferenceProxy(targetProxy, condition: condition, xcodeProj: xcodeProj) + case .other, .none: + throw TargetDependencyMappingError.unsupportedProxyType(dependency.name) + } + } + + // If none of the above matched, it's an unknown dependency type. + throw TargetDependencyMappingError.unknownDependencyType( + name: dependency.name ?? "Unknown dependency name" + ) + } + + // MARK: - Private Helpers + + private func mapNativeTargetProxy( + _ targetProxy: PBXContainerItemProxy, + condition: PlatformCondition?, + xcodeProj: XcodeProj + ) throws -> TargetDependency { + let remoteInfo = try targetProxy.remoteInfo.throwing( + TargetDependencyMappingError.missingRemoteInfoInNativeProxy + ) + + switch targetProxy.containerPortal { + case .project: + // Direct reference to another target in the same project. + return .target(name: remoteInfo, status: .required, condition: condition) + case let .fileReference(fileReference): + let projectRelativePath = try fileReference.path + .throwing(TargetDependencyMappingError.missingFileReference(fileReference.name ?? "")) + + let path = xcodeProj.srcPath.appending(component: projectRelativePath) + // Reference to a target in another project. + return .project(target: remoteInfo, path: path, status: .required, condition: condition) + case let .unknownObject(object): + throw TargetDependencyMappingError.unknownObject(object.debugDescription) + } + } + + private func mapReferenceProxy( + _ targetProxy: PBXContainerItemProxy, + condition: PlatformCondition?, + xcodeProj: XcodeProj + ) throws -> TargetDependency { + let remoteGlobalID = try targetProxy.remoteGlobalID.throwing( + TargetDependencyMappingError.missingRemoteGlobalIDInReferenceProxy + ) + + switch remoteGlobalID { + case let .object(object): + // File-based dependency + if let fileRef = object as? PBXFileReference { + return try mapFileDependency( + pathString: fileRef.path, + condition: condition, + xcodeProj: xcodeProj + ) + } else if let refProxy = object as? PBXReferenceProxy { + return try mapFileDependency( + pathString: refProxy.path, + condition: condition, + xcodeProj: xcodeProj + ) + } + throw TargetDependencyMappingError.unknownObject("\(object)") + + case .string: + // If remoteGlobalID is just a string, we can’t map a file or target from it. + throw TargetDependencyMappingError.unknownDependencyType( + name: "remoteGlobalID is a string, cannot map a known target or file reference." + ) + } + } + + /// Maps file-based dependencies (e.g., frameworks, libraries) into `TargetDependency` models. + /// - Parameters: + /// - pathString: The path string for the file-based dependency (relative or absolute). + /// - condition: An optional platform condition. + /// - xcodeProj: The Xcode project reference for resolving the directory structure. + /// - Returns: A `TargetDependency` reflecting the file’s extension (framework, library, etc.). + /// - Throws: If the path is missing or invalid. + private func mapFileDependency( + pathString: String?, + condition: PlatformCondition?, + xcodeProj: XcodeProj + ) throws -> TargetDependency { + let pathString = try pathString.throwing( + TargetDependencyMappingError.missingFileReference("Path string is nil in file dependency.") + ) + let path = xcodeProj.srcPath.appending(try RelativePath(validating: pathString)) + return try pathMapper.map(path: path, condition: condition) + } +} + +// MARK: - Errors + +/// Errors that may occur when mapping `PBXTargetDependency` instances. +enum TargetDependencyMappingError: LocalizedError, Equatable { + case targetNotFound(targetName: String, path: AbsolutePath) + case unknownDependencyType(name: String) + case missingFileReference(String) + case unknownObject(String) + case missingRemoteInfoInNativeProxy + case missingRemoteGlobalIDInReferenceProxy + case unsupportedProxyType(String?) + + var errorDescription: String? { + switch self { + case let .targetNotFound(targetName, path): + return "The target '\(targetName)' could not be found in the project at: \(path.pathString)." + case let .unknownDependencyType(name): + return "An unknown dependency type '\(name)' was encountered." + case let .missingFileReference(description): + return "File reference path is missing in target dependency: \(description)." + case let .unknownObject(description): + return "Encountered an unknown PBXObject in target dependency: \(description)." + case .missingRemoteInfoInNativeProxy: + return "A native target proxy is missing `remoteInfo` in target dependency." + case .missingRemoteGlobalIDInReferenceProxy: + return "A reference proxy is missing `remoteGlobalID` in target dependency." + case let .unsupportedProxyType(name): + return "Encountered an unsupported PBXProxyType in dependency: \(name ?? "Unknown")." + } + } +} diff --git a/Sources/XcodeProjMapper/Mappers/Targets/PBXTargetMapper.swift b/Sources/XcodeProjMapper/Mappers/Targets/PBXTargetMapper.swift new file mode 100644 index 00000000..432d0c65 --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Targets/PBXTargetMapper.swift @@ -0,0 +1,336 @@ +import FileSystem +import Foundation +import Path +import XcodeGraph +import XcodeProj + +/// Errors that may occur while mapping a `PBXTarget` into a domain-level `Target`. +enum PBXTargetMappingError: LocalizedError, Equatable { + case noProjectsFound(path: String) + case missingFilesGroup(targetName: String) + case invalidPlist(path: String) + case missingBundleIdentifier(targetName: String) + + var errorDescription: String? { + switch self { + case let .noProjectsFound(path): + return "No project was found at: \(path)." + case let .missingFilesGroup(targetName): + return "The files group is missing for the target '\(targetName)'." + case let .invalidPlist(path): + return "Failed to read a valid plist dictionary from file at: \(path)." + case let .missingBundleIdentifier(targetName): + return "The bundle identifier is missing for the target '\(targetName)'." + } + } +} + +/// A protocol defining how to map a `PBXTarget` into a domain-level `Target` model. +/// +/// Conforming types transform raw `PBXTarget` instances—including their build phases, +/// settings, and dependencies—into fully realized `Target` models suitable for analysis, +/// code generation, or tooling integration. +protocol PBXTargetMapping { + /// Maps a given `PBXTarget` into a `Target` model. + /// + /// This involves: + /// - Extracting platform, product, and deployment information. + /// - Mapping build phases (sources, resources, headers, scripts, copy files, frameworks, etc.). + /// - Resolving dependencies (project-based, frameworks, libraries, packages, SDKs). + /// - Reading settings, launch arguments, and metadata. + /// + /// - Parameters: + /// - pbxTarget: The `PBXTarget` to map. + /// - xcodeProj: Provides access to `.xcodeproj` data and the source directory for path resolution. + /// - Returns: A fully mapped `Target` model. + /// - Throws: `PBXTargetMappingError` if required data (like a bundle identifier) is missing, + /// or if necessary files/groups cannot be found. + func map(pbxTarget: PBXTarget, xcodeProj: XcodeProj) async throws -> Target +} + +// swiftlint:disable function_body_length +/// A mapper that converts a `PBXTarget` into a domain `Target` model. +/// +/// `PBXTargetMapper` orchestrates various specialized mappers (e.g., sources, resources, headers) +/// and dependency resolvers to produce a comprehensive `Target` suitable for downstream tasks. +struct PBXTargetMapper: PBXTargetMapping { + private let settingsMapper: SettingsMapping + private let sourcesMapper: PBXSourcesBuildPhaseMapping + private let resourcesMapper: PBXResourcesBuildPhaseMapping + private let headersMapper: PBXHeadersBuildPhaseMapping + private let scriptsMapper: PBXScriptsBuildPhaseMapping + private let copyFilesMapper: PBXCopyFilesBuildPhaseMapping + private let coreDataModelsMapper: PBXCoreDataModelsBuildPhaseMapping + private let frameworksMapper: PBXFrameworksBuildPhaseMapping + private let dependencyMapper: PBXTargetDependencyMapping + private let buildRuleMapper: BuildRuleMapping + + init( + settingsMapper: SettingsMapping = XCConfigurationMapper(), + sourcesMapper: PBXSourcesBuildPhaseMapping = PBXSourcesBuildPhaseMapper(), + resourcesMapper: PBXResourcesBuildPhaseMapping = PBXResourcesBuildPhaseMapper(), + headersMapper: PBXHeadersBuildPhaseMapping = PBXHeadersBuildPhaseMapper(), + scriptsMapper: PBXScriptsBuildPhaseMapping = PBXScriptsBuildPhaseMapper(), + copyFilesMapper: PBXCopyFilesBuildPhaseMapping = PBXCopyFilesBuildPhaseMapper(), + coreDataModelsMapper: PBXCoreDataModelsBuildPhaseMapping = PBXCoreDataModelsBuildPhaseMapper(), + frameworksMapper: PBXFrameworksBuildPhaseMapping = PBXFrameworksBuildPhaseMapper(), + dependencyMapper: PBXTargetDependencyMapping = PBXTargetDependencyMapper(), + buildRuleMapper: BuildRuleMapping = PBXBuildRuleMapper() + ) { + self.settingsMapper = settingsMapper + self.sourcesMapper = sourcesMapper + self.resourcesMapper = resourcesMapper + self.headersMapper = headersMapper + self.scriptsMapper = scriptsMapper + self.copyFilesMapper = copyFilesMapper + self.coreDataModelsMapper = coreDataModelsMapper + self.frameworksMapper = frameworksMapper + self.dependencyMapper = dependencyMapper + self.buildRuleMapper = buildRuleMapper + } + + func map(pbxTarget: PBXTarget, xcodeProj: XcodeProj) async throws -> Target { + let platform = try pbxTarget.platform() + let deploymentTargets = pbxTarget.deploymentTargets() + let productType = pbxTarget.productType?.mapProductType() + let product = try productType.throwing(PlatformInferenceError.noPlatformInferred(pbxTarget.name)) + + // Project settings and configurations + let settings = try settingsMapper.map( + xcodeProj: xcodeProj, + configurationList: pbxTarget.buildConfigurationList + ) + + // Build Phases + let sources = try pbxTarget.sourcesBuildPhase().map { + try sourcesMapper.map($0, xcodeProj: xcodeProj) + } ?? [] + + let resources = try pbxTarget.resourcesBuildPhase().map { + try resourcesMapper.map($0, xcodeProj: xcodeProj) + } ?? [] + + let headers = try pbxTarget.headersBuildPhase().map { + try headersMapper.map($0, xcodeProj: xcodeProj) + } ?? nil + + let runScriptPhases = pbxTarget.runScriptBuildPhases() + let scripts = try scriptsMapper.map(runScriptPhases, buildPhases: pbxTarget.buildPhases) + let rawScriptBuildPhases = scriptsMapper.mapRawScriptBuildPhases(runScriptPhases) + + let copyFilesPhases = pbxTarget.copyFilesBuildPhases() + let copyFiles = try copyFilesMapper.map(copyFilesPhases, xcodeProj: xcodeProj) + + // Core Data models + let resourceFiles = try pbxTarget.resourcesBuildPhase()?.files ?? [] + let coreDataModels = try coreDataModelsMapper.map(resourceFiles, xcodeProj: xcodeProj) + + // Frameworks & libraries + let frameworksPhase = try pbxTarget.frameworksBuildPhase() + let frameworks = try frameworksPhase.map { + try frameworksMapper.map($0, xcodeProj: xcodeProj) + } ?? [] + + // Additional files (not in build phases) + let additionalFiles = try mapAdditionalFiles(from: pbxTarget, xcodeProj: xcodeProj) + + // Resource elements + let resourceFileElements = ResourceFileElements(resources) + + // Build Rules + let buildRules = try pbxTarget.buildRules.compactMap { try buildRuleMapper.map($0) } + + // Environment & Launch + let environmentVariables = pbxTarget.extractEnvironmentVariables() + let launchArguments = try pbxTarget.launchArguments() + + // Files group + let filesGroup = try extractFilesGroup(from: pbxTarget, xcodeProj: xcodeProj) + + // Swift Playgrounds + let playgrounds = try extractPlaygrounds(from: pbxTarget, xcodeProj: xcodeProj) + + // Misc + let prune = try pbxTarget.prune() + let mergedBinaryType = try pbxTarget.mergedBinaryType() + let mergeable = try pbxTarget.mergeable() + let onDemandResourcesTags = try pbxTarget.onDemandResourcesTags() + let metadata = try pbxTarget.metadata() + + // Dependencies + let targetDependencies = try pbxTarget.dependencies.compactMap { + try dependencyMapper.map($0, xcodeProj: xcodeProj) + } + let allDependencies = (targetDependencies + frameworks).sorted { $0.name < $1.name } + + // Construct final Target + return Target( + name: pbxTarget.name, + destinations: platform, + product: product, + productName: pbxTarget.productName ?? pbxTarget.name, + bundleId: try pbxTarget.bundleIdentifier(), + deploymentTargets: deploymentTargets, + infoPlist: try await extractInfoPlist(from: pbxTarget, xcodeProj: xcodeProj), + entitlements: try extractEntitlements(from: pbxTarget, xcodeProj: xcodeProj), + settings: settings, + sources: sources, + resources: resourceFileElements, + copyFiles: copyFiles, + headers: headers, + coreDataModels: coreDataModels, + scripts: scripts, + environmentVariables: environmentVariables, + launchArguments: launchArguments, + filesGroup: filesGroup, + dependencies: allDependencies, + rawScriptBuildPhases: rawScriptBuildPhases, + playgrounds: playgrounds, + additionalFiles: additionalFiles, + buildRules: buildRules, + prune: prune, + mergedBinaryType: mergedBinaryType, + mergeable: mergeable, + onDemandResourcesTags: onDemandResourcesTags, + metadata: metadata + ) + } + + // MARK: - Private helpers + + /// Identifies files not included in any build phase, returning them as `FileElement` models. + private func mapAdditionalFiles(from pbxTarget: PBXTarget, xcodeProj: XcodeProj) throws -> [FileElement] { + guard let pbxProject = xcodeProj.pbxproj.projects.first, + let mainGroup = pbxProject.mainGroup + else { + throw PBXTargetMappingError.noProjectsFound(path: xcodeProj.projectPath.pathString) + } + + let allFiles = try collectAllFiles(from: mainGroup, xcodeProj: xcodeProj) + let filesInBuildPhases = try filesReferencedByBuildPhases(pbxTarget: pbxTarget, xcodeProj: xcodeProj) + let additionalFiles = allFiles.subtracting(filesInBuildPhases).sorted() + return additionalFiles.map { FileElement.file(path: $0) } + } + + /// Extracts the main files group for the target. + private func extractFilesGroup(from target: PBXTarget, xcodeProj: XcodeProj) throws -> ProjectGroup { + guard let pbxProject = xcodeProj.pbxproj.projects.first, + let mainGroup = pbxProject.mainGroup + else { + throw PBXTargetMappingError.missingFilesGroup(targetName: target.name) + } + return ProjectGroup.group(name: mainGroup.name ?? "MainGroup") + } + + /// Extracts and parses the project's Info.plist as a dictionary, or returns an empty dictionary if none is found. + private func extractInfoPlist(from target: PBXTarget, xcodeProj: XcodeProj) async throws -> InfoPlist { + if let (config, plistPath) = target.infoPlistPath().first { + let path = xcodeProj.srcPath.appending(try RelativePath(validating: plistPath)) + let plistDictionary = try await readPlistAsDictionary(at: path) + return .dictionary(plistDictionary, configuration: config) + } + return .dictionary([:]) + } + + /// Extracts the target's entitlements file, if present. + private func extractEntitlements(from target: PBXTarget, xcodeProj: XcodeProj) throws -> Entitlements? { + let entitlementsMap = target.entitlementsPath() + guard let configuration = target.defaultBuildConfiguration() ?? entitlementsMap.keys.first else { return nil } + guard let entitlementsPath = entitlementsMap[configuration] else { return nil } + + let path = xcodeProj.srcPath.appending(try RelativePath(validating: entitlementsPath)) + return Entitlements.file(path: path, configuration: configuration) + } + + /// Recursively collects all files from a given `PBXGroup`. + private func collectAllFiles(from group: PBXGroup, xcodeProj: XcodeProj) throws -> Set { + var files = Set() + for child in group.children { + if let file = child as? PBXFileReference, + let pathString = try file.fullPath(sourceRoot: xcodeProj.srcPathString) + { + let path = try AbsolutePath(validating: pathString) + files.insert(path) + } else if let subgroup = child as? PBXGroup { + files.formUnion(try collectAllFiles(from: subgroup, xcodeProj: xcodeProj)) + } + } + return files + } + + /// Identifies all files referenced by any build phase in the target. + private func filesReferencedByBuildPhases( + pbxTarget: PBXTarget, + xcodeProj: XcodeProj + ) throws -> Set { + let filePaths = try pbxTarget.buildPhases + .compactMap(\.files) + .flatMap { $0 } + .compactMap { buildFile -> AbsolutePath? in + guard let fileRef = buildFile.file, + let filePath = try fileRef.fullPath(sourceRoot: xcodeProj.srcPathString) + else { + return nil + } + return try AbsolutePath(validating: filePath) + } + return Set(filePaths) + } + + /// Extracts playground files from the target's sources. + private func extractPlaygrounds(from pbxTarget: PBXTarget, xcodeProj: XcodeProj) throws -> [AbsolutePath] { + let sources = try pbxTarget.sourcesBuildPhase().map { + try sourcesMapper.map($0, xcodeProj: xcodeProj) + } ?? [] + return sources.filter { $0.path.fileExtension == .playground }.map(\.path) + } + + /// Reads and parses a plist file into a `[String: Plist.Value]` dictionary. + private func readPlistAsDictionary( + at path: AbsolutePath, + fileSystem: FileSysteming = FileSystem() + ) async throws -> [String: Plist.Value] { + var format = PropertyListSerialization.PropertyListFormat.xml + + let data = try await fileSystem.readFile(at: path) + + guard let plist = try? PropertyListSerialization.propertyList( + from: data, + options: .mutableContainersAndLeaves, + format: &format + ) as? [String: Any] else { + throw PBXTargetMappingError.invalidPlist(path: path.pathString) + } + + return try plist.reduce(into: [String: Plist.Value]()) { result, item in + result[item.key] = try convertToPlistValue(item.value) + } + } + + /// Converts a raw plist value into a `Plist.Value`. + private func convertToPlistValue(_ value: Any) throws -> Plist.Value { + switch value { + case let stringValue as String: + return .string(stringValue) + case let intValue as Int: + return .integer(intValue) + case let doubleValue as Double: + return .real(doubleValue) + case let boolValue as Bool: + return .boolean(boolValue) + case let arrayValue as [Any]: + let converted = try arrayValue.map { try convertToPlistValue($0) } + return .array(converted) + case let dictValue as [String: Any]: + let converted = try dictValue.reduce(into: [String: Plist.Value]()) { dictResult, entry in + dictResult[entry.key] = try convertToPlistValue(entry.value) + } + return .dictionary(converted) + default: + // If unrecognized, store its string description + return .string(String(describing: value)) + } + } +} + +// swiftlint:enable function_body_length diff --git a/Sources/XcodeProjMapper/Mappers/Targets/TargetDependency+GraphMapping.swift b/Sources/XcodeProjMapper/Mappers/Targets/TargetDependency+GraphMapping.swift new file mode 100644 index 00000000..b0d65102 --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Targets/TargetDependency+GraphMapping.swift @@ -0,0 +1,273 @@ +import Foundation +import Path +import XcodeGraph +import XcodeMetadata +import XcodeProj + +// swiftlint:disable function_body_length +extension TargetDependency { + /// Maps this `TargetDependency` to a `GraphDependency` by resolving paths, product types, + /// and linking details. Project-based dependencies are resolved using `allTargetsMap`. + /// + /// - Parameters: + /// - sourceDirectory: The root directory for resolving relative paths. + /// - allTargetsMap: A map of target names to `Target` models for resolving project-based dependencies. + /// - target: The target of this dependency. + /// - xcframeworkMetadataProvider: Provides metadata (linking, architectures, etc.) for `.xcframework` dependencies. + /// - libraryMetadataProvider: Provides metadata for libraries. + /// - frameworkMetadataProvider: Provides metadata for frameworks. + /// - systemFrameworkMetadataProvider: Provides metadata for system frameworks. + /// - developerDirectoryProvider: Provides xcode developer directory. + /// - Returns: A corresponding `GraphDependency` model for this dependency. + /// - Throws: `TargetDependencyMappingError` if a referenced target is not found or if the dependency type is unknown. + func graphDependency( + sourceDirectory: AbsolutePath, + allTargetsMap: [String: Target], + target: Target, + xcframeworkMetadataProvider: XCFrameworkMetadataProviding = XCFrameworkMetadataProvider(), + libraryMetadataProvider: LibraryMetadataProviding = LibraryMetadataProvider(), + frameworkMetadataProvider: FrameworkMetadataProviding = FrameworkMetadataProvider(), + systemFrameworkMetadataProvider: SystemFrameworkMetadataProviding = SystemFrameworkMetadataProvider(), + developerDirectoryProvider: DeveloperDirectoryProviding = DeveloperDirectoryProvider() + ) async throws -> GraphDependency { + switch self { + // MARK: - Simple Cases + + case let .target(name, status, _): + return .target(name: name, path: sourceDirectory, status: status) + + case let .project(targetName, projectPath, status, _): + return try mapProjectGraphDependency( + projectPath: projectPath, + targetName: targetName, + status: status, + allTargetsMap: allTargetsMap + ) + + // MARK: - Precompiled Binary Cases + + case let .framework(path, status, _): + let metadata = try await frameworkMetadataProvider.loadMetadata(at: path, status: status) + return .framework( + path: path, + binaryPath: metadata.binaryPath, + dsymPath: metadata.dsymPath, + bcsymbolmapPaths: metadata.bcsymbolmapPaths, + linking: metadata.linking, + architectures: metadata.architectures, + status: status + ) + + case let .xcframework(path, status, _): + let metadata = try await xcframeworkMetadataProvider.loadMetadata(at: path, status: status) + return .xcframework( + .init( + path: path, + infoPlist: metadata.infoPlist, + linking: metadata.linking, + mergeable: metadata.mergeable, + status: status, + macroPath: metadata.macroPath, + swiftModules: metadata.swiftModules, + moduleMaps: metadata.moduleMaps + ) + ) + + case let .library(path, publicHeaders, swiftModuleMap, _): + let metadata = try await libraryMetadataProvider.loadMetadata( + at: path, + publicHeaders: publicHeaders, + swiftModuleMap: swiftModuleMap + ) + return .library( + path: path, + publicHeaders: publicHeaders, + linking: metadata.linking, + architectures: metadata.architectures, + swiftModuleMap: swiftModuleMap + ) + + // MARK: - Package & SDK + + case let .package(product, type, _): + return .packageProduct( + path: sourceDirectory, + product: product, + type: type.graphPackageType + ) + + case let .sdk(name, status, _): + return .sdk( + name: name, + path: sourceDirectory, + status: status, + source: .developer + ) + + // MARK: - XCTest (System Provided) + + case .xctest: + let frameworkData = try systemFrameworkMetadataProvider.loadMetadata( + sdkName: "XCTest.framework", + status: .required, + platform: target.legacyPlatform, + source: .developer + ) + let developerDirectory = try await developerDirectoryProvider.developerDirectory() + let path = try AbsolutePath( + validating: developerDirectory.pathString + frameworkData + .path.pathString + ) + + let metadata = try await frameworkMetadataProvider.loadMetadata(at: path, status: .required) + return .framework( + path: path, + binaryPath: metadata.binaryPath, + dsymPath: metadata.dsymPath, + bcsymbolmapPaths: metadata.bcsymbolmapPaths, + linking: metadata.linking, + architectures: metadata.architectures, + status: .required + ) + } + } + + /// Resolves a project-based target dependency into a `GraphDependency`. + /// + /// - Parameters: + /// - projectPath: The absolute path of the `.xcodeproj` directory. + /// - targetName: The name of the target within that project. + /// - status: The linking status of the dependency. + /// - allTargetsMap: A dictionary of target names to `Target` models for resolution. + /// - Returns: A `GraphDependency` representing the resolved dependency. + /// - Throws: `TargetDependencyMappingError.targetNotFound` if `targetName` isn't in `allTargetsMap`, + /// `TargetDependencyMappingError.unknownDependencyType` if the product type can't be mapped. + private func mapProjectGraphDependency( + projectPath: AbsolutePath, + targetName: String, + status: LinkingStatus, + allTargetsMap: [String: Target] + ) throws -> GraphDependency { + guard let target = allTargetsMap[targetName] else { + throw TargetDependencyMappingError.targetNotFound( + targetName: targetName, + path: projectPath + ) + } + + let product = target.product + switch product { + case .framework, .staticFramework: + let linking: BinaryLinking = (product == .staticFramework) ? .static : .dynamic + return .framework( + path: projectPath, + binaryPath: projectPath.appending(component: "\(targetName).framework"), + dsymPath: nil, + bcsymbolmapPaths: [], + linking: linking, + architectures: [], + status: status + ) + + case .staticLibrary, .dynamicLibrary: + let linking: BinaryLinking = (product == .staticLibrary) ? .static : .dynamic + let libName = (linking == .static) ? "lib\(targetName).a" : "lib\(targetName).dylib" + let publicHeadersPath = projectPath.appending(component: "include") + return .library( + path: projectPath.appending(component: libName), + publicHeaders: publicHeadersPath, + linking: linking, + architectures: [], + swiftModuleMap: nil + ) + + case .bundle: + return .bundle(path: projectPath.appending(component: "\(targetName).bundle")) + + case .app, .commandLineTool: + return .target(name: targetName, path: projectPath, status: status) + + default: + throw TargetDependencyMappingError.unknownDependencyType(name: product.description) + } + } +} + +/// Resolves the system-provided `XCTest.framework` path, falling back to a standard location +/// inside the selected Xcode’s SharedFrameworks directory. +/// +/// - Parameter developerDirectoryProvider: Provides the current Xcode Developer directory (via `xcode-select -p`). +/// - Returns: The absolute path to `XCTest.framework`. +/// - Throws: If the developer directory cannot be retrieved or validated. +private func xctestFrameworkPath( + developerDirectoryProvider: DeveloperDirectoryProviding = DeveloperDirectoryProvider() +) async throws -> AbsolutePath { + let devDir = try await developerDirectoryProvider.developerDirectory() + // Typically: /Applications/Xcode.app/Contents/Developer + // Move one directory up (/Contents) and then into SharedFrameworks/XCTest.framework + return devDir.parentDirectory.appending(components: ["SharedFrameworks", "XCTest.framework"]) +} + +extension TargetDependency.PackageType { + /// Translates `TargetDependency.PackageType` into `GraphDependency.PackageProductType`. + var graphPackageType: GraphDependency.PackageProductType { + switch self { + case .runtime: + return .runtime + case .runtimeEmbedded: + return .runtimeEmbedded + case .plugin: + return .plugin + case .macro: + return .macro + } + } +} + +extension PBXProductType { + /// Maps `PBXProductType` to a `Product`, or returns `nil` if unsupported. + func mapProductType() -> Product? { + switch self { + case .application, .messagesApplication, .onDemandInstallCapableApplication: + return .app + case .framework, .xcFramework: + return .framework + case .staticFramework: + return .staticFramework + case .dynamicLibrary: + return .dynamicLibrary + case .staticLibrary, .metalLibrary: + return .staticLibrary + case .bundle, .ocUnitTestBundle: + return .bundle + case .unitTestBundle: + return .unitTests + case .uiTestBundle: + return .uiTests + case .appExtension: + return .appExtension + case .extensionKitExtension, .xcodeExtension: + return .extensionKitExtension + case .commandLineTool: + return .commandLineTool + case .messagesExtension: + return .messagesExtension + case .stickerPack: + return .stickerPackExtension + case .xpcService: + return .xpc + case .watchApp, .watch2App, .watch2AppContainer: + return .watch2App + case .watchExtension, .watch2Extension: + return .watch2Extension + case .tvExtension: + return .tvTopShelfExtension + case .systemExtension: + return .systemExtension + case .instrumentsPackage, .intentsServiceExtension, .driverExtension, .none: + return nil + } + } +} + +// swiftlint:enable function_body_length diff --git a/Sources/XcodeProjMapper/Mappers/Workspace/XCWorkspaceMapper.swift b/Sources/XcodeProjMapper/Mappers/Workspace/XCWorkspaceMapper.swift new file mode 100644 index 00000000..db6e15be --- /dev/null +++ b/Sources/XcodeProjMapper/Mappers/Workspace/XCWorkspaceMapper.swift @@ -0,0 +1,130 @@ +import Foundation +import Path +import PathKit +import XcodeGraph +import XcodeProj + +/// A protocol defining how to map an `.xcworkspace` into a `Workspace` model. +/// +/// Conforming types extract project references, shared schemes, and any other relevant data +/// to produce a high-level `Workspace` domain model. +protocol WorkspaceMapping { + /// Maps the `.xcworkspace` into a `Workspace` domain model. + /// + /// This includes: + /// - Identifying `.xcodeproj` references in the workspace data structure. + /// - Mapping any shared schemes present in `xcshareddata/xcschemes`. + /// + /// - Parameter xcworkspace: The `XCWorkspace` to be mapped. + /// - Returns: A fully constructed `Workspace` representing the workspace’s structure. + /// - Throws: If reading projects or schemes fails. + func map(xcworkspace: XCWorkspace) async throws -> Workspace +} + +/// A mapper that converts an `.xcworkspace` into a `Workspace` model. +/// +/// `XCWorkspaceMapper`: +/// - Locates all referenced Xcode projects (`.xcodeproj`) in the workspace data, +/// - Maps shared schemes (if any), +/// - Produces a `Workspace` model suitable for analysis or code generation. +struct XCWorkspaceMapper: WorkspaceMapping { + private let schemeMapper: SchemeMapping + + /// Creates a new mapper that uses the provided `SchemeMapping` instance for scheme parsing. + /// - Parameter schemeMapper: Defaults to `XCSchemeMapper()` for scheme resolution. + init(schemeMapper: SchemeMapping = XCSchemeMapper()) { + self.schemeMapper = schemeMapper + } + + func map(xcworkspace: XCWorkspace) async throws -> Workspace { + let xcWorkspacePath = xcworkspace.workspacePath + let srcPath = xcWorkspacePath.parentDirectory + + let projectPaths = try await extractProjectPaths( + from: xcworkspace.data.children, + srcPath: srcPath + ) + + let workspaceName = xcWorkspacePath.basenameWithoutExt + let schemes = try mapSchemes(from: xcworkspace) + + let generationOptions = Workspace.GenerationOptions( + enableAutomaticXcodeSchemes: nil, + autogeneratedWorkspaceSchemes: .disabled, + lastXcodeUpgradeCheck: nil, + renderMarkdownReadme: false + ) + + return Workspace( + path: srcPath, + xcWorkspacePath: xcWorkspacePath, + name: workspaceName, + projects: projectPaths, + schemes: schemes, + generationOptions: generationOptions, + ideTemplateMacros: nil, + additionalFiles: [] + ) + } + + // MARK: - Private Helpers + + /// Recursively identifies all `.xcodeproj` files within the workspace structure. + /// + /// - Parameters: + /// - elements: The workspace elements (files/groups). + /// - srcPath: The workspace’s root directory for resolving relative references. + /// - Returns: An array of absolute paths to `.xcodeproj` directories found in the workspace. + private func extractProjectPaths( + from elements: [XCWorkspaceDataElement], + srcPath: AbsolutePath + ) async throws -> [AbsolutePath] { + var paths: [AbsolutePath] = [] + + for element in elements { + switch element { + case let .file(ref): + let refPath = try await ref.path(srcPath: srcPath) + if refPath.fileExtension == .xcodeproj { + paths.append(refPath) + } + case let .group(group): + // For each group, create a nested source path and recurse. + let nestedSrcPath = srcPath.appending(component: group.location.path) + let groupPaths = try await extractProjectPaths( + from: group.children, + srcPath: nestedSrcPath + ) + paths.append(contentsOf: groupPaths) + } + } + + return paths + } + + /// Maps any shared schemes defined within the `.xcworkspace`. + /// + /// Schemes are typically located under `xcshareddata/xcschemes`. If found, + /// this method parses them and maps them into domain `Scheme` models. + /// + /// - Parameter xcworkspace: The workspace whose schemes should be mapped. + /// - Returns: An array of `Scheme` instances for all shared schemes in the workspace. + private func mapSchemes(from xcworkspace: XCWorkspace) throws -> [Scheme] { + let srcPath = xcworkspace.workspacePath.parentDirectory + let sharedDataPath = Path(srcPath.pathString) + "xcshareddata/xcschemes" + + guard sharedDataPath.exists else { + return [] + } + + let schemePaths = try sharedDataPath.children().filter { $0.extension == "xcscheme" } + return try schemePaths.map { schemePath in + let xcscheme = try XCScheme(path: schemePath) + return try schemeMapper.map( + xcscheme, + shared: true, + graphType: .workspace(xcworkspace) + ) + } + } +} diff --git a/Sources/XcodeProjMapper/Utilities/ConfigurationMatcher.swift b/Sources/XcodeProjMapper/Utilities/ConfigurationMatcher.swift new file mode 100644 index 00000000..ff325dfa --- /dev/null +++ b/Sources/XcodeProjMapper/Utilities/ConfigurationMatcher.swift @@ -0,0 +1,54 @@ +import Foundation +import XcodeGraph + +/// A protocol defining methods for determining the variant of a build configuration +/// and validating configuration names. +protocol ConfigurationMatching { + /// Returns the build configuration variant for a given configuration name. + /// + /// This method checks for keywords that identify known variants and defaults to `.debug` if none match. + /// + /// - Parameter name: The name of the build configuration. + /// - Returns: The determined `BuildConfiguration.Variant` for the given name. + func variant(for name: String) -> BuildConfiguration.Variant + + /// Validates that a configuration name is non-empty and contains no whitespace. + /// + /// - Parameter name: The configuration name to validate. + /// - Returns: `true` if the name is valid; `false` otherwise. + func validateConfigurationName(_ name: String) -> Bool +} + +/// A concrete implementation of `ConfigurationMatching` that uses predefined keyword patterns +/// to determine configuration variants. +struct ConfigurationMatcher: ConfigurationMatching { + /// Represents a pattern mapping a set of keywords to a configuration variant. + struct Pattern { + let keywords: Set + let variant: BuildConfiguration.Variant + } + + /// Common patterns for identifying build configuration variants. + let patterns: [Pattern] + + /// Initializes a new `ConfigurationMatcher` with default patterns. + /// + /// - Parameter patterns: An optional array of `Pattern` to override defaults. + init(patterns: [Pattern]? = nil) { + self.patterns = patterns ?? [ + Pattern(keywords: ["debug", "development", "dev"], variant: .debug), + Pattern(keywords: ["release", "prod", "production"], variant: .release), + ] + } + + func variant(for name: String) -> BuildConfiguration.Variant { + let lowercased = name.lowercased() + return patterns.first { pattern in + pattern.keywords.contains(where: { lowercased.contains($0) }) + }?.variant ?? .debug + } + + func validateConfigurationName(_ name: String) -> Bool { + !name.isEmpty && name.rangeOfCharacter(from: .whitespaces) == nil + } +} diff --git a/Sources/XcodeProjMapper/Utilities/DeveloperDirectoryProvider.swift b/Sources/XcodeProjMapper/Utilities/DeveloperDirectoryProvider.swift new file mode 100644 index 00000000..64e22fa6 --- /dev/null +++ b/Sources/XcodeProjMapper/Utilities/DeveloperDirectoryProvider.swift @@ -0,0 +1,32 @@ +import Command +import Foundation +import Path + +/// A protocol that obtains the current developer directory (via `xcode-select -p`) asynchronously. +protocol DeveloperDirectoryProviding { + /// Returns the absolute path to the currently selected Xcode’s Developer directory. + /// - Throws: If `xcode-select -p` fails or if the output is invalid. + func developerDirectory() async throws -> AbsolutePath +} + +/// Default implementation of `DeveloperDirectoryProviding` that uses `CommandRunner`. +struct DeveloperDirectoryProvider: DeveloperDirectoryProviding { + private let commandRunner: CommandRunning + + /// Creates a new provider that uses the given `CommandRunning` instance. + /// - Parameter commandRunner: By default, uses `CommandRunner()`. + init(commandRunner: CommandRunning = CommandRunner()) { + self.commandRunner = commandRunner + } + + /// Uses `xcode-select -p` to get the path to the currently selected Xcode’s Developer folder. + /// - Throws: If `xcode-select -p` fails or if the path output is invalid. + /// - Returns: A valid `AbsolutePath` pointing to the developer directory. + func developerDirectory() async throws -> AbsolutePath { + let stream = commandRunner.run(arguments: ["xcode-select", "-p"]) + let rawPath = try await stream.concatenatedString() + .trimmingCharacters(in: .whitespacesAndNewlines) + + return try AbsolutePath(validating: rawPath) + } +} diff --git a/Sources/XcodeProjMapper/Utilities/Optional+Throwing.swift b/Sources/XcodeProjMapper/Utilities/Optional+Throwing.swift new file mode 100644 index 00000000..564ae36b --- /dev/null +++ b/Sources/XcodeProjMapper/Utilities/Optional+Throwing.swift @@ -0,0 +1,13 @@ +import Foundation + +extension Optional { + /// Unwraps the optional value or throws the provided error if `nil`. + /// + /// - Parameter error: An autoclosure that generates the error to throw if the optional is `nil`. + /// - Returns: The unwrapped value of the optional. + /// - Throws: The provided error if the optional is `nil`. + func throwing(_ error: @autoclosure () -> Error) throws -> Wrapped { + guard let value = self else { throw error() } + return value + } +} diff --git a/Sources/XcodeProjMapper/Utilities/PathDependencyMapper.swift b/Sources/XcodeProjMapper/Utilities/PathDependencyMapper.swift new file mode 100644 index 00000000..417c1f6d --- /dev/null +++ b/Sources/XcodeProjMapper/Utilities/PathDependencyMapper.swift @@ -0,0 +1,71 @@ +import Foundation +import Path +import XcodeGraph + +/// A protocol defining how to map a file path into a `TargetDependency` based on its extension. +protocol PathDependencyMapping { + /// Maps the given path to a `TargetDependency`. + /// + /// - Parameters: + /// - path: The file path to map. + /// - condition: An optional platform condition (e.g., iOS only). + /// - Returns: The corresponding `TargetDependency`, if the path extension is recognized. + /// - Throws: `PathDependencyError.invalidExtension` if the file extension is not supported. + func map(path: AbsolutePath, condition: PlatformCondition?) throws -> TargetDependency +} + +/// A mapper that converts file paths (like `.framework`, `.xcframework`, or libraries) to `TargetDependency` models. +struct PathDependencyMapper: PathDependencyMapping { + func map(path: AbsolutePath, condition: PlatformCondition?) throws -> TargetDependency { + let status: LinkingStatus = .required + + switch path.fileExtension { + case .framework: + return .framework(path: path, status: status, condition: condition) + case .xcframework: + return .xcframework(path: path, status: status, condition: condition) + case .dynamicLibrary, .textBasedDynamicLibrary, .staticLibrary: + return .library( + path: path, + publicHeaders: path.parentDirectory, // heuristics; can be overridden if needed + swiftModuleMap: nil, + condition: condition + ) + case .xcodeproj, .xcworkspace, .coreData, .playground, .none: + throw PathDependencyError.invalidExtension(path: path.pathString) + } + } +} + +/// Errors that may occur when mapping paths to `TargetDependency`. +enum PathDependencyError: Error, LocalizedError { + case invalidExtension(path: String) + + var errorDescription: String? { + switch self { + case let .invalidExtension(path): + return "Encountered an invalid or unsupported file extension: \(path)" + } + } +} + +/// Common file extensions encountered in Xcode projects and their associated artifacts. +enum FileExtension: String { + case xcodeproj + case xcworkspace + case framework + case xcframework + case staticLibrary = "a" + case dynamicLibrary = "dylib" + case textBasedDynamicLibrary = "tbd" + case coreData = "xcdatamodeld" + case playground +} + +/// A convenience extension to retrieve a file's `FileExtension` from `AbsolutePath`. +extension AbsolutePath { + var fileExtension: FileExtension? { + guard let ext = `extension`?.lowercased() else { return nil } + return FileExtension(rawValue: ext) + } +} diff --git a/Tests/Fixtures/MyFramework.xcframework/Info.plist b/Tests/Fixtures/MyFramework.xcframework/Info.plist new file mode 100644 index 00000000..51e4cc34 --- /dev/null +++ b/Tests/Fixtures/MyFramework.xcframework/Info.plist @@ -0,0 +1,39 @@ + + + + + AvailableLibraries + + + LibraryIdentifier + ios-x86_64-simulator + LibraryPath + MyFramework.framework + SupportedArchitectures + + x86_64 + + SupportedPlatform + ios + SupportedPlatformVariant + simulator + + + LibraryIdentifier + ios-arm64 + LibraryPath + MyFramework.framework + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + diff --git a/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Headers/MyFramework-Swift.h b/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Headers/MyFramework-Swift.h new file mode 100644 index 00000000..1b9cd391 --- /dev/null +++ b/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Headers/MyFramework-Swift.h @@ -0,0 +1,204 @@ +// Generated by Apple Swift version 5.1 (swiftlang-1100.0.270.13 clang-1100.0.33.7) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgcc-compat" + +#if !defined(__has_include) +# define __has_include(x) 0 +#endif +#if !defined(__has_attribute) +# define __has_attribute(x) 0 +#endif +#if !defined(__has_feature) +# define __has_feature(x) 0 +#endif +#if !defined(__has_warning) +# define __has_warning(x) 0 +#endif + +#if __has_include() +# include +#endif + +#pragma clang diagnostic ignored "-Wauto-import" +#include +#include +#include +#include + +#if !defined(SWIFT_TYPEDEFS) +# define SWIFT_TYPEDEFS 1 +# if __has_include() +# include +# elif !defined(__cplusplus) +typedef uint_least16_t char16_t; +typedef uint_least32_t char32_t; +# endif +typedef float swift_float2 __attribute__((__ext_vector_type__(2))); +typedef float swift_float3 __attribute__((__ext_vector_type__(3))); +typedef float swift_float4 __attribute__((__ext_vector_type__(4))); +typedef double swift_double2 __attribute__((__ext_vector_type__(2))); +typedef double swift_double3 __attribute__((__ext_vector_type__(3))); +typedef double swift_double4 __attribute__((__ext_vector_type__(4))); +typedef int swift_int2 __attribute__((__ext_vector_type__(2))); +typedef int swift_int3 __attribute__((__ext_vector_type__(3))); +typedef int swift_int4 __attribute__((__ext_vector_type__(4))); +typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2))); +typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3))); +typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); +#endif + +#if !defined(SWIFT_PASTE) +# define SWIFT_PASTE_HELPER(x, y) x##y +# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y) +#endif +#if !defined(SWIFT_METATYPE) +# define SWIFT_METATYPE(X) Class +#endif +#if !defined(SWIFT_CLASS_PROPERTY) +# if __has_feature(objc_class_property) +# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__ +# else +# define SWIFT_CLASS_PROPERTY(...) +# endif +#endif + +#if __has_attribute(objc_runtime_name) +# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) +#else +# define SWIFT_RUNTIME_NAME(X) +#endif +#if __has_attribute(swift_name) +# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) +#else +# define SWIFT_COMPILE_NAME(X) +#endif +#if __has_attribute(objc_method_family) +# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) +#else +# define SWIFT_METHOD_FAMILY(X) +#endif +#if __has_attribute(noescape) +# define SWIFT_NOESCAPE __attribute__((noescape)) +#else +# define SWIFT_NOESCAPE +#endif +#if __has_attribute(warn_unused_result) +# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +#else +# define SWIFT_WARN_UNUSED_RESULT +#endif +#if __has_attribute(noreturn) +# define SWIFT_NORETURN __attribute__((noreturn)) +#else +# define SWIFT_NORETURN +#endif +#if !defined(SWIFT_CLASS_EXTRA) +# define SWIFT_CLASS_EXTRA +#endif +#if !defined(SWIFT_PROTOCOL_EXTRA) +# define SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_ENUM_EXTRA) +# define SWIFT_ENUM_EXTRA +#endif +#if !defined(SWIFT_CLASS) +# if __has_attribute(objc_subclassing_restricted) +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# else +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# endif +#endif +#if !defined(SWIFT_RESILIENT_CLASS) +# if __has_attribute(objc_class_stub) +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub)) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME) +# else +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME) +# endif +#endif + +#if !defined(SWIFT_PROTOCOL) +# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +#endif + +#if !defined(SWIFT_EXTENSION) +# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__) +#endif + +#if !defined(OBJC_DESIGNATED_INITIALIZER) +# if __has_attribute(objc_designated_initializer) +# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) +# else +# define OBJC_DESIGNATED_INITIALIZER +# endif +#endif +#if !defined(SWIFT_ENUM_ATTR) +# if defined(__has_attribute) && __has_attribute(enum_extensibility) +# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility))) +# else +# define SWIFT_ENUM_ATTR(_extensibility) +# endif +#endif +#if !defined(SWIFT_ENUM) +# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# if __has_feature(generalized_swift_name) +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# else +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility) +# endif +#endif +#if !defined(SWIFT_UNAVAILABLE) +# define SWIFT_UNAVAILABLE __attribute__((unavailable)) +#endif +#if !defined(SWIFT_UNAVAILABLE_MSG) +# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg))) +#endif +#if !defined(SWIFT_AVAILABILITY) +# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__))) +#endif +#if !defined(SWIFT_WEAK_IMPORT) +# define SWIFT_WEAK_IMPORT __attribute__((weak_import)) +#endif +#if !defined(SWIFT_DEPRECATED) +# define SWIFT_DEPRECATED __attribute__((deprecated)) +#endif +#if !defined(SWIFT_DEPRECATED_MSG) +# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__))) +#endif +#if __has_feature(attribute_diagnose_if_objc) +# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) +#else +# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) +#endif +#if !defined(IBSegueAction) +# define IBSegueAction +#endif +#if __has_feature(modules) +#if __has_warning("-Watimport-in-framework-header") +#pragma clang diagnostic ignored "-Watimport-in-framework-header" +#endif +#endif + +#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" +#pragma clang diagnostic ignored "-Wduplicate-method-arg" +#if __has_warning("-Wpragma-clang-attribute") +# pragma clang diagnostic ignored "-Wpragma-clang-attribute" +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma clang diagnostic ignored "-Wnullability" + +#if __has_attribute(external_source_symbol) +# pragma push_macro("any") +# undef any +# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="MyFramework",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol)) +# pragma pop_macro("any") +#endif + +#if __has_attribute(external_source_symbol) +# pragma clang attribute pop +#endif +#pragma clang diagnostic pop diff --git a/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Info.plist b/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Info.plist new file mode 100644 index 00000000..40a2eeab Binary files /dev/null and b/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Info.plist differ diff --git a/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Modules/MyFramework.swiftmodule/arm64-apple-ios.swiftdoc b/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Modules/MyFramework.swiftmodule/arm64-apple-ios.swiftdoc new file mode 100644 index 00000000..00046fd4 Binary files /dev/null and b/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Modules/MyFramework.swiftmodule/arm64-apple-ios.swiftdoc differ diff --git a/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Modules/MyFramework.swiftmodule/arm64-apple-ios.swiftinterface b/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Modules/MyFramework.swiftmodule/arm64-apple-ios.swiftinterface new file mode 100644 index 00000000..7ea721d1 --- /dev/null +++ b/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Modules/MyFramework.swiftmodule/arm64-apple-ios.swiftinterface @@ -0,0 +1,10 @@ +// swift-interface-format-version: 1.0 +// swift-compiler-version: Apple Swift version 5.1 (swiftlang-1100.0.270.13 clang-1100.0.33.7) +// swift-module-flags: -target arm64-apple-ios13.0 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name MyFramework +import Foundation +import Swift +public class MyFramework { + public var name: Swift.String + public init() + @objc deinit +} diff --git a/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Modules/MyFramework.swiftmodule/arm64.swiftdoc b/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Modules/MyFramework.swiftmodule/arm64.swiftdoc new file mode 100644 index 00000000..00046fd4 Binary files /dev/null and b/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Modules/MyFramework.swiftmodule/arm64.swiftdoc differ diff --git a/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Modules/MyFramework.swiftmodule/arm64.swiftinterface b/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Modules/MyFramework.swiftmodule/arm64.swiftinterface new file mode 100644 index 00000000..7ea721d1 --- /dev/null +++ b/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Modules/MyFramework.swiftmodule/arm64.swiftinterface @@ -0,0 +1,10 @@ +// swift-interface-format-version: 1.0 +// swift-compiler-version: Apple Swift version 5.1 (swiftlang-1100.0.270.13 clang-1100.0.33.7) +// swift-module-flags: -target arm64-apple-ios13.0 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name MyFramework +import Foundation +import Swift +public class MyFramework { + public var name: Swift.String + public init() + @objc deinit +} diff --git a/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Modules/module.modulemap b/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Modules/module.modulemap new file mode 100644 index 00000000..4d3769ea --- /dev/null +++ b/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/Modules/module.modulemap @@ -0,0 +1,4 @@ +framework module MyFramework { + header "MyFramework-Swift.h" + requires objc +} diff --git a/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/MyFramework b/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/MyFramework new file mode 100755 index 00000000..1106020e Binary files /dev/null and b/Tests/Fixtures/MyFramework.xcframework/ios-arm64/MyFramework.framework/MyFramework differ diff --git a/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Headers/MyFramework-Swift.h b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Headers/MyFramework-Swift.h new file mode 100644 index 00000000..1b9cd391 --- /dev/null +++ b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Headers/MyFramework-Swift.h @@ -0,0 +1,204 @@ +// Generated by Apple Swift version 5.1 (swiftlang-1100.0.270.13 clang-1100.0.33.7) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgcc-compat" + +#if !defined(__has_include) +# define __has_include(x) 0 +#endif +#if !defined(__has_attribute) +# define __has_attribute(x) 0 +#endif +#if !defined(__has_feature) +# define __has_feature(x) 0 +#endif +#if !defined(__has_warning) +# define __has_warning(x) 0 +#endif + +#if __has_include() +# include +#endif + +#pragma clang diagnostic ignored "-Wauto-import" +#include +#include +#include +#include + +#if !defined(SWIFT_TYPEDEFS) +# define SWIFT_TYPEDEFS 1 +# if __has_include() +# include +# elif !defined(__cplusplus) +typedef uint_least16_t char16_t; +typedef uint_least32_t char32_t; +# endif +typedef float swift_float2 __attribute__((__ext_vector_type__(2))); +typedef float swift_float3 __attribute__((__ext_vector_type__(3))); +typedef float swift_float4 __attribute__((__ext_vector_type__(4))); +typedef double swift_double2 __attribute__((__ext_vector_type__(2))); +typedef double swift_double3 __attribute__((__ext_vector_type__(3))); +typedef double swift_double4 __attribute__((__ext_vector_type__(4))); +typedef int swift_int2 __attribute__((__ext_vector_type__(2))); +typedef int swift_int3 __attribute__((__ext_vector_type__(3))); +typedef int swift_int4 __attribute__((__ext_vector_type__(4))); +typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2))); +typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3))); +typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); +#endif + +#if !defined(SWIFT_PASTE) +# define SWIFT_PASTE_HELPER(x, y) x##y +# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y) +#endif +#if !defined(SWIFT_METATYPE) +# define SWIFT_METATYPE(X) Class +#endif +#if !defined(SWIFT_CLASS_PROPERTY) +# if __has_feature(objc_class_property) +# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__ +# else +# define SWIFT_CLASS_PROPERTY(...) +# endif +#endif + +#if __has_attribute(objc_runtime_name) +# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) +#else +# define SWIFT_RUNTIME_NAME(X) +#endif +#if __has_attribute(swift_name) +# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) +#else +# define SWIFT_COMPILE_NAME(X) +#endif +#if __has_attribute(objc_method_family) +# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) +#else +# define SWIFT_METHOD_FAMILY(X) +#endif +#if __has_attribute(noescape) +# define SWIFT_NOESCAPE __attribute__((noescape)) +#else +# define SWIFT_NOESCAPE +#endif +#if __has_attribute(warn_unused_result) +# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +#else +# define SWIFT_WARN_UNUSED_RESULT +#endif +#if __has_attribute(noreturn) +# define SWIFT_NORETURN __attribute__((noreturn)) +#else +# define SWIFT_NORETURN +#endif +#if !defined(SWIFT_CLASS_EXTRA) +# define SWIFT_CLASS_EXTRA +#endif +#if !defined(SWIFT_PROTOCOL_EXTRA) +# define SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_ENUM_EXTRA) +# define SWIFT_ENUM_EXTRA +#endif +#if !defined(SWIFT_CLASS) +# if __has_attribute(objc_subclassing_restricted) +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# else +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# endif +#endif +#if !defined(SWIFT_RESILIENT_CLASS) +# if __has_attribute(objc_class_stub) +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub)) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME) +# else +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME) +# endif +#endif + +#if !defined(SWIFT_PROTOCOL) +# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +#endif + +#if !defined(SWIFT_EXTENSION) +# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__) +#endif + +#if !defined(OBJC_DESIGNATED_INITIALIZER) +# if __has_attribute(objc_designated_initializer) +# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) +# else +# define OBJC_DESIGNATED_INITIALIZER +# endif +#endif +#if !defined(SWIFT_ENUM_ATTR) +# if defined(__has_attribute) && __has_attribute(enum_extensibility) +# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility))) +# else +# define SWIFT_ENUM_ATTR(_extensibility) +# endif +#endif +#if !defined(SWIFT_ENUM) +# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# if __has_feature(generalized_swift_name) +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# else +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility) +# endif +#endif +#if !defined(SWIFT_UNAVAILABLE) +# define SWIFT_UNAVAILABLE __attribute__((unavailable)) +#endif +#if !defined(SWIFT_UNAVAILABLE_MSG) +# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg))) +#endif +#if !defined(SWIFT_AVAILABILITY) +# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__))) +#endif +#if !defined(SWIFT_WEAK_IMPORT) +# define SWIFT_WEAK_IMPORT __attribute__((weak_import)) +#endif +#if !defined(SWIFT_DEPRECATED) +# define SWIFT_DEPRECATED __attribute__((deprecated)) +#endif +#if !defined(SWIFT_DEPRECATED_MSG) +# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__))) +#endif +#if __has_feature(attribute_diagnose_if_objc) +# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) +#else +# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) +#endif +#if !defined(IBSegueAction) +# define IBSegueAction +#endif +#if __has_feature(modules) +#if __has_warning("-Watimport-in-framework-header") +#pragma clang diagnostic ignored "-Watimport-in-framework-header" +#endif +#endif + +#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" +#pragma clang diagnostic ignored "-Wduplicate-method-arg" +#if __has_warning("-Wpragma-clang-attribute") +# pragma clang diagnostic ignored "-Wpragma-clang-attribute" +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma clang diagnostic ignored "-Wnullability" + +#if __has_attribute(external_source_symbol) +# pragma push_macro("any") +# undef any +# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="MyFramework",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol)) +# pragma pop_macro("any") +#endif + +#if __has_attribute(external_source_symbol) +# pragma clang attribute pop +#endif +#pragma clang diagnostic pop diff --git a/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Info.plist b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Info.plist new file mode 100644 index 00000000..b1987383 Binary files /dev/null and b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Info.plist differ diff --git a/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Modules/MyFramework.swiftmodule/x86_64-apple-ios-simulator.swiftdoc b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Modules/MyFramework.swiftmodule/x86_64-apple-ios-simulator.swiftdoc new file mode 100644 index 00000000..49cbc2c0 Binary files /dev/null and b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Modules/MyFramework.swiftmodule/x86_64-apple-ios-simulator.swiftdoc differ diff --git a/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Modules/MyFramework.swiftmodule/x86_64-apple-ios-simulator.swiftinterface b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Modules/MyFramework.swiftmodule/x86_64-apple-ios-simulator.swiftinterface new file mode 100644 index 00000000..89965e2b --- /dev/null +++ b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Modules/MyFramework.swiftmodule/x86_64-apple-ios-simulator.swiftinterface @@ -0,0 +1,10 @@ +// swift-interface-format-version: 1.0 +// swift-compiler-version: Apple Swift version 5.1 (swiftlang-1100.0.270.13 clang-1100.0.33.7) +// swift-module-flags: -target x86_64-apple-ios13.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name MyFramework +import Foundation +import Swift +public class MyFramework { + public var name: Swift.String + public init() + @objc deinit +} diff --git a/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Modules/MyFramework.swiftmodule/x86_64.swiftdoc b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Modules/MyFramework.swiftmodule/x86_64.swiftdoc new file mode 100644 index 00000000..49cbc2c0 Binary files /dev/null and b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Modules/MyFramework.swiftmodule/x86_64.swiftdoc differ diff --git a/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Modules/MyFramework.swiftmodule/x86_64.swiftinterface b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Modules/MyFramework.swiftmodule/x86_64.swiftinterface new file mode 100644 index 00000000..89965e2b --- /dev/null +++ b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Modules/MyFramework.swiftmodule/x86_64.swiftinterface @@ -0,0 +1,10 @@ +// swift-interface-format-version: 1.0 +// swift-compiler-version: Apple Swift version 5.1 (swiftlang-1100.0.270.13 clang-1100.0.33.7) +// swift-module-flags: -target x86_64-apple-ios13.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name MyFramework +import Foundation +import Swift +public class MyFramework { + public var name: Swift.String + public init() + @objc deinit +} diff --git a/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Modules/module.modulemap b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Modules/module.modulemap new file mode 100644 index 00000000..4d3769ea --- /dev/null +++ b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/Modules/module.modulemap @@ -0,0 +1,4 @@ +framework module MyFramework { + header "MyFramework-Swift.h" + requires objc +} diff --git a/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/MyFramework b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/MyFramework new file mode 100755 index 00000000..29efd934 Binary files /dev/null and b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/MyFramework differ diff --git a/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/_CodeSignature/CodeResources b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/_CodeSignature/CodeResources new file mode 100644 index 00000000..450c6351 --- /dev/null +++ b/Tests/Fixtures/MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/_CodeSignature/CodeResources @@ -0,0 +1,190 @@ + + + + + files + + Headers/MyFramework-Swift.h + + EOml7gTGbnkeX3Xpxg7tlYPRKtU= + + Info.plist + + F+EfNZCopyyBoAd9dBDmL0zDb4s= + + Modules/MyFramework.swiftmodule/x86_64-apple-ios-simulator.swiftdoc + + 7bg9TJQa4x30QgKxioKlaO1K3NM= + + Modules/MyFramework.swiftmodule/x86_64-apple-ios-simulator.swiftinterface + + IpEcA6BZsvJ6HhGVH4IfKVk3Efc= + + Modules/MyFramework.swiftmodule/x86_64-apple-ios-simulator.swiftmodule + + xX8pm1gJtDr/i8kuOa8qHfr4+XU= + + Modules/MyFramework.swiftmodule/x86_64.swiftdoc + + 7bg9TJQa4x30QgKxioKlaO1K3NM= + + Modules/MyFramework.swiftmodule/x86_64.swiftinterface + + IpEcA6BZsvJ6HhGVH4IfKVk3Efc= + + Modules/MyFramework.swiftmodule/x86_64.swiftmodule + + xX8pm1gJtDr/i8kuOa8qHfr4+XU= + + Modules/module.modulemap + + QNWjsEW54sEWY7N9pv5xhf1yaNA= + + + files2 + + Headers/MyFramework-Swift.h + + hash2 + + 2diZoQecvqo1AfgzNjWxZhINTQrVumTXsf8L3xYG8SY= + + + Modules/MyFramework.swiftmodule/x86_64-apple-ios-simulator.swiftdoc + + hash2 + + UNdAHDwb9sXOwAf4sScUFrgtpLKuRR1AkmGUmuSQLmA= + + + Modules/MyFramework.swiftmodule/x86_64-apple-ios-simulator.swiftinterface + + hash2 + + VHwpf/TdwuHZvE7vkuo7VCgvDnYAmf1/y3nOP7tHkrM= + + + Modules/MyFramework.swiftmodule/x86_64-apple-ios-simulator.swiftmodule + + hash2 + + Elp5fP58+yjQtEigQ0mx4tCrNq8ylzTJnzvR6NyStbA= + + + Modules/MyFramework.swiftmodule/x86_64.swiftdoc + + hash2 + + UNdAHDwb9sXOwAf4sScUFrgtpLKuRR1AkmGUmuSQLmA= + + + Modules/MyFramework.swiftmodule/x86_64.swiftinterface + + hash2 + + VHwpf/TdwuHZvE7vkuo7VCgvDnYAmf1/y3nOP7tHkrM= + + + Modules/MyFramework.swiftmodule/x86_64.swiftmodule + + hash2 + + Elp5fP58+yjQtEigQ0mx4tCrNq8ylzTJnzvR6NyStbA= + + + Modules/module.modulemap + + hash2 + + Ze/OfIIWDLWEMkV5nVXLs+Mzzp3k2eZtpQBPldwh0k4= + + + + rules + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/Tests/Fixtures/MyFrameworkMissingArch.xcframework/Info.plist b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/Info.plist new file mode 100644 index 00000000..3d5676bd --- /dev/null +++ b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/Info.plist @@ -0,0 +1,39 @@ + + + + + AvailableLibraries + + + LibraryIdentifier + ios-x86_64-simulator + LibraryPath + MyFrameworkMissingArch.framework + SupportedArchitectures + + x86_64 + + SupportedPlatform + ios + SupportedPlatformVariant + simulator + + + LibraryIdentifier + ios-arm64 + LibraryPath + MyFrameworkMissingArch.framework + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + diff --git a/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Headers/MyFramework-Swift.h b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Headers/MyFramework-Swift.h new file mode 100644 index 00000000..1b9cd391 --- /dev/null +++ b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Headers/MyFramework-Swift.h @@ -0,0 +1,204 @@ +// Generated by Apple Swift version 5.1 (swiftlang-1100.0.270.13 clang-1100.0.33.7) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgcc-compat" + +#if !defined(__has_include) +# define __has_include(x) 0 +#endif +#if !defined(__has_attribute) +# define __has_attribute(x) 0 +#endif +#if !defined(__has_feature) +# define __has_feature(x) 0 +#endif +#if !defined(__has_warning) +# define __has_warning(x) 0 +#endif + +#if __has_include() +# include +#endif + +#pragma clang diagnostic ignored "-Wauto-import" +#include +#include +#include +#include + +#if !defined(SWIFT_TYPEDEFS) +# define SWIFT_TYPEDEFS 1 +# if __has_include() +# include +# elif !defined(__cplusplus) +typedef uint_least16_t char16_t; +typedef uint_least32_t char32_t; +# endif +typedef float swift_float2 __attribute__((__ext_vector_type__(2))); +typedef float swift_float3 __attribute__((__ext_vector_type__(3))); +typedef float swift_float4 __attribute__((__ext_vector_type__(4))); +typedef double swift_double2 __attribute__((__ext_vector_type__(2))); +typedef double swift_double3 __attribute__((__ext_vector_type__(3))); +typedef double swift_double4 __attribute__((__ext_vector_type__(4))); +typedef int swift_int2 __attribute__((__ext_vector_type__(2))); +typedef int swift_int3 __attribute__((__ext_vector_type__(3))); +typedef int swift_int4 __attribute__((__ext_vector_type__(4))); +typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2))); +typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3))); +typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); +#endif + +#if !defined(SWIFT_PASTE) +# define SWIFT_PASTE_HELPER(x, y) x##y +# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y) +#endif +#if !defined(SWIFT_METATYPE) +# define SWIFT_METATYPE(X) Class +#endif +#if !defined(SWIFT_CLASS_PROPERTY) +# if __has_feature(objc_class_property) +# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__ +# else +# define SWIFT_CLASS_PROPERTY(...) +# endif +#endif + +#if __has_attribute(objc_runtime_name) +# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) +#else +# define SWIFT_RUNTIME_NAME(X) +#endif +#if __has_attribute(swift_name) +# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) +#else +# define SWIFT_COMPILE_NAME(X) +#endif +#if __has_attribute(objc_method_family) +# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) +#else +# define SWIFT_METHOD_FAMILY(X) +#endif +#if __has_attribute(noescape) +# define SWIFT_NOESCAPE __attribute__((noescape)) +#else +# define SWIFT_NOESCAPE +#endif +#if __has_attribute(warn_unused_result) +# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +#else +# define SWIFT_WARN_UNUSED_RESULT +#endif +#if __has_attribute(noreturn) +# define SWIFT_NORETURN __attribute__((noreturn)) +#else +# define SWIFT_NORETURN +#endif +#if !defined(SWIFT_CLASS_EXTRA) +# define SWIFT_CLASS_EXTRA +#endif +#if !defined(SWIFT_PROTOCOL_EXTRA) +# define SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_ENUM_EXTRA) +# define SWIFT_ENUM_EXTRA +#endif +#if !defined(SWIFT_CLASS) +# if __has_attribute(objc_subclassing_restricted) +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# else +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# endif +#endif +#if !defined(SWIFT_RESILIENT_CLASS) +# if __has_attribute(objc_class_stub) +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub)) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME) +# else +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME) +# endif +#endif + +#if !defined(SWIFT_PROTOCOL) +# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +#endif + +#if !defined(SWIFT_EXTENSION) +# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__) +#endif + +#if !defined(OBJC_DESIGNATED_INITIALIZER) +# if __has_attribute(objc_designated_initializer) +# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) +# else +# define OBJC_DESIGNATED_INITIALIZER +# endif +#endif +#if !defined(SWIFT_ENUM_ATTR) +# if defined(__has_attribute) && __has_attribute(enum_extensibility) +# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility))) +# else +# define SWIFT_ENUM_ATTR(_extensibility) +# endif +#endif +#if !defined(SWIFT_ENUM) +# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# if __has_feature(generalized_swift_name) +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# else +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility) +# endif +#endif +#if !defined(SWIFT_UNAVAILABLE) +# define SWIFT_UNAVAILABLE __attribute__((unavailable)) +#endif +#if !defined(SWIFT_UNAVAILABLE_MSG) +# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg))) +#endif +#if !defined(SWIFT_AVAILABILITY) +# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__))) +#endif +#if !defined(SWIFT_WEAK_IMPORT) +# define SWIFT_WEAK_IMPORT __attribute__((weak_import)) +#endif +#if !defined(SWIFT_DEPRECATED) +# define SWIFT_DEPRECATED __attribute__((deprecated)) +#endif +#if !defined(SWIFT_DEPRECATED_MSG) +# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__))) +#endif +#if __has_feature(attribute_diagnose_if_objc) +# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) +#else +# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) +#endif +#if !defined(IBSegueAction) +# define IBSegueAction +#endif +#if __has_feature(modules) +#if __has_warning("-Watimport-in-framework-header") +#pragma clang diagnostic ignored "-Watimport-in-framework-header" +#endif +#endif + +#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" +#pragma clang diagnostic ignored "-Wduplicate-method-arg" +#if __has_warning("-Wpragma-clang-attribute") +# pragma clang diagnostic ignored "-Wpragma-clang-attribute" +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma clang diagnostic ignored "-Wnullability" + +#if __has_attribute(external_source_symbol) +# pragma push_macro("any") +# undef any +# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="MyFramework",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol)) +# pragma pop_macro("any") +#endif + +#if __has_attribute(external_source_symbol) +# pragma clang attribute pop +#endif +#pragma clang diagnostic pop diff --git a/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Info.plist b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Info.plist new file mode 100644 index 00000000..40a2eeab Binary files /dev/null and b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Info.plist differ diff --git a/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Modules/MyFramework.swiftmodule/arm64-apple-ios.swiftdoc b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Modules/MyFramework.swiftmodule/arm64-apple-ios.swiftdoc new file mode 100644 index 00000000..00046fd4 Binary files /dev/null and b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Modules/MyFramework.swiftmodule/arm64-apple-ios.swiftdoc differ diff --git a/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Modules/MyFramework.swiftmodule/arm64-apple-ios.swiftinterface b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Modules/MyFramework.swiftmodule/arm64-apple-ios.swiftinterface new file mode 100644 index 00000000..7ea721d1 --- /dev/null +++ b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Modules/MyFramework.swiftmodule/arm64-apple-ios.swiftinterface @@ -0,0 +1,10 @@ +// swift-interface-format-version: 1.0 +// swift-compiler-version: Apple Swift version 5.1 (swiftlang-1100.0.270.13 clang-1100.0.33.7) +// swift-module-flags: -target arm64-apple-ios13.0 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name MyFramework +import Foundation +import Swift +public class MyFramework { + public var name: Swift.String + public init() + @objc deinit +} diff --git a/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Modules/MyFramework.swiftmodule/arm64.swiftdoc b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Modules/MyFramework.swiftmodule/arm64.swiftdoc new file mode 100644 index 00000000..00046fd4 Binary files /dev/null and b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Modules/MyFramework.swiftmodule/arm64.swiftdoc differ diff --git a/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Modules/MyFramework.swiftmodule/arm64.swiftinterface b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Modules/MyFramework.swiftmodule/arm64.swiftinterface new file mode 100644 index 00000000..7ea721d1 --- /dev/null +++ b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Modules/MyFramework.swiftmodule/arm64.swiftinterface @@ -0,0 +1,10 @@ +// swift-interface-format-version: 1.0 +// swift-compiler-version: Apple Swift version 5.1 (swiftlang-1100.0.270.13 clang-1100.0.33.7) +// swift-module-flags: -target arm64-apple-ios13.0 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name MyFramework +import Foundation +import Swift +public class MyFramework { + public var name: Swift.String + public init() + @objc deinit +} diff --git a/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Modules/module.modulemap b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Modules/module.modulemap new file mode 100644 index 00000000..4d3769ea --- /dev/null +++ b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/Modules/module.modulemap @@ -0,0 +1,4 @@ +framework module MyFramework { + header "MyFramework-Swift.h" + requires objc +} diff --git a/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/MyFrameworkMissingArch b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/MyFrameworkMissingArch new file mode 100755 index 00000000..1106020e Binary files /dev/null and b/Tests/Fixtures/MyFrameworkMissingArch.xcframework/ios-arm64/MyFrameworkMissingArch.framework/MyFrameworkMissingArch differ diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/Info.plist b/Tests/Fixtures/MyMergeableFramework.xcframework/Info.plist new file mode 100644 index 00000000..de5e7cb6 --- /dev/null +++ b/Tests/Fixtures/MyMergeableFramework.xcframework/Info.plist @@ -0,0 +1,47 @@ + + + + + AvailableLibraries + + + BinaryPath + MyMergeableFramework.framework/MyMergeableFramework + LibraryIdentifier + ios-x86_64-simulator + LibraryPath + MyMergeableFramework.framework + MergeableMetadata + + SupportedArchitectures + + x86_64 + + SupportedPlatform + ios + SupportedPlatformVariant + simulator + + + BinaryPath + MyMergeableFramework.framework/MyMergeableFramework + LibraryIdentifier + ios-arm64 + LibraryPath + MyMergeableFramework.framework + MergeableMetadata + + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.1 + + diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Headers/MyMergeableFramework-Swift.h b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Headers/MyMergeableFramework-Swift.h new file mode 100644 index 00000000..322761ad --- /dev/null +++ b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Headers/MyMergeableFramework-Swift.h @@ -0,0 +1,311 @@ +#if 0 +#elif defined(__arm64__) && __arm64__ +// Generated by Apple Swift version 5.9 (swiftlang-5.9.0.128.2 clang-1500.0.40.1) +#ifndef MYMERGEABLEFRAMEWORK_SWIFT_H +#define MYMERGEABLEFRAMEWORK_SWIFT_H +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgcc-compat" + +#if !defined(__has_include) +# define __has_include(x) 0 +#endif +#if !defined(__has_attribute) +# define __has_attribute(x) 0 +#endif +#if !defined(__has_feature) +# define __has_feature(x) 0 +#endif +#if !defined(__has_warning) +# define __has_warning(x) 0 +#endif + +#if __has_include() +# include +#endif + +#pragma clang diagnostic ignored "-Wauto-import" +#if defined(__OBJC__) +#include +#endif +#if defined(__cplusplus) +#include +#include +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#endif +#if defined(__cplusplus) +#if defined(__arm64e__) && __has_include() +# include +#else +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-macro-identifier" +# ifndef __ptrauth_swift_value_witness_function_pointer +# define __ptrauth_swift_value_witness_function_pointer(x) +# endif +# ifndef __ptrauth_swift_class_method_pointer +# define __ptrauth_swift_class_method_pointer(x) +# endif +#pragma clang diagnostic pop +#endif +#endif + +#if !defined(SWIFT_TYPEDEFS) +# define SWIFT_TYPEDEFS 1 +# if __has_include() +# include +# elif !defined(__cplusplus) +typedef uint_least16_t char16_t; +typedef uint_least32_t char32_t; +# endif +typedef float swift_float2 __attribute__((__ext_vector_type__(2))); +typedef float swift_float3 __attribute__((__ext_vector_type__(3))); +typedef float swift_float4 __attribute__((__ext_vector_type__(4))); +typedef double swift_double2 __attribute__((__ext_vector_type__(2))); +typedef double swift_double3 __attribute__((__ext_vector_type__(3))); +typedef double swift_double4 __attribute__((__ext_vector_type__(4))); +typedef int swift_int2 __attribute__((__ext_vector_type__(2))); +typedef int swift_int3 __attribute__((__ext_vector_type__(3))); +typedef int swift_int4 __attribute__((__ext_vector_type__(4))); +typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2))); +typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3))); +typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); +#endif + +#if !defined(SWIFT_PASTE) +# define SWIFT_PASTE_HELPER(x, y) x##y +# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y) +#endif +#if !defined(SWIFT_METATYPE) +# define SWIFT_METATYPE(X) Class +#endif +#if !defined(SWIFT_CLASS_PROPERTY) +# if __has_feature(objc_class_property) +# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__ +# else +# define SWIFT_CLASS_PROPERTY(...) +# endif +#endif +#if !defined(SWIFT_RUNTIME_NAME) +# if __has_attribute(objc_runtime_name) +# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) +# else +# define SWIFT_RUNTIME_NAME(X) +# endif +#endif +#if !defined(SWIFT_COMPILE_NAME) +# if __has_attribute(swift_name) +# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) +# else +# define SWIFT_COMPILE_NAME(X) +# endif +#endif +#if !defined(SWIFT_METHOD_FAMILY) +# if __has_attribute(objc_method_family) +# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) +# else +# define SWIFT_METHOD_FAMILY(X) +# endif +#endif +#if !defined(SWIFT_NOESCAPE) +# if __has_attribute(noescape) +# define SWIFT_NOESCAPE __attribute__((noescape)) +# else +# define SWIFT_NOESCAPE +# endif +#endif +#if !defined(SWIFT_RELEASES_ARGUMENT) +# if __has_attribute(ns_consumed) +# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed)) +# else +# define SWIFT_RELEASES_ARGUMENT +# endif +#endif +#if !defined(SWIFT_WARN_UNUSED_RESULT) +# if __has_attribute(warn_unused_result) +# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +# else +# define SWIFT_WARN_UNUSED_RESULT +# endif +#endif +#if !defined(SWIFT_NORETURN) +# if __has_attribute(noreturn) +# define SWIFT_NORETURN __attribute__((noreturn)) +# else +# define SWIFT_NORETURN +# endif +#endif +#if !defined(SWIFT_CLASS_EXTRA) +# define SWIFT_CLASS_EXTRA +#endif +#if !defined(SWIFT_PROTOCOL_EXTRA) +# define SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_ENUM_EXTRA) +# define SWIFT_ENUM_EXTRA +#endif +#if !defined(SWIFT_CLASS) +# if __has_attribute(objc_subclassing_restricted) +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# else +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# endif +#endif +#if !defined(SWIFT_RESILIENT_CLASS) +# if __has_attribute(objc_class_stub) +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub)) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME) +# else +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME) +# endif +#endif +#if !defined(SWIFT_PROTOCOL) +# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_EXTENSION) +# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__) +#endif +#if !defined(OBJC_DESIGNATED_INITIALIZER) +# if __has_attribute(objc_designated_initializer) +# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) +# else +# define OBJC_DESIGNATED_INITIALIZER +# endif +#endif +#if !defined(SWIFT_ENUM_ATTR) +# if __has_attribute(enum_extensibility) +# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility))) +# else +# define SWIFT_ENUM_ATTR(_extensibility) +# endif +#endif +#if !defined(SWIFT_ENUM) +# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# if __has_feature(generalized_swift_name) +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# else +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility) +# endif +#endif +#if !defined(SWIFT_UNAVAILABLE) +# define SWIFT_UNAVAILABLE __attribute__((unavailable)) +#endif +#if !defined(SWIFT_UNAVAILABLE_MSG) +# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg))) +#endif +#if !defined(SWIFT_AVAILABILITY) +# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__))) +#endif +#if !defined(SWIFT_WEAK_IMPORT) +# define SWIFT_WEAK_IMPORT __attribute__((weak_import)) +#endif +#if !defined(SWIFT_DEPRECATED) +# define SWIFT_DEPRECATED __attribute__((deprecated)) +#endif +#if !defined(SWIFT_DEPRECATED_MSG) +# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__))) +#endif +#if !defined(SWIFT_DEPRECATED_OBJC) +# if __has_feature(attribute_diagnose_if_objc) +# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) +# else +# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) +# endif +#endif +#if defined(__OBJC__) +#if !defined(IBSegueAction) +# define IBSegueAction +#endif +#endif +#if !defined(SWIFT_EXTERN) +# if defined(__cplusplus) +# define SWIFT_EXTERN extern "C" +# else +# define SWIFT_EXTERN extern +# endif +#endif +#if !defined(SWIFT_CALL) +# define SWIFT_CALL __attribute__((swiftcall)) +#endif +#if !defined(SWIFT_INDIRECT_RESULT) +# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result)) +#endif +#if !defined(SWIFT_CONTEXT) +# define SWIFT_CONTEXT __attribute__((swift_context)) +#endif +#if !defined(SWIFT_ERROR_RESULT) +# define SWIFT_ERROR_RESULT __attribute__((swift_error_result)) +#endif +#if defined(__cplusplus) +# define SWIFT_NOEXCEPT noexcept +#else +# define SWIFT_NOEXCEPT +#endif +#if !defined(SWIFT_C_INLINE_THUNK) +# if __has_attribute(always_inline) +# if __has_attribute(nodebug) +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug)) +# else +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) +# endif +# else +# define SWIFT_C_INLINE_THUNK inline +# endif +#endif +#if defined(_WIN32) +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport) +#endif +#else +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL +#endif +#endif +#if defined(__OBJC__) +#if __has_feature(objc_modules) +#if __has_warning("-Watimport-in-framework-header") +#pragma clang diagnostic ignored "-Watimport-in-framework-header" +#endif +#endif + +#endif +#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" +#pragma clang diagnostic ignored "-Wduplicate-method-arg" +#if __has_warning("-Wpragma-clang-attribute") +# pragma clang diagnostic ignored "-Wpragma-clang-attribute" +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma clang diagnostic ignored "-Wnullability" +#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" + +#if __has_attribute(external_source_symbol) +# pragma push_macro("any") +# undef any +# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="MyMergeableFramework",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol)) +# pragma pop_macro("any") +#endif + +#if defined(__OBJC__) +#endif +#if __has_attribute(external_source_symbol) +# pragma clang attribute pop +#endif +#if defined(__cplusplus) +#endif +#pragma clang diagnostic pop +#endif + +#else +#error unsupported Swift architecture +#endif diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Info.plist b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Info.plist new file mode 100644 index 00000000..d6c28783 Binary files /dev/null and b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Info.plist differ diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios.abi.json b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios.abi.json new file mode 100644 index 00000000..dc109fc0 --- /dev/null +++ b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios.abi.json @@ -0,0 +1,9 @@ +{ + "ABIRoot": { + "kind": "Root", + "name": "TopLevel", + "printedName": "TopLevel", + "json_format_version": 8 + }, + "ConstValues": [] +} \ No newline at end of file diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios.private.swiftinterface b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios.private.swiftinterface new file mode 100644 index 00000000..50bdc703 --- /dev/null +++ b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios.private.swiftinterface @@ -0,0 +1,8 @@ +// swift-interface-format-version: 1.0 +// swift-compiler-version: Apple Swift version 5.9 (swiftlang-5.9.0.128.2 clang-1500.0.40.1) +// swift-module-flags: -target arm64-apple-ios14.0 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Osize -module-name MyMergeableFramework +// swift-module-flags-ignorable: -enable-bare-slash-regex +import Swift +import _Concurrency +import _StringProcessing +import _SwiftConcurrencyShims diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios.swiftdoc b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios.swiftdoc new file mode 100644 index 00000000..505ecb4f Binary files /dev/null and b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios.swiftdoc differ diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios.swiftinterface b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios.swiftinterface new file mode 100644 index 00000000..50bdc703 --- /dev/null +++ b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios.swiftinterface @@ -0,0 +1,8 @@ +// swift-interface-format-version: 1.0 +// swift-compiler-version: Apple Swift version 5.9 (swiftlang-5.9.0.128.2 clang-1500.0.40.1) +// swift-module-flags: -target arm64-apple-ios14.0 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Osize -module-name MyMergeableFramework +// swift-module-flags-ignorable: -enable-bare-slash-regex +import Swift +import _Concurrency +import _StringProcessing +import _SwiftConcurrencyShims diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios.swiftmodule b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios.swiftmodule new file mode 100644 index 00000000..92f4d52b Binary files /dev/null and b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios.swiftmodule differ diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/module.modulemap b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/module.modulemap new file mode 100644 index 00000000..99e6c409 --- /dev/null +++ b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/Modules/module.modulemap @@ -0,0 +1,4 @@ +framework module MyMergeableFramework { + header "MyMergeableFramework-Swift.h" + requires objc +} diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/MyMergeableFramework b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/MyMergeableFramework new file mode 100755 index 00000000..4ccbd468 Binary files /dev/null and b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-arm64/MyMergeableFramework.framework/MyMergeableFramework differ diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Headers/MyMergeableFramework-Swift.h b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Headers/MyMergeableFramework-Swift.h new file mode 100644 index 00000000..7af841e7 --- /dev/null +++ b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Headers/MyMergeableFramework-Swift.h @@ -0,0 +1,618 @@ +#if 0 +#elif defined(__arm64__) && __arm64__ +// Generated by Apple Swift version 5.9 (swiftlang-5.9.0.128.2 clang-1500.0.40.1) +#ifndef MYMERGEABLEFRAMEWORK_SWIFT_H +#define MYMERGEABLEFRAMEWORK_SWIFT_H +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgcc-compat" + +#if !defined(__has_include) +# define __has_include(x) 0 +#endif +#if !defined(__has_attribute) +# define __has_attribute(x) 0 +#endif +#if !defined(__has_feature) +# define __has_feature(x) 0 +#endif +#if !defined(__has_warning) +# define __has_warning(x) 0 +#endif + +#if __has_include() +# include +#endif + +#pragma clang diagnostic ignored "-Wauto-import" +#if defined(__OBJC__) +#include +#endif +#if defined(__cplusplus) +#include +#include +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#endif +#if defined(__cplusplus) +#if defined(__arm64e__) && __has_include() +# include +#else +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-macro-identifier" +# ifndef __ptrauth_swift_value_witness_function_pointer +# define __ptrauth_swift_value_witness_function_pointer(x) +# endif +# ifndef __ptrauth_swift_class_method_pointer +# define __ptrauth_swift_class_method_pointer(x) +# endif +#pragma clang diagnostic pop +#endif +#endif + +#if !defined(SWIFT_TYPEDEFS) +# define SWIFT_TYPEDEFS 1 +# if __has_include() +# include +# elif !defined(__cplusplus) +typedef uint_least16_t char16_t; +typedef uint_least32_t char32_t; +# endif +typedef float swift_float2 __attribute__((__ext_vector_type__(2))); +typedef float swift_float3 __attribute__((__ext_vector_type__(3))); +typedef float swift_float4 __attribute__((__ext_vector_type__(4))); +typedef double swift_double2 __attribute__((__ext_vector_type__(2))); +typedef double swift_double3 __attribute__((__ext_vector_type__(3))); +typedef double swift_double4 __attribute__((__ext_vector_type__(4))); +typedef int swift_int2 __attribute__((__ext_vector_type__(2))); +typedef int swift_int3 __attribute__((__ext_vector_type__(3))); +typedef int swift_int4 __attribute__((__ext_vector_type__(4))); +typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2))); +typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3))); +typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); +#endif + +#if !defined(SWIFT_PASTE) +# define SWIFT_PASTE_HELPER(x, y) x##y +# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y) +#endif +#if !defined(SWIFT_METATYPE) +# define SWIFT_METATYPE(X) Class +#endif +#if !defined(SWIFT_CLASS_PROPERTY) +# if __has_feature(objc_class_property) +# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__ +# else +# define SWIFT_CLASS_PROPERTY(...) +# endif +#endif +#if !defined(SWIFT_RUNTIME_NAME) +# if __has_attribute(objc_runtime_name) +# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) +# else +# define SWIFT_RUNTIME_NAME(X) +# endif +#endif +#if !defined(SWIFT_COMPILE_NAME) +# if __has_attribute(swift_name) +# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) +# else +# define SWIFT_COMPILE_NAME(X) +# endif +#endif +#if !defined(SWIFT_METHOD_FAMILY) +# if __has_attribute(objc_method_family) +# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) +# else +# define SWIFT_METHOD_FAMILY(X) +# endif +#endif +#if !defined(SWIFT_NOESCAPE) +# if __has_attribute(noescape) +# define SWIFT_NOESCAPE __attribute__((noescape)) +# else +# define SWIFT_NOESCAPE +# endif +#endif +#if !defined(SWIFT_RELEASES_ARGUMENT) +# if __has_attribute(ns_consumed) +# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed)) +# else +# define SWIFT_RELEASES_ARGUMENT +# endif +#endif +#if !defined(SWIFT_WARN_UNUSED_RESULT) +# if __has_attribute(warn_unused_result) +# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +# else +# define SWIFT_WARN_UNUSED_RESULT +# endif +#endif +#if !defined(SWIFT_NORETURN) +# if __has_attribute(noreturn) +# define SWIFT_NORETURN __attribute__((noreturn)) +# else +# define SWIFT_NORETURN +# endif +#endif +#if !defined(SWIFT_CLASS_EXTRA) +# define SWIFT_CLASS_EXTRA +#endif +#if !defined(SWIFT_PROTOCOL_EXTRA) +# define SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_ENUM_EXTRA) +# define SWIFT_ENUM_EXTRA +#endif +#if !defined(SWIFT_CLASS) +# if __has_attribute(objc_subclassing_restricted) +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# else +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# endif +#endif +#if !defined(SWIFT_RESILIENT_CLASS) +# if __has_attribute(objc_class_stub) +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub)) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME) +# else +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME) +# endif +#endif +#if !defined(SWIFT_PROTOCOL) +# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_EXTENSION) +# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__) +#endif +#if !defined(OBJC_DESIGNATED_INITIALIZER) +# if __has_attribute(objc_designated_initializer) +# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) +# else +# define OBJC_DESIGNATED_INITIALIZER +# endif +#endif +#if !defined(SWIFT_ENUM_ATTR) +# if __has_attribute(enum_extensibility) +# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility))) +# else +# define SWIFT_ENUM_ATTR(_extensibility) +# endif +#endif +#if !defined(SWIFT_ENUM) +# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# if __has_feature(generalized_swift_name) +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# else +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility) +# endif +#endif +#if !defined(SWIFT_UNAVAILABLE) +# define SWIFT_UNAVAILABLE __attribute__((unavailable)) +#endif +#if !defined(SWIFT_UNAVAILABLE_MSG) +# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg))) +#endif +#if !defined(SWIFT_AVAILABILITY) +# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__))) +#endif +#if !defined(SWIFT_WEAK_IMPORT) +# define SWIFT_WEAK_IMPORT __attribute__((weak_import)) +#endif +#if !defined(SWIFT_DEPRECATED) +# define SWIFT_DEPRECATED __attribute__((deprecated)) +#endif +#if !defined(SWIFT_DEPRECATED_MSG) +# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__))) +#endif +#if !defined(SWIFT_DEPRECATED_OBJC) +# if __has_feature(attribute_diagnose_if_objc) +# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) +# else +# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) +# endif +#endif +#if defined(__OBJC__) +#if !defined(IBSegueAction) +# define IBSegueAction +#endif +#endif +#if !defined(SWIFT_EXTERN) +# if defined(__cplusplus) +# define SWIFT_EXTERN extern "C" +# else +# define SWIFT_EXTERN extern +# endif +#endif +#if !defined(SWIFT_CALL) +# define SWIFT_CALL __attribute__((swiftcall)) +#endif +#if !defined(SWIFT_INDIRECT_RESULT) +# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result)) +#endif +#if !defined(SWIFT_CONTEXT) +# define SWIFT_CONTEXT __attribute__((swift_context)) +#endif +#if !defined(SWIFT_ERROR_RESULT) +# define SWIFT_ERROR_RESULT __attribute__((swift_error_result)) +#endif +#if defined(__cplusplus) +# define SWIFT_NOEXCEPT noexcept +#else +# define SWIFT_NOEXCEPT +#endif +#if !defined(SWIFT_C_INLINE_THUNK) +# if __has_attribute(always_inline) +# if __has_attribute(nodebug) +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug)) +# else +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) +# endif +# else +# define SWIFT_C_INLINE_THUNK inline +# endif +#endif +#if defined(_WIN32) +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport) +#endif +#else +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL +#endif +#endif +#if defined(__OBJC__) +#if __has_feature(objc_modules) +#if __has_warning("-Watimport-in-framework-header") +#pragma clang diagnostic ignored "-Watimport-in-framework-header" +#endif +#endif + +#endif +#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" +#pragma clang diagnostic ignored "-Wduplicate-method-arg" +#if __has_warning("-Wpragma-clang-attribute") +# pragma clang diagnostic ignored "-Wpragma-clang-attribute" +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma clang diagnostic ignored "-Wnullability" +#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" + +#if __has_attribute(external_source_symbol) +# pragma push_macro("any") +# undef any +# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="MyMergeableFramework",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol)) +# pragma pop_macro("any") +#endif + +#if defined(__OBJC__) +#endif +#if __has_attribute(external_source_symbol) +# pragma clang attribute pop +#endif +#if defined(__cplusplus) +#endif +#pragma clang diagnostic pop +#endif + +#elif defined(__x86_64__) && __x86_64__ +// Generated by Apple Swift version 5.9 (swiftlang-5.9.0.128.2 clang-1500.0.40.1) +#ifndef MYMERGEABLEFRAMEWORK_SWIFT_H +#define MYMERGEABLEFRAMEWORK_SWIFT_H +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgcc-compat" + +#if !defined(__has_include) +# define __has_include(x) 0 +#endif +#if !defined(__has_attribute) +# define __has_attribute(x) 0 +#endif +#if !defined(__has_feature) +# define __has_feature(x) 0 +#endif +#if !defined(__has_warning) +# define __has_warning(x) 0 +#endif + +#if __has_include() +# include +#endif + +#pragma clang diagnostic ignored "-Wauto-import" +#if defined(__OBJC__) +#include +#endif +#if defined(__cplusplus) +#include +#include +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#endif +#if defined(__cplusplus) +#if defined(__arm64e__) && __has_include() +# include +#else +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-macro-identifier" +# ifndef __ptrauth_swift_value_witness_function_pointer +# define __ptrauth_swift_value_witness_function_pointer(x) +# endif +# ifndef __ptrauth_swift_class_method_pointer +# define __ptrauth_swift_class_method_pointer(x) +# endif +#pragma clang diagnostic pop +#endif +#endif + +#if !defined(SWIFT_TYPEDEFS) +# define SWIFT_TYPEDEFS 1 +# if __has_include() +# include +# elif !defined(__cplusplus) +typedef uint_least16_t char16_t; +typedef uint_least32_t char32_t; +# endif +typedef float swift_float2 __attribute__((__ext_vector_type__(2))); +typedef float swift_float3 __attribute__((__ext_vector_type__(3))); +typedef float swift_float4 __attribute__((__ext_vector_type__(4))); +typedef double swift_double2 __attribute__((__ext_vector_type__(2))); +typedef double swift_double3 __attribute__((__ext_vector_type__(3))); +typedef double swift_double4 __attribute__((__ext_vector_type__(4))); +typedef int swift_int2 __attribute__((__ext_vector_type__(2))); +typedef int swift_int3 __attribute__((__ext_vector_type__(3))); +typedef int swift_int4 __attribute__((__ext_vector_type__(4))); +typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2))); +typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3))); +typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); +#endif + +#if !defined(SWIFT_PASTE) +# define SWIFT_PASTE_HELPER(x, y) x##y +# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y) +#endif +#if !defined(SWIFT_METATYPE) +# define SWIFT_METATYPE(X) Class +#endif +#if !defined(SWIFT_CLASS_PROPERTY) +# if __has_feature(objc_class_property) +# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__ +# else +# define SWIFT_CLASS_PROPERTY(...) +# endif +#endif +#if !defined(SWIFT_RUNTIME_NAME) +# if __has_attribute(objc_runtime_name) +# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) +# else +# define SWIFT_RUNTIME_NAME(X) +# endif +#endif +#if !defined(SWIFT_COMPILE_NAME) +# if __has_attribute(swift_name) +# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) +# else +# define SWIFT_COMPILE_NAME(X) +# endif +#endif +#if !defined(SWIFT_METHOD_FAMILY) +# if __has_attribute(objc_method_family) +# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) +# else +# define SWIFT_METHOD_FAMILY(X) +# endif +#endif +#if !defined(SWIFT_NOESCAPE) +# if __has_attribute(noescape) +# define SWIFT_NOESCAPE __attribute__((noescape)) +# else +# define SWIFT_NOESCAPE +# endif +#endif +#if !defined(SWIFT_RELEASES_ARGUMENT) +# if __has_attribute(ns_consumed) +# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed)) +# else +# define SWIFT_RELEASES_ARGUMENT +# endif +#endif +#if !defined(SWIFT_WARN_UNUSED_RESULT) +# if __has_attribute(warn_unused_result) +# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +# else +# define SWIFT_WARN_UNUSED_RESULT +# endif +#endif +#if !defined(SWIFT_NORETURN) +# if __has_attribute(noreturn) +# define SWIFT_NORETURN __attribute__((noreturn)) +# else +# define SWIFT_NORETURN +# endif +#endif +#if !defined(SWIFT_CLASS_EXTRA) +# define SWIFT_CLASS_EXTRA +#endif +#if !defined(SWIFT_PROTOCOL_EXTRA) +# define SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_ENUM_EXTRA) +# define SWIFT_ENUM_EXTRA +#endif +#if !defined(SWIFT_CLASS) +# if __has_attribute(objc_subclassing_restricted) +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# else +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# endif +#endif +#if !defined(SWIFT_RESILIENT_CLASS) +# if __has_attribute(objc_class_stub) +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub)) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME) +# else +# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) +# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME) +# endif +#endif +#if !defined(SWIFT_PROTOCOL) +# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_EXTENSION) +# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__) +#endif +#if !defined(OBJC_DESIGNATED_INITIALIZER) +# if __has_attribute(objc_designated_initializer) +# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) +# else +# define OBJC_DESIGNATED_INITIALIZER +# endif +#endif +#if !defined(SWIFT_ENUM_ATTR) +# if __has_attribute(enum_extensibility) +# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility))) +# else +# define SWIFT_ENUM_ATTR(_extensibility) +# endif +#endif +#if !defined(SWIFT_ENUM) +# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# if __has_feature(generalized_swift_name) +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# else +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility) +# endif +#endif +#if !defined(SWIFT_UNAVAILABLE) +# define SWIFT_UNAVAILABLE __attribute__((unavailable)) +#endif +#if !defined(SWIFT_UNAVAILABLE_MSG) +# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg))) +#endif +#if !defined(SWIFT_AVAILABILITY) +# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__))) +#endif +#if !defined(SWIFT_WEAK_IMPORT) +# define SWIFT_WEAK_IMPORT __attribute__((weak_import)) +#endif +#if !defined(SWIFT_DEPRECATED) +# define SWIFT_DEPRECATED __attribute__((deprecated)) +#endif +#if !defined(SWIFT_DEPRECATED_MSG) +# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__))) +#endif +#if !defined(SWIFT_DEPRECATED_OBJC) +# if __has_feature(attribute_diagnose_if_objc) +# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) +# else +# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) +# endif +#endif +#if defined(__OBJC__) +#if !defined(IBSegueAction) +# define IBSegueAction +#endif +#endif +#if !defined(SWIFT_EXTERN) +# if defined(__cplusplus) +# define SWIFT_EXTERN extern "C" +# else +# define SWIFT_EXTERN extern +# endif +#endif +#if !defined(SWIFT_CALL) +# define SWIFT_CALL __attribute__((swiftcall)) +#endif +#if !defined(SWIFT_INDIRECT_RESULT) +# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result)) +#endif +#if !defined(SWIFT_CONTEXT) +# define SWIFT_CONTEXT __attribute__((swift_context)) +#endif +#if !defined(SWIFT_ERROR_RESULT) +# define SWIFT_ERROR_RESULT __attribute__((swift_error_result)) +#endif +#if defined(__cplusplus) +# define SWIFT_NOEXCEPT noexcept +#else +# define SWIFT_NOEXCEPT +#endif +#if !defined(SWIFT_C_INLINE_THUNK) +# if __has_attribute(always_inline) +# if __has_attribute(nodebug) +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug)) +# else +# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) +# endif +# else +# define SWIFT_C_INLINE_THUNK inline +# endif +#endif +#if defined(_WIN32) +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport) +#endif +#else +#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL) +# define SWIFT_IMPORT_STDLIB_SYMBOL +#endif +#endif +#if defined(__OBJC__) +#if __has_feature(objc_modules) +#if __has_warning("-Watimport-in-framework-header") +#pragma clang diagnostic ignored "-Watimport-in-framework-header" +#endif +#endif + +#endif +#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" +#pragma clang diagnostic ignored "-Wduplicate-method-arg" +#if __has_warning("-Wpragma-clang-attribute") +# pragma clang diagnostic ignored "-Wpragma-clang-attribute" +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma clang diagnostic ignored "-Wnullability" +#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" + +#if __has_attribute(external_source_symbol) +# pragma push_macro("any") +# undef any +# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="MyMergeableFramework",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol)) +# pragma pop_macro("any") +#endif + +#if defined(__OBJC__) +#endif +#if __has_attribute(external_source_symbol) +# pragma clang attribute pop +#endif +#if defined(__cplusplus) +#endif +#pragma clang diagnostic pop +#endif + +#else +#error unsupported Swift architecture +#endif diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Info.plist b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Info.plist new file mode 100644 index 00000000..185edbee Binary files /dev/null and b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Info.plist differ diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.abi.json b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.abi.json new file mode 100644 index 00000000..dc109fc0 --- /dev/null +++ b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.abi.json @@ -0,0 +1,9 @@ +{ + "ABIRoot": { + "kind": "Root", + "name": "TopLevel", + "printedName": "TopLevel", + "json_format_version": 8 + }, + "ConstValues": [] +} \ No newline at end of file diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface new file mode 100644 index 00000000..45ada548 --- /dev/null +++ b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface @@ -0,0 +1,8 @@ +// swift-interface-format-version: 1.0 +// swift-compiler-version: Apple Swift version 5.9 (swiftlang-5.9.0.128.2 clang-1500.0.40.1) +// swift-module-flags: -target x86_64-apple-ios14.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Osize -module-name MyMergeableFramework +// swift-module-flags-ignorable: -enable-bare-slash-regex +import Swift +import _Concurrency +import _StringProcessing +import _SwiftConcurrencyShims diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.swiftdoc b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.swiftdoc new file mode 100644 index 00000000..fd7d1731 Binary files /dev/null and b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.swiftdoc differ diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.swiftinterface b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.swiftinterface new file mode 100644 index 00000000..45ada548 --- /dev/null +++ b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.swiftinterface @@ -0,0 +1,8 @@ +// swift-interface-format-version: 1.0 +// swift-compiler-version: Apple Swift version 5.9 (swiftlang-5.9.0.128.2 clang-1500.0.40.1) +// swift-module-flags: -target x86_64-apple-ios14.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Osize -module-name MyMergeableFramework +// swift-module-flags-ignorable: -enable-bare-slash-regex +import Swift +import _Concurrency +import _StringProcessing +import _SwiftConcurrencyShims diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.swiftmodule b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.swiftmodule new file mode 100644 index 00000000..ce543b11 Binary files /dev/null and b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.swiftmodule differ diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/module.modulemap b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/module.modulemap new file mode 100644 index 00000000..99e6c409 --- /dev/null +++ b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/Modules/module.modulemap @@ -0,0 +1,4 @@ +framework module MyMergeableFramework { + header "MyMergeableFramework-Swift.h" + requires objc +} diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/MyMergeableFramework b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/MyMergeableFramework new file mode 100755 index 00000000..cfc0d3b4 Binary files /dev/null and b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/MyMergeableFramework differ diff --git a/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/_CodeSignature/CodeResources b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/_CodeSignature/CodeResources new file mode 100644 index 00000000..be447e37 --- /dev/null +++ b/Tests/Fixtures/MyMergeableFramework.xcframework/ios-x86_64-simulator/MyMergeableFramework.framework/_CodeSignature/CodeResources @@ -0,0 +1,234 @@ + + + + + files + + Headers/MyMergeableFramework-Swift.h + + w4RCnG9viE9H6Vi5Xvc7xNSXQWo= + + Info.plist + + B4D9Z6PAzO2461cS+x0GGl49Qos= + + Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios-simulator.abi.json + + FSPnLbho3G+LL9smI3XgVOqBIQ4= + + Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface + + +cndGEHri5o4xbeGuXqc8tjCbDE= + + Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios-simulator.swiftdoc + + 4MNDefVXH0W/y8yOPHNbaUNNc3o= + + Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios-simulator.swiftinterface + + +cndGEHri5o4xbeGuXqc8tjCbDE= + + Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios-simulator.swiftmodule + + O8UHwu8DlLugSOakEEzRbuenAxk= + + Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.abi.json + + FSPnLbho3G+LL9smI3XgVOqBIQ4= + + Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface + + GmmgVTYC5jtSKF5lQ6YQwDDJxEg= + + Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.swiftdoc + + FUxCtrHHM8DgtintSO6HuyC1yAk= + + Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.swiftinterface + + GmmgVTYC5jtSKF5lQ6YQwDDJxEg= + + Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.swiftmodule + + 7BWFUC3C/3V/HnsSzQ8XIGQEsPU= + + Modules/module.modulemap + + Lm9sgJu+TJhB5uO0fxIf0JhX6gs= + + + files2 + + Headers/MyMergeableFramework-Swift.h + + hash2 + + 5kx+ZnJRNdQAKfoKZ10+1mb7b/1XrESTIECwI7sjXf8= + + + Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios-simulator.abi.json + + hash2 + + KnRdWE4y6t4QM5zi5JDptPdHFgJy1Tku+7GLkZS2aNM= + + + Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface + + hash2 + + RR6oRarwN9IKLHbt6oyiRw4JSSpsHdX6UJ393EBV1cA= + + + Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios-simulator.swiftdoc + + hash2 + + +Wh8NXttchbLfw99ZTyFVBdP3cUVs3W3ReSPfGuJtnU= + + + Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios-simulator.swiftinterface + + hash2 + + RR6oRarwN9IKLHbt6oyiRw4JSSpsHdX6UJ393EBV1cA= + + + Modules/MyMergeableFramework.swiftmodule/arm64-apple-ios-simulator.swiftmodule + + hash2 + + dbCh5gJV6yjNaL7rc4EcNMlLBpHCgw9nhE5MERxO+34= + + + Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.abi.json + + hash2 + + KnRdWE4y6t4QM5zi5JDptPdHFgJy1Tku+7GLkZS2aNM= + + + Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface + + hash2 + + PgTuaVbq1xW21aZaMmexdhvJheLdYs2MyWHe2UWsPes= + + + Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.swiftdoc + + hash2 + + CPD7nA2tyFGLcLmrR11zo/QVVwYpsg7kb3m4XywB0nw= + + + Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.swiftinterface + + hash2 + + PgTuaVbq1xW21aZaMmexdhvJheLdYs2MyWHe2UWsPes= + + + Modules/MyMergeableFramework.swiftmodule/x86_64-apple-ios-simulator.swiftmodule + + hash2 + + 7uevwCK+DoZS/VRMEMF5HNbx3YRjz9oEwaR16F1pY1c= + + + Modules/module.modulemap + + hash2 + + d0CZBm0cgf2B8Z+wMc3nuPQhyFa6e66QTjvMZk84KW0= + + + + rules + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/Tests/Fixtures/MyStaticLibrary.xcframework/Info.plist b/Tests/Fixtures/MyStaticLibrary.xcframework/Info.plist new file mode 100644 index 00000000..ea5d5237 --- /dev/null +++ b/Tests/Fixtures/MyStaticLibrary.xcframework/Info.plist @@ -0,0 +1,39 @@ + + + + + AvailableLibraries + + + LibraryIdentifier + ios-x86_64-simulator + LibraryPath + libMyStaticLibrary.a + SupportedArchitectures + + x86_64 + + SupportedPlatform + ios + SupportedPlatformVariant + simulator + + + LibraryIdentifier + ios-arm64 + LibraryPath + libMyStaticLibrary.a + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + diff --git a/Tests/Fixtures/MyStaticLibrary.xcframework/ios-arm64/libMyStaticLibrary.a b/Tests/Fixtures/MyStaticLibrary.xcframework/ios-arm64/libMyStaticLibrary.a new file mode 100644 index 00000000..bfc9226e Binary files /dev/null and b/Tests/Fixtures/MyStaticLibrary.xcframework/ios-arm64/libMyStaticLibrary.a differ diff --git a/Tests/Fixtures/MyStaticLibrary.xcframework/ios-x86_64-simulator/libMyStaticLibrary.a b/Tests/Fixtures/MyStaticLibrary.xcframework/ios-x86_64-simulator/libMyStaticLibrary.a new file mode 100644 index 00000000..3bcb4bf7 Binary files /dev/null and b/Tests/Fixtures/MyStaticLibrary.xcframework/ios-x86_64-simulator/libMyStaticLibrary.a differ diff --git a/Tests/Fixtures/libStaticLibrary.a b/Tests/Fixtures/libStaticLibrary.a new file mode 100644 index 00000000..c541b643 Binary files /dev/null and b/Tests/Fixtures/libStaticLibrary.a differ diff --git a/Tests/Fixtures/xpm.framework.dSYM/Contents/Info.plist b/Tests/Fixtures/xpm.framework.dSYM/Contents/Info.plist new file mode 100644 index 00000000..0c2324d5 --- /dev/null +++ b/Tests/Fixtures/xpm.framework.dSYM/Contents/Info.plist @@ -0,0 +1,20 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleIdentifier + com.apple.xcode.dsym.com.xcodepm.tuist + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + dSYM + CFBundleSignature + ???? + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Tests/Fixtures/xpm.framework.dSYM/Contents/Resources/DWARF/xpm b/Tests/Fixtures/xpm.framework.dSYM/Contents/Resources/DWARF/xpm new file mode 100644 index 00000000..a9884cde Binary files /dev/null and b/Tests/Fixtures/xpm.framework.dSYM/Contents/Resources/DWARF/xpm differ diff --git a/Tests/Fixtures/xpm.framework/Headers/xpm.h b/Tests/Fixtures/xpm.framework/Headers/xpm.h new file mode 100644 index 00000000..91bd67d6 --- /dev/null +++ b/Tests/Fixtures/xpm.framework/Headers/xpm.h @@ -0,0 +1,19 @@ +// +// tuist.h +// tuist +// +// Created by Pedro Piñera Buendía on 03.06.18. +// Copyright © 2018 com.xcodepm. All rights reserved. +// + +#import + +//! Project version number for tuist. +FOUNDATION_EXPORT double tuistVersionNumber; + +//! Project version string for tuist. +FOUNDATION_EXPORT const unsigned char tuistVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Tests/Fixtures/xpm.framework/Info.plist b/Tests/Fixtures/xpm.framework/Info.plist new file mode 100644 index 00000000..12224c19 Binary files /dev/null and b/Tests/Fixtures/xpm.framework/Info.plist differ diff --git a/Tests/Fixtures/xpm.framework/Modules/module.modulemap b/Tests/Fixtures/xpm.framework/Modules/module.modulemap new file mode 100644 index 00000000..5e94fd75 --- /dev/null +++ b/Tests/Fixtures/xpm.framework/Modules/module.modulemap @@ -0,0 +1,6 @@ +framework module tuist { + umbrella header "tuist.h" + + export * + module * { export * } +} diff --git a/Tests/Fixtures/xpm.framework/PrivateHeaders/private.h b/Tests/Fixtures/xpm.framework/PrivateHeaders/private.h new file mode 100644 index 00000000..e69de29b diff --git a/Tests/Fixtures/xpm.framework/xpm b/Tests/Fixtures/xpm.framework/xpm new file mode 100755 index 00000000..95b2bcd3 Binary files /dev/null and b/Tests/Fixtures/xpm.framework/xpm differ diff --git a/Tests/XcodeGraphTests/Extensions/XCTestCase+Extras.swift b/Tests/XcodeGraphTests/Extensions/XCTestCase+Extras.swift index 8befa2ce..066cd3e1 100644 --- a/Tests/XcodeGraphTests/Extensions/XCTestCase+Extras.swift +++ b/Tests/XcodeGraphTests/Extensions/XCTestCase+Extras.swift @@ -13,7 +13,7 @@ extension XCTestCase { XCTAssertEqual(subject, decoded, "The subject is not equal to it's encoded & decoded version") } - func XCTTry(_ closure: @autoclosure @escaping () throws -> T, file: StaticString = #file, line: UInt = #line) -> T { + func XCTTry(_ closure: @autoclosure @escaping () throws -> T, file: StaticString = #filePath, line: UInt = #line) -> T { var value: T! do { value = try closure() @@ -26,7 +26,7 @@ extension XCTestCase { func XCTAssertEqualDictionaries( _ first: [T: Any], _ second: [T: Any], - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { let firstDictionary = NSDictionary(dictionary: first) diff --git a/Tests/XcodeGraphTests/Models/TestableTargetTests.swift b/Tests/XcodeGraphTests/Models/TestableTargetTests.swift index ea780400..6e56f7f7 100644 --- a/Tests/XcodeGraphTests/Models/TestableTargetTests.swift +++ b/Tests/XcodeGraphTests/Models/TestableTargetTests.swift @@ -7,7 +7,7 @@ import XCTest final class TestableTargetTests: XCTestCase { func test_codable_with_deprecated_parallelizable() { // Given - let subject = TestableTarget( + let subject = TestableTarget.test( target: .init( projectPath: try! AbsolutePath(validating: "/path/to/project"), name: "name" diff --git a/Tests/XcodeMetadataTests/AssertionsTesting.swift b/Tests/XcodeMetadataTests/AssertionsTesting.swift new file mode 100644 index 00000000..d2f75e87 --- /dev/null +++ b/Tests/XcodeMetadataTests/AssertionsTesting.swift @@ -0,0 +1,30 @@ +import Foundation +import Path +import ServiceContextModule +import Testing + +enum AssertionsTesting { + // MARK: - Fixtures + + /// Resolves a fixture path relative to the project's root. + static func fixturePath(path: RelativePath) -> AbsolutePath { + try! AbsolutePath( + validating: #filePath + ) + .parentDirectory + .parentDirectory + .appending(components: "Fixtures") + .appending(path) + } +} + +extension AbsolutePath: Swift.ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + do { + self = try AbsolutePath(validating: value) + } catch { + Issue.record("Invalid path at: \(value) - Error: \(error)") + self = AbsolutePath("/") + } + } +} diff --git a/Tests/XcodeMetadataTests/FrameworkMetadataProviderTests.swift b/Tests/XcodeMetadataTests/FrameworkMetadataProviderTests.swift new file mode 100644 index 00000000..3dbc1d37 --- /dev/null +++ b/Tests/XcodeMetadataTests/FrameworkMetadataProviderTests.swift @@ -0,0 +1,39 @@ +import Path +import Testing +import XcodeGraph +import XcodeMetadata + +@Suite +struct FrameworkMetadataProviderTests { + var subject: FrameworkMetadataProvider + + init() { + subject = FrameworkMetadataProvider() + } + + @Test + func loadMetadata() async throws { + // Given + let frameworkPath = AssertionsTesting.fixturePath(path: try RelativePath(validating: "xpm.framework")) + + // When + let metadata = try await subject.loadMetadata(at: frameworkPath, status: .required) + + // Then + let expectedBinaryPath = frameworkPath.appending(component: frameworkPath.basenameWithoutExt) + let expectedDsymPath = frameworkPath.parentDirectory.appending(component: "xpm.framework.dSYM") + + #expect( + metadata == FrameworkMetadata( + path: frameworkPath, + binaryPath: expectedBinaryPath, + dsymPath: expectedDsymPath, + bcsymbolmapPaths: [], + linking: .dynamic, + architectures: [.x8664, .arm64], + status: .required + ), + "Loaded metadata does not match expected metadata" + ) + } +} diff --git a/Tests/XcodeMetadataTests/LibraryMetadataProviderTests.swift b/Tests/XcodeMetadataTests/LibraryMetadataProviderTests.swift new file mode 100644 index 00000000..cab1193a --- /dev/null +++ b/Tests/XcodeMetadataTests/LibraryMetadataProviderTests.swift @@ -0,0 +1,39 @@ +import Path +import Testing +import XcodeGraph +@testable import XcodeMetadata + +@Suite +struct LibraryMetadataProviderTests { + var subject: LibraryMetadataProvider + + /// Initializes the test suite, setting up the required `LibraryMetadataProvider` instance. + init() { + subject = LibraryMetadataProvider() + } + + @Test + func loadMetadata() async throws { + // Given + let libraryPath = AssertionsTesting.fixturePath(path: try RelativePath(validating: "libStaticLibrary.a")) + + // When + let metadata = try await subject.loadMetadata( + at: libraryPath, + publicHeaders: libraryPath.parentDirectory, + swiftModuleMap: nil + ) + + // Then + #expect( + metadata == LibraryMetadata( + path: libraryPath, + publicHeaders: libraryPath.parentDirectory, + swiftModuleMap: nil, + architectures: [.x8664], + linking: .static + ), + "Loaded metadata does not match expected metadata" + ) + } +} diff --git a/Tests/XcodeMetadataTests/PrecompiledMetadataProviderTests.swift b/Tests/XcodeMetadataTests/PrecompiledMetadataProviderTests.swift new file mode 100644 index 00000000..e9b4082f --- /dev/null +++ b/Tests/XcodeMetadataTests/PrecompiledMetadataProviderTests.swift @@ -0,0 +1,96 @@ +import Foundation +import Path +import Testing +import XcodeGraph +@testable import XcodeMetadata + +@Suite +struct PrecompiledMetadataProviderTests { + var subject: PrecompiledMetadataProvider + + /// Initializes the test suite, setting up the required `PrecompiledMetadataProvider` instance. + init() { + subject = PrecompiledMetadataProvider() + } + + @Test + func metadataStatic() throws { + // Given + let binaryPath = AssertionsTesting.fixturePath(path: try RelativePath(validating: "libStaticLibrary.a")) + + // When + let architectures = try subject.architectures(binaryPath: binaryPath) + let linking = try subject.linking(binaryPath: binaryPath) + let uuids = try subject.uuids(binaryPath: binaryPath) + + // Then + #expect(architectures == [.x8664], "Architectures do not match expected value") + #expect(linking == BinaryLinking.static, "Linking does not match expected value") + #expect(uuids == Set(), "UUIDs do not match expected value") + } + + @Test + func metadataFramework() throws { + // Given + let binaryPath = AssertionsTesting.fixturePath(path: try RelativePath(validating: "xpm.framework/xpm")) + + // When + let architectures = try subject.architectures(binaryPath: binaryPath) + let linking = try subject.linking(binaryPath: binaryPath) + let uuids = try subject.uuids(binaryPath: binaryPath) + + // Then + #expect(architectures == [.x8664, .arm64], "Architectures do not match expected value") + #expect(linking == BinaryLinking.dynamic, "Linking does not match expected value") + #expect( + uuids == Set([ + UUID(uuidString: "FB17107A-86FA-3880-92AC-C9AA9E04BA98"), + UUID(uuidString: "510FD121-B669-3524-A748-2DDF357A051C"), + ]), + "UUIDs do not match expected value" + ) + } + + @Test + func metadataXCFramework() throws { + // Given + let binaryPath = AssertionsTesting.fixturePath( + path: try RelativePath( + validating: "MyFramework.xcframework/ios-x86_64-simulator/MyFramework.framework/MyFramework" + ) + ) + + // When + let architectures = try subject.architectures(binaryPath: binaryPath) + let linking = try subject.linking(binaryPath: binaryPath) + let uuids = try subject.uuids(binaryPath: binaryPath) + + // Then + #expect(architectures == [.x8664], "Architectures do not match expected value") + #expect(linking == BinaryLinking.dynamic, "Linking does not match expected value") + #expect( + uuids == Set([ + UUID(uuidString: "725302D8-8353-312F-8BF4-564B24F7B3E8"), + ]), + "UUIDs do not match expected value" + ) + } + + @Test + func metadataStaticXCFramework() throws { + // Given + let binaryPath = AssertionsTesting.fixturePath( + path: try RelativePath(validating: "MyStaticLibrary.xcframework/ios-arm64/libMyStaticLibrary.a") + ) + + // When + let architectures = try subject.architectures(binaryPath: binaryPath) + let linking = try subject.linking(binaryPath: binaryPath) + let uuids = try subject.uuids(binaryPath: binaryPath) + + // Then + #expect(architectures == [.arm64], "Architectures do not match expected value") + #expect(linking == BinaryLinking.static, "Linking does not match expected value") + #expect(uuids == Set(), "UUIDs do not match expected value") + } +} diff --git a/Tests/XcodeMetadataTests/SystemFrameworkMetadataProviderTests.swift b/Tests/XcodeMetadataTests/SystemFrameworkMetadataProviderTests.swift new file mode 100644 index 00000000..11d4d152 --- /dev/null +++ b/Tests/XcodeMetadataTests/SystemFrameworkMetadataProviderTests.swift @@ -0,0 +1,108 @@ +import Testing +import XcodeGraph +@testable import XcodeMetadata + +@Suite +struct SystemFrameworkMetadataProviderTests { + var subject: SystemFrameworkMetadataProvider + + /// Initializes the test suite, setting up the required `SystemFrameworkMetadataProvider` instance. + init() { + subject = SystemFrameworkMetadataProvider() + } + + @Test + func loadMetadataFramework() throws { + // Given + let sdkName = "UIKit.framework" + + // When + let metadata = try subject.loadMetadata(sdkName: sdkName, status: .required, platform: .iOS, source: .system) + + // Then + #expect( + metadata == SystemFrameworkMetadata( + name: sdkName, + path: "/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/UIKit.framework", + status: .required, + source: .system + ), + "Metadata does not match expected value" + ) + } + + @Test + func loadMetadataLibrary() throws { + // Given + let sdkName = "libc++.tbd" + + // When + let metadata = try subject.loadMetadata(sdkName: sdkName, status: .required, platform: .iOS, source: .system) + + // Then + #expect( + metadata == SystemFrameworkMetadata( + name: sdkName, + path: "/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/libc++.tbd", + status: .required, + source: .system + ), + "Metadata does not match expected value" + ) + } + + @Test + func loadMetadataSwiftLibrary() throws { + // Given + let sdkName = "libswiftObservation.tbd" + + // When + let metadata = try subject.loadMetadata(sdkName: sdkName, status: .required, platform: .iOS, source: .system) + + // Then + #expect( + metadata == SystemFrameworkMetadata( + name: sdkName, + path: "/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/swift/libswiftObservation.tbd", + status: .required, + source: .system + ), + "Metadata does not match expected value" + ) + } + + @Test + func loadMetadataUnsupportedType() throws { + // Given + let sdkName = "UIKit.xcframework" + + // When / Then + #expect( + throws: SystemFrameworkMetadataProviderError.unsupportedSDK(name: sdkName) + ) { + try subject.loadMetadata(sdkName: sdkName, status: .required, platform: .iOS, source: .system) + } + } + + @Test + func loadMetadataDeveloperSourceSupportedPlatform() throws { + // Given + let sdkName = "XCTest.framework" + let source = SDKSource.developer + let platform = Platform.iOS + + // When + let metadata = try subject.loadMetadata(sdkName: sdkName, status: .required, platform: platform, source: source) + + // Then + #expect( + metadata == SystemFrameworkMetadata( + name: sdkName, + path: "/Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework", + status: .required, + source: .developer + ), + "Metadata does not match expected value" + ) + } +} diff --git a/Tests/XcodeMetadataTests/XCFrameworkMetadataProviderTests.swift b/Tests/XcodeMetadataTests/XCFrameworkMetadataProviderTests.swift new file mode 100644 index 00000000..37977b63 --- /dev/null +++ b/Tests/XcodeMetadataTests/XCFrameworkMetadataProviderTests.swift @@ -0,0 +1,251 @@ +import Path +import ServiceContextModule +import Testing +import XcodeGraph +@testable import XcodeMetadata + +@Suite +struct XCFrameworkMetadataProviderTests { + private var subject: XCFrameworkMetadataProvider + + init() { + subject = XCFrameworkMetadataProvider() + } + + @Test + func librariesWhenFrameworkIsPresent() async throws { + // Given + let relativePath = try RelativePath(validating: "MyFramework.xcframework") + let frameworkPath = AssertionsTesting.fixturePath( + path: relativePath + ) + + // When + let infoPlist = try await subject.infoPlist(xcframeworkPath: frameworkPath) + + // Then + let expectedPath = try RelativePath(validating: "MyFramework.framework") + #expect( + infoPlist.libraries == [ + XCFrameworkInfoPlist.Library( + identifier: "ios-x86_64-simulator", + path: expectedPath, + mergeable: false, + platform: .iOS, + architectures: [.x8664] + ), + XCFrameworkInfoPlist.Library( + identifier: "ios-arm64", + path: expectedPath, + mergeable: false, + platform: .iOS, + architectures: [.arm64] + ), + ], + "Libraries do not match expected value" + ) + for library in infoPlist.libraries { + #expect(library.binaryName == "MyFramework", "Binary name does not match") + } + } + + @Test + func librariesWhenStaticLibraryIsPresent() async throws { + // Given + let relativePath = try RelativePath(validating: "MyStaticLibrary.xcframework") + let xcframeworkPath = AssertionsTesting.fixturePath( + path: relativePath + ) + + // When + let infoPlist = try await subject.infoPlist(xcframeworkPath: xcframeworkPath) + + let expectedPath = try RelativePath(validating: "libMyStaticLibrary.a") + + // Then + #expect( + infoPlist.libraries == [ + XCFrameworkInfoPlist.Library( + identifier: "ios-x86_64-simulator", + path: expectedPath, + mergeable: false, + platform: .iOS, + architectures: [.x8664] + ), + XCFrameworkInfoPlist.Library( + identifier: "ios-arm64", + path: expectedPath, + mergeable: false, + platform: .iOS, + architectures: [.arm64] + ), + ], + "Libraries do not match expected value" + ) + for library in infoPlist.libraries { + #expect(library.binaryName == "libMyStaticLibrary", "Binary name does not match") + } + } + + @Test + func loadMetadataDynamicLibrary() async throws { + // Given + let relativePath = try RelativePath(validating: "MyFramework.xcframework") + + let frameworkPath = AssertionsTesting.fixturePath( + path: relativePath + ) + + // When + let metadata = try await subject.loadMetadata(at: frameworkPath, status: .required) + + // Then + let expectedInfoPlist = XCFrameworkInfoPlist(libraries: [ + XCFrameworkInfoPlist.Library( + identifier: "ios-x86_64-simulator", + path: try RelativePath(validating: "MyFramework.framework"), + mergeable: false, + platform: .iOS, + architectures: [.x8664] + ), + XCFrameworkInfoPlist.Library( + identifier: "ios-arm64", + path: try RelativePath(validating: "MyFramework.framework"), + mergeable: false, + platform: .iOS, + architectures: [.arm64] + ), + ]) + + #expect( + metadata == XCFrameworkMetadata( + path: frameworkPath, + infoPlist: expectedInfoPlist, + linking: .dynamic, + mergeable: false, + status: .required, + macroPath: nil, + swiftModules: [ + frameworkPath.appending( + components: "ios-arm64", + "MyFramework.framework", + "Modules", + "MyFramework.swiftmodule" + ), + frameworkPath.appending( + components: "ios-x86_64-simulator", + "MyFramework.framework", + "Modules", + "MyFramework.swiftmodule" + ), + ], + moduleMaps: [ + frameworkPath.appending(components: "ios-arm64", "MyFramework.framework", "Modules", "module.modulemap"), + frameworkPath.appending( + components: "ios-x86_64-simulator", + "MyFramework.framework", + "Modules", + "module.modulemap" + ), + ] + ), + "Metadata does not match expected value" + ) + } + + @Test + func loadMetadataMergeableDynamicLibrary() async throws { + // Given + let frameworkPath = AssertionsTesting.fixturePath( + path: try RelativePath(validating: "MyMergeableFramework.xcframework") + ) + + // When + let metadata = try await subject.loadMetadata(at: frameworkPath, status: .required) + + // Then + let expectedInfoPlist = XCFrameworkInfoPlist(libraries: [ + XCFrameworkInfoPlist.Library( + identifier: "ios-x86_64-simulator", + path: try RelativePath(validating: "MyMergeableFramework.framework"), + mergeable: true, + platform: .iOS, + architectures: [.x8664] + ), + XCFrameworkInfoPlist.Library( + identifier: "ios-arm64", + path: try RelativePath(validating: "MyMergeableFramework.framework"), + mergeable: true, + platform: .iOS, + architectures: [.arm64] + ), + ]) + + #expect( + metadata == XCFrameworkMetadata( + path: frameworkPath, + infoPlist: expectedInfoPlist, + linking: .dynamic, + mergeable: true, + status: .required, + macroPath: nil, + swiftModules: [ + frameworkPath.appending( + components: "ios-arm64", + "MyMergeableFramework.framework", + "Modules", + "MyMergeableFramework.swiftmodule" + ), + frameworkPath.appending( + components: "ios-arm64", + "MyMergeableFramework.framework", + "Modules", + "MyMergeableFramework.swiftmodule", + "arm64-apple-ios.swiftmodule" + ), + frameworkPath.appending( + components: "ios-x86_64-simulator", + "MyMergeableFramework.framework", + "Modules", + "MyMergeableFramework.swiftmodule" + ), + frameworkPath.appending( + components: "ios-x86_64-simulator", + "MyMergeableFramework.framework", + "Modules", + "MyMergeableFramework.swiftmodule", + "x86_64-apple-ios-simulator.swiftmodule" + ), + ], + moduleMaps: [ + frameworkPath.appending( + components: "ios-arm64", + "MyMergeableFramework.framework", + "Modules", + "module.modulemap" + ), + frameworkPath.appending( + components: "ios-x86_64-simulator", + "MyMergeableFramework.framework", + "Modules", + "module.modulemap" + ), + ] + ) + ) + } + + @Test + func loadMetadataFrameworkMissingArchitecture() async throws { + // Given + let frameworkPath = AssertionsTesting.fixturePath( + path: try RelativePath(validating: "MyFrameworkMissingArch.xcframework") + ) + + // When + let metadata = try await subject.loadMetadata(at: frameworkPath, status: .required) + + // Then + #expect(metadata.infoPlist.libraries.count == 2, "Libraries count mismatch") + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Graph/GraphMapperTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Graph/GraphMapperTests.swift new file mode 100644 index 00000000..ed3eb39c --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Graph/GraphMapperTests.swift @@ -0,0 +1,216 @@ +import Foundation +import Path +import Testing +import XcodeGraph +import XcodeProj +@testable import XcodeProjMapper + +@Suite +struct XcodeGraphMapperTests { + @Test("Maps a single project into a workspace graph") + func testSingleProjectGraph() async throws { + // Given + let pbxProj = PBXProj() + let debug: XCBuildConfiguration = .testDebug().add(to: pbxProj) + let releaseConfig: XCBuildConfiguration = .testRelease().add(to: pbxProj) + let configurationList: XCConfigurationList = .test(buildConfigurations: [debug, releaseConfig]) + .add(to: pbxProj) + + let xcodeProj = try await XcodeProj.test( + projectName: "SingleProject", + configurationList: configurationList, + pbxProj: pbxProj + ) + + let sourceFile = try PBXFileReference.test( + path: "ViewController.swift", + lastKnownFileType: "sourcecode.swift" + ).add(to: pbxProj).addToMainGroup(in: pbxProj) + + let buildFile = PBXBuildFile(file: sourceFile).add(to: pbxProj) + let sourcesPhase = PBXSourcesBuildPhase(files: [buildFile]).add(to: pbxProj) + + // Add a single target to the project + try PBXNativeTarget.test( + name: "App", + buildConfigurationList: configurationList, + buildPhases: [sourcesPhase], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let projectPath = xcodeProj.projectPath + try xcodeProj.write(path: xcodeProj.path!) + let mapper = XcodeGraphMapper() + // When + let graph = try await mapper.buildGraph(from: .project(xcodeProj)) + + // Then + #expect(graph.name == "Workspace") + #expect(graph.projects.count == 1) + #expect(graph.packages.isEmpty == true) + #expect(graph.dependencies.isEmpty == true) + #expect(graph.dependencyConditions.isEmpty == true) + // Workspace should wrap the single project + #expect(graph.workspace.projects.count == 1) + #expect(graph.workspace.projects.first == projectPath) + #expect(graph.workspace.name == "Workspace") + } + + @Test("Maps a workspace with multiple projects into a single graph") + func testWorkspaceGraphMultipleProjects() async throws { + // Given + let pbxProjA = PBXProj() + let pbxProjB = PBXProj() + + let debug: XCBuildConfiguration = .testDebug().add(to: pbxProjA).add(to: pbxProjB) + let releaseConfig: XCBuildConfiguration = .testRelease().add(to: pbxProjA).add(to: pbxProjB) + let configurationList: XCConfigurationList = .test( + buildConfigurations: [debug, releaseConfig] + ) + .add(to: pbxProjA) + .add(to: pbxProjB) + + let projectA = try await XcodeProj.test( + projectName: "ProjectA", + configurationList: configurationList, + pbxProj: pbxProjA + ) + + let projectB = try await XcodeProj.test( + projectName: "ProjectB", + configurationList: configurationList, + pbxProj: pbxProjB + ) + + let sourceFile = try PBXFileReference.test( + path: "ViewController.swift", + lastKnownFileType: "sourcecode.swift" + ).add(to: pbxProjA).addToMainGroup(in: pbxProjA) + + let buildFile = PBXBuildFile(file: sourceFile).add(to: pbxProjB).add(to: pbxProjA) + let sourcesPhase = PBXSourcesBuildPhase(files: [buildFile]).add(to: pbxProjB).add(to: pbxProjA) + + // Add targets to each project + try PBXNativeTarget.test( + name: "ATarget", + buildConfigurationList: configurationList, + buildPhases: [sourcesPhase], + productType: .framework + ) + .add(to: pbxProjA) + .add(to: pbxProjA.rootObject) + + try PBXNativeTarget.test( + name: "BTarget", + buildConfigurationList: configurationList, + buildPhases: [sourcesPhase], + productType: .framework + ) + .add(to: pbxProjB) + .add(to: pbxProjB.rootObject) + + let projectAPath = try #require(projectA.path?.string) + let projectBPath = try #require(projectB.path?.string) + + let xcworkspace = XCWorkspace( + data: XCWorkspaceData( + children: [ + .file(.init(location: .absolute(projectAPath))), + .file(.init(location: .absolute(projectBPath))), + ] + ), + path: .init(projectAPath.appending("/Workspace.xcworkspace")) + ) + + try projectA.write(path: projectA.path!) + try projectB.write(path: projectB.path!) + let mapper = XcodeGraphMapper() + + // When + let graph = try await mapper.buildGraph(from: .workspace(xcworkspace)) + print(projectA.path!) + // Then + #expect(graph.workspace.name == "Workspace") + #expect(graph.workspace.projects.contains(projectA.projectPath) == true) + #expect(graph.workspace.projects.contains(projectB.projectPath) == true) + #expect(graph.projects.count == 2) + + let mappedProjectA = try #require(graph.projects[projectA.projectPath]) + let mappedProjectB = try #require(graph.projects[projectB.projectPath]) + #expect(mappedProjectA.targets["ATarget"] != nil) + #expect(mappedProjectB.targets["BTarget"] != nil) + + // No packages or dependencies + #expect(graph.packages.isEmpty == true) + #expect(graph.dependencies.isEmpty == true) + #expect(graph.dependencyConditions.isEmpty == true) + } + + @Test("Maps a project graph with dependencies between targets") + func testGraphWithDependencies() async throws { + // Given + let pbxProj = PBXProj() + let debug: XCBuildConfiguration = .testDebug().add(to: pbxProj) + let releaseConfig: XCBuildConfiguration = .testRelease().add(to: pbxProj) + let configurationList: XCConfigurationList = .test( + buildConfigurations: [debug, releaseConfig] + ) + .add(to: pbxProj) + + let xcodeProj = try await XcodeProj.test( + projectName: "ProjectWithDeps", + configurationList: configurationList, + pbxProj: pbxProj + ) + + let sourceFile = try PBXFileReference.test( + path: "ViewController.swift", + lastKnownFileType: "sourcecode.swift" + ) + .add(to: pbxProj) + .addToMainGroup(in: pbxProj) + + let buildFile = PBXBuildFile(file: sourceFile).add(to: pbxProj) + let sourcesPhase = PBXSourcesBuildPhase(files: [buildFile]).add(to: pbxProj) + + let appTarget = try PBXNativeTarget.test( + name: "App", + buildConfigurationList: configurationList, + buildPhases: [sourcesPhase], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + // App -> AFramework dependency + let frameworkTarget = try PBXNativeTarget.test( + name: "AFramework", + buildConfigurationList: configurationList, + buildPhases: [sourcesPhase], + productType: .framework + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let dep = PBXTargetDependency( + name: "AFramework", + target: frameworkTarget + ) + .add(to: pbxProj) + appTarget.dependencies.append(dep) + try xcodeProj.write(path: xcodeProj.path!) + let mapper = XcodeGraphMapper() + + // When + let graph = try await mapper.buildGraph(from: .project(xcodeProj)) + + // Then + // Verify dependencies are mapped + let targetDep = GraphDependency.target(name: "AFramework", path: xcodeProj.srcPath) + let expectedDependency = try #require(graph.dependencies.first?.value) + + #expect(expectedDependency == [targetDep]) + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Package/XCPackageMapperTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Package/XCPackageMapperTests.swift new file mode 100644 index 00000000..44cc77a2 --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Package/XCPackageMapperTests.swift @@ -0,0 +1,185 @@ +import Path +import Testing +import XcodeGraph +import XcodeProj +@testable import XcodeProjMapper + +@Suite +struct XCPackageMapperTests { + let mapper: XCPackageMapper + + init() { + mapper = XCPackageMapper() + } + + @Test("Maps a remote package with a valid URL and up-to-next-major requirement") + func testMapPackageWithValidURL() async throws { + // Given + let package = XCRemoteSwiftPackageReference( + repositoryURL: "https://github.com/example/package.git", + versionRequirement: .upToNextMajorVersion("1.0.0") + ) + + // When + let result = try mapper.map(package: package) + + // Then + #expect( + result + == .remote( + url: "https://github.com/example/package.git", + requirement: .upToNextMajor("1.0.0") + ) + ) + } + + @Test("Maps an up-to-next-major version requirement correctly") + func testMapRequirementUpToNextMajor() async throws { + // Given + let package = XCRemoteSwiftPackageReference( + repositoryURL: "https://github.com/example/package.git", + versionRequirement: .upToNextMajorVersion("1.0.0") + ) + + let mapped = try mapper.map(package: package) + + // Then + #expect(mapped.requirement == .upToNextMajor("1.0.0")) + } + + @Test("Maps an up-to-next-minor version requirement correctly") + func testMapRequirementUpToNextMinor() async throws { + // Given + let package = XCRemoteSwiftPackageReference( + repositoryURL: "https://github.com/example/package.git", + versionRequirement: .upToNextMinorVersion("1.2.0") + ) + + // When + let mapped = try mapper.map(package: package) + + // Then + #expect(mapped.requirement == .upToNextMinor("1.2.0")) + } + + @Test("Maps an exact version requirement correctly") + func testMapRequirementExact() async throws { + // Given + let package = XCRemoteSwiftPackageReference( + repositoryURL: "https://github.com/example/package.git", + versionRequirement: .exact("1.2.3") + ) + + // When + let mapped = try mapper.map(package: package) + + // Then + #expect(mapped.requirement == .exact("1.2.3")) + } + + @Test("Maps a range version requirement correctly") + func testMapRequirementRange() async throws { + // Given + let package = XCRemoteSwiftPackageReference( + repositoryURL: "https://github.com/example/package.git", + versionRequirement: .range(from: "1.0.0", to: "2.0.0") + ) + + // When + let mapped = try mapper.map(package: package) + + // Then + #expect(mapped.requirement == .range(from: "1.0.0", to: "2.0.0")) + } + + @Test("Maps a branch-based version requirement correctly") + func testMapRequirementBranch() async throws { + // Given + let package = XCRemoteSwiftPackageReference( + repositoryURL: "https://github.com/example/package.git", + versionRequirement: .branch("main") + ) + + // When + let mapped = try mapper.map(package: package) + + // Then + #expect(mapped.requirement == .branch("main")) + } + + @Test("Maps a revision-based version requirement correctly") + func testMapRequirementRevision() async throws { + // Given + let package = XCRemoteSwiftPackageReference( + repositoryURL: "https://github.com/example/package.git", + versionRequirement: .revision("abc123") + ) + + // When + let mapped = try mapper.map(package: package) + + // Then + #expect(mapped.requirement == .revision("abc123")) + } + + @Test("Maps a missing version requirement to up-to-next-major(0.0.0)") + func testMapRequirementNoVersionRequirement() async throws { + // Given + let package = XCRemoteSwiftPackageReference( + repositoryURL: "https://github.com/example/package.git", + versionRequirement: nil + ) + + // When + let mapped = try mapper.map(package: package) + + // Then + #expect(mapped.requirement == .upToNextMajor("0.0.0")) + } + + @Test("Maps a local package reference correctly") + func testMapLocalPackage() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let localPackage = XCLocalSwiftPackageReference(relativePath: "Packages/Example") + + // When + let result = try mapper.map(package: localPackage, sourceDirectory: xcodeProj.srcPath) + + // Then + let expectedPath = xcodeProj.srcPath.appending( + try RelativePath(validating: "Packages/Example") + ) + #expect(result == .local(path: expectedPath)) + } + + @Test("Throws an error if remote package has no repository URL") + func testMapPackageWithoutURL() async throws { + // Given + let package = XCRemoteSwiftPackageReference( + repositoryURL: "", + versionRequirement: .exact("1.2.3") + ) + package.repositoryURL = nil + + // When / Then (expecting a throw) + #expect { + try mapper.map(package: package) + } throws: { error in + // Because 'repositoryURL' was set to nil, + // we verify that the correct error message appears. + return error.localizedDescription == "The repository URL is missing for the package: Unknown Package." + } + } +} + +extension Package { + fileprivate var requirement: Requirement? { + switch self { + case let .remote(_, requirement): + return requirement + case .local: + return nil + } + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXCopyFilesBuildPhaseMapperTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXCopyFilesBuildPhaseMapperTests.swift new file mode 100644 index 00000000..98149258 --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXCopyFilesBuildPhaseMapperTests.swift @@ -0,0 +1,61 @@ +import Testing +import XcodeGraph +import XcodeProj +@testable import XcodeProjMapper + +@Suite +struct PBXCopyFilesBuildPhaseMapperTests { + @Test("Maps copy files actions, verifying code-sign-on-copy attributes") + func testMapCopyFiles() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + + let fileRef = try PBXFileReference.test( + sourceTree: .group, + name: "MyLibrary.dylib", + path: "MyLibrary.dylib" + ) + .add(to: pbxProj) + .addToMainGroup(in: pbxProj) + + let buildFile = PBXBuildFile( + file: fileRef, + settings: ["ATTRIBUTES": ["CodeSignOnCopy"]] + ).add(to: pbxProj) + + let copyFilesPhase = PBXCopyFilesBuildPhase( + dstPath: "Libraries", + dstSubfolderSpec: .frameworks, + name: "Embed Libraries", + files: [buildFile] + ) + .add(to: pbxProj) + + try PBXNativeTarget.test( + name: "App", + buildPhases: [copyFilesPhase], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let mapper = PBXCopyFilesBuildPhaseMapper() + + // When + let copyActions = try mapper.map([copyFilesPhase], xcodeProj: xcodeProj) + + // Then + #expect(copyActions.count == 1) + + let action = try #require(copyActions.first) + #expect(action.name == "Embed Libraries") + #expect(action.destination == .frameworks) + #expect(action.subpath == "Libraries") + #expect(action.files.count == 1) + + let fileAction = try #require(action.files.first) + #expect(fileAction.codeSignOnCopy == true) + #expect(fileAction.path.basename == "MyLibrary.dylib") + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXCoreDataModelsBuildPhaseMapperTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXCoreDataModelsBuildPhaseMapperTests.swift new file mode 100644 index 00000000..f7a2eb5f --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXCoreDataModelsBuildPhaseMapperTests.swift @@ -0,0 +1,57 @@ +import Testing +import XcodeGraph +import XcodeProj +@testable import XcodeProjMapper + +@Suite +struct PBXCoreDataModelsBuildPhaseMapperTests { + @Test("Maps CoreData models from version groups within resources phase") + func testMapCoreDataModels() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + + let versionChildRef = try PBXFileReference.test( + name: "Model.xcdatamodel", + path: "Model.xcdatamodel" + ) + .add(to: pbxProj) + .addToMainGroup(in: pbxProj) + + let versionGroup = try XCVersionGroup.test( + currentVersion: versionChildRef, + children: [versionChildRef], + path: "Model.xcdatamodeld", + sourceTree: .group, + versionGroupType: "wrapper.xcdatamodel", + name: "Model.xcdatamodeld", + pbxProj: pbxProj + ) + .add(to: pbxProj) + .addToMainGroup(in: pbxProj) + versionGroup.currentVersion?.add(to: pbxProj) + + let buildFile = PBXBuildFile(file: versionGroup).add(to: pbxProj) + let resourcesPhase = PBXResourcesBuildPhase(files: [buildFile]).add(to: pbxProj) + + try PBXNativeTarget.test( + name: "App", + buildPhases: [resourcesPhase], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let mapper = PBXCoreDataModelsBuildPhaseMapper() + + // When + let models = try mapper.map([buildFile], xcodeProj: xcodeProj) + + // Then + #expect(models.count == 1) + let model = try #require(models.first) + #expect(model.path.basename == "Model.xcdatamodeld") + #expect(model.versions.count == 1) + #expect(model.currentVersion.contains("Model.xcdatamodel") == true) + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXFrameworksBuildPhaseMapperTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXFrameworksBuildPhaseMapperTests.swift new file mode 100644 index 00000000..a0e3bfab --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXFrameworksBuildPhaseMapperTests.swift @@ -0,0 +1,43 @@ +import Testing +import XcodeGraph +import XcodeProj +@testable import XcodeProjMapper + +@Suite +struct PBXFrameworksBuildPhaseMapperTests { + @Test("Maps frameworks from frameworks phase") + func testMapFrameworks() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + + let frameworkRef = try PBXFileReference( + sourceTree: .group, + name: "MyFramework.framework", + path: "Frameworks/MyFramework.framework" + ) + .add(to: pbxProj) + .addToMainGroup(in: pbxProj) + + let frameworkBuildFile = PBXBuildFile(file: frameworkRef).add(to: pbxProj) + let frameworksPhase = PBXFrameworksBuildPhase(files: [frameworkBuildFile]).add(to: pbxProj) + + try PBXNativeTarget( + name: "App", + buildPhases: [frameworksPhase], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let mapper = PBXFrameworksBuildPhaseMapper() + + // When + let frameworks = try mapper.map(frameworksPhase, xcodeProj: xcodeProj) + + // Then + #expect(frameworks.count == 1) + let dependency = try #require(frameworks.first) + #expect(dependency.name == "MyFramework") + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXHeadersBuildPhaseMapperTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXHeadersBuildPhaseMapperTests.swift new file mode 100644 index 00000000..93d61329 --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXHeadersBuildPhaseMapperTests.swift @@ -0,0 +1,71 @@ +import Testing +import XcodeGraph +import XcodeProj +@testable import XcodeProjMapper + +@Suite +struct PBXHeadersBuildPhaseMapperTests { + @Test("Maps public, private, and project headers from headers phase") + func testMapHeaders() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + + let publicHeaderRef = try PBXFileReference.test( + name: "PublicHeader.h", + path: "Include/PublicHeader.h" + ) + .add(to: pbxProj) + .addToMainGroup(in: pbxProj) + + let publicBuildFile = PBXBuildFile( + file: publicHeaderRef, + settings: ["ATTRIBUTES": ["Public"]] + ).add(to: pbxProj) + + let privateHeaderRef = try PBXFileReference.test( + name: "PrivateHeader.h", + path: "Include/PrivateHeader.h" + ) + .add(to: pbxProj) + .addToMainGroup(in: pbxProj) + + let privateBuildFile = PBXBuildFile( + file: privateHeaderRef, + settings: ["ATTRIBUTES": ["Private"]] + ).add(to: pbxProj) + + let projectHeaderRef = try PBXFileReference.test( + name: "ProjectHeader.h", + path: "Include/ProjectHeader.h" + ) + .add(to: pbxProj) + .addToMainGroup(in: pbxProj) + + let projectBuildFile = PBXBuildFile(file: projectHeaderRef).add(to: pbxProj) + + let headersPhase = PBXHeadersBuildPhase( + files: [publicBuildFile, privateBuildFile, projectBuildFile] + ) + .add(to: pbxProj) + + try PBXNativeTarget( + name: "App", + buildPhases: [headersPhase], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let mapper = PBXHeadersBuildPhaseMapper() + + // When + let headers = try mapper.map(headersPhase, xcodeProj: xcodeProj) + + // Then + try #require(headers != nil) + #expect(headers?.public.map(\.basename).contains("PublicHeader.h") == true) + #expect(headers?.private.map(\.basename).contains("PrivateHeader.h") == true) + #expect(headers?.project.map(\.basename).contains("ProjectHeader.h") == true) + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXResourcesBuildPhaseMapperTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXResourcesBuildPhaseMapperTests.swift new file mode 100644 index 00000000..38faec55 --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXResourcesBuildPhaseMapperTests.swift @@ -0,0 +1,87 @@ +import Testing +import XcodeGraph +import XcodeProj +@testable import XcodeProjMapper + +@Suite +struct PBXResourcesBuildPhaseMapperTests { + @Test("Maps resources (like xcassets) from resources phase") + func testMapResources() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + + let assetRef = try PBXFileReference( + sourceTree: .group, + name: "Assets.xcassets", + path: "Assets.xcassets" + ) + .add(to: pbxProj) + .addToMainGroup(in: pbxProj) + .add(to: pbxProj) + + let buildFile = PBXBuildFile(file: assetRef).add(to: pbxProj) + let resourcesPhase = PBXResourcesBuildPhase(files: [buildFile]).add(to: pbxProj) + + try PBXNativeTarget( + name: "App", + buildPhases: [resourcesPhase], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let mapper = PBXResourcesBuildPhaseMapper() + + // When + let resources = try mapper.map(resourcesPhase, xcodeProj: xcodeProj) + + // Then + #expect(resources.count == 1) + let resource = try #require(resources.first) + switch resource { + case let .file(path, _, _): + #expect(path.basename == "Assets.xcassets") + default: + Issue.record("Expected a file resource.") + } + } + + @Test("Maps localized variant groups from resources") + func testMapVariantGroup() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + + let fileRef1 = PBXFileReference.test( + name: "Localizable.strings", + path: "en.lproj/Localizable.strings" + ).add(to: pbxProj) + let fileRef2 = PBXFileReference.test( + name: "Localizable.strings", + path: "fr.lproj/Localizable.strings" + ).add(to: pbxProj) + + let variantGroup = try PBXVariantGroup.mockVariant( + children: [fileRef1, fileRef2] + ) + .add(to: pbxProj) + .addToMainGroup(in: pbxProj) + + let buildFile = PBXBuildFile(file: variantGroup).add(to: pbxProj) + let resourcesPhase = PBXResourcesBuildPhase(files: [buildFile]).add(to: pbxProj) + + try PBXNativeTarget.test(buildPhases: [resourcesPhase]) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let mapper = PBXResourcesBuildPhaseMapper() + + // When + let resources = try mapper.map(resourcesPhase, xcodeProj: xcodeProj) + + // Then + #expect(resources.count == 2) + #expect(resources.first?.path.basename == "Localizable.strings") + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXScriptsBuildPhaseMapperTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXScriptsBuildPhaseMapperTests.swift new file mode 100644 index 00000000..16869a4d --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXScriptsBuildPhaseMapperTests.swift @@ -0,0 +1,73 @@ +import Testing +import XcodeGraph +import XcodeProj +@testable import XcodeProjMapper + +@Suite +struct PBXScriptsBuildPhaseMapperTests { + @Test("Maps embedded run scripts with specified input/output paths") + func testMapScripts() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + + let scriptPhase = PBXShellScriptBuildPhase.test( + name: "Run Script", + shellScript: "echo Hello", + inputPaths: ["$(SRCROOT)/input.txt"], + outputPaths: ["$(DERIVED_FILE_DIR)/output.txt"] + ) + .add(to: pbxProj) + + try PBXNativeTarget.test( + name: "App", + buildPhases: [scriptPhase], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let mapper = PBXScriptsBuildPhaseMapper() + + // When + let scripts = try mapper.map([scriptPhase], buildPhases: [scriptPhase]) + + // Then + #expect(scripts.count == 1) + let script = try #require(scripts.first) + #expect(script.name == "Run Script") + #expect(script.script == .embedded("echo Hello")) + #expect(script.inputPaths == ["$(SRCROOT)/input.txt"]) + #expect(script.outputPaths == ["$(DERIVED_FILE_DIR)/output.txt"]) + } + + @Test("Maps raw script build phases not covered by other categories") + func testMapRawScriptBuildPhases() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + + let scriptPhase = PBXShellScriptBuildPhase.test( + name: "Test Script" + ) + .add(to: pbxProj) + + try PBXNativeTarget.test( + name: "App", + buildPhases: [scriptPhase], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let mapper = PBXScriptsBuildPhaseMapper() + + // When + let rawPhases = try mapper.map([scriptPhase], buildPhases: [scriptPhase]) + + // Then + #expect(rawPhases.count == 1) + let rawPhase = try #require(rawPhases.first) + #expect(rawPhase.name == "Test Script") + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXSourcesBuildPhaseMapperTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXSourcesBuildPhaseMapperTests.swift new file mode 100644 index 00000000..86e362ec --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Phases/PBXSourcesBuildPhaseMapperTests.swift @@ -0,0 +1,139 @@ +import Testing +import XcodeGraph +import XcodeProj +@testable import XcodeProjMapper + +@Suite +struct PBXSourcesBuildPhaseMapperTests { + @Test("Maps Swift source files with compiler flags from sources phase") + func testMapSources() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + + // Create a file reference for a Swift source and add it to the main group. + let fileRef = try PBXFileReference( + sourceTree: .group, + name: "main.swift", + path: "main.swift" + ) + .add(to: pbxProj) + .addToMainGroup(in: pbxProj) + + // Add a build file with compiler flags. + let buildFile = PBXBuildFile( + file: fileRef, + settings: ["COMPILER_FLAGS": "-DDEBUG"] + ) + .add(to: pbxProj) + + // Create a sources build phase with the file. + let sourcesPhase = PBXSourcesBuildPhase(files: [buildFile]) + .add(to: pbxProj) + + // Add a native target that includes the sources phase. + try PBXNativeTarget( + name: "App", + buildPhases: [sourcesPhase], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let mapper = PBXSourcesBuildPhaseMapper() + + // When + let sources = try mapper.map(sourcesPhase, xcodeProj: xcodeProj) + + // Then + #expect(sources.count == 1) + let sourceFile = try #require(sources.first) + #expect(sourceFile.path.basename == "main.swift") + #expect(sourceFile.compilerFlags == "-DDEBUG") + } + + @Test("Handles source files without file references gracefully") + func testMapSourceFile_missingFileRef() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + + // A build file with no file reference. + let buildFile = PBXBuildFile() + let sourcesPhase = PBXSourcesBuildPhase(files: [buildFile]).add(to: pbxProj) + _ = try PBXNativeTarget.test(buildPhases: [sourcesPhase]) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let mapper = PBXSourcesBuildPhaseMapper() + + // When + let sources = try mapper.map(sourcesPhase, xcodeProj: xcodeProj) + + // Then + #expect(sources.isEmpty == true) // Gracefully handled empty result. + } + + @Test("Gracefully handles non-existent file paths for source files") + func testMapSourceFile_unresolvableFullPath() async throws { + // Given + // Use a provider with an invalid source directory to simulate missing files. + let xcodeProj = try await XcodeProj.test( + projectName: "TestProject" + ) + let pbxProj = xcodeProj.pbxproj + + let fileRef = PBXFileReference( + name: "NonExistent.swift", + path: "NonExistent.swift" + ) + let buildFile = PBXBuildFile(file: fileRef).add(to: pbxProj) + let sourcesPhase = PBXSourcesBuildPhase(files: [buildFile]).add(to: pbxProj) + + try PBXNativeTarget.test(buildPhases: [sourcesPhase]) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let mapper = PBXSourcesBuildPhaseMapper() + + // When + let sources = try mapper.map(sourcesPhase, xcodeProj: xcodeProj) + + // Then + #expect(sources.isEmpty == true) + } + + @Test( + "Correctly identifies code generation attributes for source files", + arguments: [FileCodeGen.public, .private, .project, .disabled] + ) + func testCodeGenAttributes(_ fileCodeGen: FileCodeGen) async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + + let fileRef = try PBXFileReference.test(name: "File.swift", path: "File.swift") + .add(to: pbxProj) + .addToMainGroup(in: pbxProj) + + let buildFile = PBXBuildFile( + file: fileRef, + settings: ["ATTRIBUTES": [fileCodeGen.rawValue]] + ).add(to: pbxProj) + + let sourcesPhase = PBXSourcesBuildPhase(files: [buildFile]).add(to: pbxProj) + try PBXNativeTarget.test(buildPhases: [sourcesPhase]) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let mapper = PBXSourcesBuildPhaseMapper() + + // When + let sources = try mapper.map(sourcesPhase, xcodeProj: xcodeProj) + + // Then + #expect(sources.count == 1) + let sourceFile = try #require(sources.first) + #expect(sourceFile.codeGen == fileCodeGen) + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Project/PBXProjectMapperTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Project/PBXProjectMapperTests.swift new file mode 100644 index 00000000..83d4a67d --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Project/PBXProjectMapperTests.swift @@ -0,0 +1,154 @@ +import Path +import Testing +import XcodeGraph +import XcodeProj +@testable import XcodeProjMapper + +@Suite +struct PBXProjectMapperTests { + @Test("Maps a basic project with default attributes") + func testMapBasicProject() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let mapper = PBXProjectMapper() + + // When + let project = try await mapper.map(xcodeProj: xcodeProj) + + // Then + #expect(project.name == "TestProject") + #expect(project.path == xcodeProj.srcPath) + #expect(project.sourceRootPath == xcodeProj.srcPath) + #expect(project.xcodeProjPath == xcodeProj.projectPath) + #expect(project.type == .local) + } + + @Test("Maps a project with custom attributes (org name, class prefix, upgrade check)") + func testMapProjectWithCustomAttributes() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + + let mapper = PBXProjectMapper() + + let customAttributes: [String: Any] = [ + "ORGANIZATIONNAME": "Example Org", + "CLASSPREFIX": "EX", + "LastUpgradeCheck": "1500", + ] + xcodeProj.pbxproj.projects.first?.attributes = customAttributes + + // When + let project = try await mapper.map(xcodeProj: xcodeProj) + + // Then + #expect(project.name == "TestProject") + #expect(project.organizationName == "Example Org") + #expect(project.classPrefix == "EX") + #expect(project.lastUpgradeCheck == "1500") + } + + @Test("Maps a project with remote package dependencies") + func testMapProjectWithRemotePackages() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + + let package = XCRemoteSwiftPackageReference( + repositoryURL: "https://github.com/example/package.git", + versionRequirement: .upToNextMajorVersion("1.0.0") + ) + pbxProj.add(object: package) + try xcodeProj.mainPBXProject().remotePackages.append(package) + let mapper = PBXProjectMapper() + + // When + let project = try await mapper.map(xcodeProj: xcodeProj) + + // Then + #expect(project.packages.count == 1) + guard case let .remote(url, requirement) = project.packages[0] else { + Issue.record("Expected remote package") + return + } + #expect(url == "https://github.com/example/package.git") + #expect(requirement == .upToNextMajor("1.0.0")) + } + + @Test("Maps a project with known regions") + func testMapProjectWithKnownRegions() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + try xcodeProj.mainPBXProject().knownRegions = ["en", "es", "fr"] + let mapper = PBXProjectMapper() + + // When + let project = try await mapper.map(xcodeProj: xcodeProj) + + // Then + #expect(project.defaultKnownRegions?.count == 3) + #expect(project.defaultKnownRegions?.contains("en") == true) + #expect(project.defaultKnownRegions?.contains("es") == true) + #expect(project.defaultKnownRegions?.contains("fr") == true) + } + + @Test("Maps a project with a custom development region") + func testMapProjectWithDevelopmentRegion() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + try xcodeProj.mainPBXProject().developmentRegion = "fr" + let mapper = PBXProjectMapper() + + // When + let project = try await mapper.map(xcodeProj: xcodeProj) + + // Then + #expect(project.developmentRegion == "fr") + } + + @Test("Maps a project with default resource synthesizers") + func testMapProjectWithResourceSynthesizers() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let mapper = PBXProjectMapper() + + // When + let project = try await mapper.map(xcodeProj: xcodeProj) + let synthesizers = project.resourceSynthesizers + + // Then + // Check for strings synthesizer + let stringsSynthesizer = synthesizers.first { $0.parser == .strings } + #expect(stringsSynthesizer != nil) + #expect(stringsSynthesizer?.extensions.contains("strings") == true) + #expect(stringsSynthesizer?.extensions.contains("stringsdict") == true) + + // Check for assets synthesizer + let assetsSynthesizer = synthesizers.first { $0.parser == .assets } + #expect(assetsSynthesizer != nil) + #expect(assetsSynthesizer?.extensions.contains("xcassets") == true) + + // Verify all expected synthesizer types are present + let expectedParsers: Set = [ + .strings, .assets, .plists, .fonts, .coreData, + .interfaceBuilder, .json, .yaml, .files, .stringsCatalog, + ] + let actualParsers = Set(synthesizers.map(\.parser)) + #expect(actualParsers == expectedParsers) + } + + @Test("Maps a project with associated schemes") + func testMapProjectWithSchemes() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let scheme = XCScheme.test(name: "TestScheme") + xcodeProj.sharedData = XCSharedData(schemes: [scheme]) + let mapper = PBXProjectMapper() + + // When + let project = try await mapper.map(xcodeProj: xcodeProj) + + // Then + #expect(project.schemes.count == 1) + #expect(project.schemes[0].name == "TestScheme") + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Schemes/XCSchemeMapperTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Schemes/XCSchemeMapperTests.swift new file mode 100644 index 00000000..f6a2b15d --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Schemes/XCSchemeMapperTests.swift @@ -0,0 +1,280 @@ +import AEXML +import Path +import Testing +import XcodeGraph +@testable import XcodeProj +@testable import XcodeProjMapper + +@Suite +struct XCSchemeMapperTests { + let xcodeProj: XcodeProj + let mapper: XCSchemeMapper + let graphType: XcodeMapperGraphType + + init() async throws { + xcodeProj = try await XcodeProj.test() + mapper = XCSchemeMapper() + graphType = .project(xcodeProj) + } + + @Test("Maps shared project schemes correctly") + func testMapSharedProjectSchemes() async throws { + // Given + let xcscheme = XCScheme.test(name: "SharedScheme") + + // When + let scheme = try mapper.map(xcscheme, shared: true, graphType: graphType) + + // Then + #expect(scheme.name == "SharedScheme") + #expect(scheme.shared == true) + } + + @Test("Maps user (non-shared) project schemes correctly") + func testMapUserSchemes() async throws { + // Given + let xcscheme = XCScheme.test(name: "UserScheme") + + // When + let scheme = try mapper.map(xcscheme, shared: false, graphType: graphType) + + // Then + #expect(scheme.name == "UserScheme") + #expect(scheme.shared == false) + } + + @Test("Maps a build action within a scheme") + func testMapBuildAction() async throws { + // Given + let targetRef = XCScheme.BuildableReference( + referencedContainer: "container:App.xcodeproj", + blueprintIdentifier: "123", + buildableName: "App.app", + blueprintName: "App" + ) + let buildActionEntry = XCScheme.BuildAction.Entry( + buildableReference: targetRef, + buildFor: [.running, .testing] + ) + let buildAction = XCScheme.BuildAction( + buildActionEntries: [buildActionEntry], + parallelizeBuild: true, + buildImplicitDependencies: true, + runPostActionsOnFailure: true + ) + let xcscheme = XCScheme.test(name: "UserScheme", buildAction: buildAction) + + // When + let mapped = try mapper.map(xcscheme, shared: false, graphType: graphType) + let mappedAction = mapped.buildAction + // Then + #expect(mappedAction != nil) + #expect(mappedAction?.targets.count == 1) + #expect(mappedAction?.targets[0].name == "App") + #expect(mappedAction?.runPostActionsOnFailure == true) + #expect(mappedAction?.findImplicitDependencies == true) + } + + @Test("Maps a test action with testable references, coverage, and environment") + func testMapTestAction() async throws { + // Given + let targetRef = XCScheme.BuildableReference( + referencedContainer: "container:App.xcodeproj", + blueprintIdentifier: "123", + buildableName: "AppTests.xctest", + blueprintName: "AppTests" + ) + let testableEntry = XCScheme.TestableReference.test( + skipped: false, + buildableReference: targetRef + ) + let envVar = XCScheme.EnvironmentVariable( + variable: "TEST_ENV", + value: "test_value", + enabled: true + ) + let launchArg = XCScheme.CommandLineArguments.CommandLineArgument( + name: "test_arg", + enabled: true + ) + let testAction = XCScheme.TestAction( + buildConfiguration: "Debug", + macroExpansion: nil, + testables: [testableEntry], + codeCoverageEnabled: true, + commandlineArguments: XCScheme.CommandLineArguments(arguments: [launchArg]), + environmentVariables: [envVar], + language: "en", + region: "US" + ) + let xcscheme = XCScheme.test(name: "UserScheme", testAction: testAction) + + // When + let mapped = try mapper.map(xcscheme, shared: false, graphType: graphType) + let mappedAction = mapped.testAction + + // Then + #expect(mappedAction != nil) + #expect(mappedAction?.targets.count == 1) + #expect(mappedAction?.targets[0].target.name == "AppTests") + #expect(mappedAction?.configurationName == "Debug") + #expect(mappedAction?.coverage == true) + #expect(mappedAction?.arguments?.environmentVariables["TEST_ENV"]?.value == "test_value") + #expect(mappedAction?.arguments?.launchArguments.first?.name == "test_arg") + #expect(mappedAction?.language == "en") + #expect(mappedAction?.region == "US") + } + + @Test("Maps a run action with environment variables and launch arguments") + func testMapRunAction() async throws { + // Given + let targetRef = XCScheme.BuildableReference( + referencedContainer: "container:App.xcodeproj", + blueprintIdentifier: "123", + buildableName: "App.app", + blueprintName: "App" + ) + let runnable = XCScheme.BuildableProductRunnable(buildableReference: targetRef) + let envVar = XCScheme.EnvironmentVariable(variable: "RUN_ENV", value: "run_value", enabled: true) + let launchArg = XCScheme.CommandLineArguments.CommandLineArgument(name: "run_arg", enabled: true) + let element = runnable.xmlElement() + let launchAction = XCScheme.LaunchAction( + runnable: try .init(element: element), + buildConfiguration: "Debug", + selectedDebuggerIdentifier: "", + commandlineArguments: XCScheme.CommandLineArguments(arguments: [launchArg]), + environmentVariables: [envVar] + ) + + let xcscheme = XCScheme.test(name: "UserScheme", launchAction: launchAction) + + // When + let mapped = try mapper.map(xcscheme, shared: false, graphType: graphType) + let mappedAction = mapped.runAction + + // Then + #expect(mappedAction != nil) + #expect(mappedAction?.executable?.name == "App") + #expect(mappedAction?.configurationName == "Debug") + #expect(mappedAction?.attachDebugger == true) + #expect(mappedAction?.arguments?.environmentVariables["RUN_ENV"]?.value == "run_value") + #expect(mappedAction?.arguments?.launchArguments.first?.name == "run_arg") + } + + @Test("Maps an archive action with organizer reveal enabled") + func testMapArchiveAction() async throws { + // Given + let archiveAction = XCScheme.ArchiveAction( + buildConfiguration: "Release", + revealArchiveInOrganizer: true + ) + + let xcscheme = XCScheme.test(name: "UserScheme", archiveAction: archiveAction) + + // When + let mapped = try mapper.map(xcscheme, shared: false, graphType: graphType) + let mappedAction = mapped.archiveAction + + // Then + #expect(mappedAction != nil) + #expect(mappedAction?.configurationName == "Release") + #expect(mappedAction?.revealArchiveInOrganizer == true) + } + + @Test("Maps a profile action to a runnable and configuration") + func testMapProfileAction() async throws { + // Given + let targetRef = XCScheme.BuildableReference( + referencedContainer: "container:App.xcodeproj", + blueprintIdentifier: "123", + buildableName: "App.app", + blueprintName: "App" + ) + let runnable = XCScheme.BuildableProductRunnable(buildableReference: targetRef) + let profileAction = XCScheme.ProfileAction( + runnable: runnable, + buildConfiguration: "Release" + ) + + let xcscheme = XCScheme.test(name: "UserScheme", profileAction: profileAction) + + // When + let mapped = try mapper.map(xcscheme, shared: false, graphType: graphType) + let mappedAction = mapped.profileAction + + // Then + #expect(mappedAction != nil) + #expect(mappedAction?.executable?.name == "App") + #expect(mappedAction?.configurationName == "Release") + } + + @Test("Maps an analyze action to the appropriate configuration") + func testMapAnalyzeAction() async throws { + // Given + let analyzeAction = XCScheme.AnalyzeAction(buildConfiguration: "Debug") + + let xcscheme = XCScheme.test(name: "UserScheme", analyzeAction: analyzeAction) + + // When + let mapped = try mapper.map(xcscheme, shared: false, graphType: graphType) + let mappedAction = mapped.analyzeAction + + // Then + #expect(mappedAction != nil) + #expect(mappedAction?.configurationName == "Debug") + } + + @Test("Maps target references in a scheme's build action") + func testMapTargetReference() async throws { + // Given + let targetRef = XCScheme.BuildableReference( + referencedContainer: "container:App.xcodeproj", + blueprintIdentifier: "123", + buildableName: "App.app", + blueprintName: "App" + ) + let buildActionEntry = XCScheme.BuildAction.Entry( + buildableReference: targetRef, + buildFor: [.running] + ) + let buildAction = XCScheme.BuildAction( + buildActionEntries: [buildActionEntry], + parallelizeBuild: true, + buildImplicitDependencies: true + ) + let xcscheme = XCScheme.test(name: "UserScheme", buildAction: buildAction) + + // When + let mapped = try mapper.map(xcscheme, shared: false, graphType: graphType) + let mappedAction = try #require(mapped.buildAction) + + // Then + #expect(mappedAction.targets.count == 1) + #expect(mappedAction.targets[0].name == "App") + #expect(mappedAction.targets[0].projectPath == xcodeProj.projectPath) + } + + @Test("Handles schemes without any actions gracefully") + func testNilActions() async throws { + // Given + let scheme = XCScheme.test( + buildAction: nil, + testAction: nil, + launchAction: nil, + archiveAction: nil, + profileAction: nil, + analyzeAction: nil + ) + + // When + let mapped = try mapper.map(scheme, shared: true, graphType: graphType) + + // Then + #expect(mapped.buildAction == nil) + #expect(mapped.testAction == nil) + #expect(mapped.runAction == nil) + #expect(mapped.profileAction == nil) + #expect(mapped.analyzeAction == nil) + #expect(mapped.archiveAction == nil) + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Settings/BuildSettingsTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Settings/BuildSettingsTests.swift new file mode 100644 index 00000000..bb4d1d00 --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Settings/BuildSettingsTests.swift @@ -0,0 +1,104 @@ +import Path +import Testing +import XcodeGraph +import XcodeProj +@testable import XcodeProjMapper + +@Suite +struct BuildSettingsTests { + @Test("Extracts a string value from build settings") + func testStringExtraction() async throws { + // Given + let settings: [String: Any] = ["COMPILER_FLAGS": "-ObjC"] + + // When + let value = settings.string(for: .compilerFlags) + + // Then + try #require(value != nil) + #expect(value == "-ObjC") + } + + @Test("Extracts a boolean value from build settings and returns nil for invalid types") + func testBoolExtraction() { + // Given + let settings: [String: Any] = ["PRUNE": true] + let invalidSettings: [String: Any] = ["PRUNE": "notABool"] + + // When + let boolValue = settings.bool(for: .prune) + let invalidBool = invalidSettings.bool(for: .prune) + + // Then + #expect(boolValue == true) + #expect(invalidBool == nil) + } + + @Test("Extracts a string array from build settings") + func testStringArrayExtraction() async throws { + // Given + let settings: [String: Any] = ["LAUNCH_ARGUMENTS": ["-enableFeature", "-verbose"]] + + // When + let args = settings.stringArray(for: .launchArguments) + + // Then + try #require(args != nil) + #expect(args?.count == 2) + #expect(args?.contains("-verbose") == true) + } + + @Test("Extracts a dictionary of strings (e.g., environment variables) from build settings") + func testStringDictExtraction() async throws { + // Given + let settings: [String: Any] = ["ENVIRONMENT_VARIABLES": ["KEY": "VALUE"]] + + // When + let envVars = settings.stringDict(for: .environmentVariables) + + // Then + try #require(envVars != nil) + #expect(envVars?["KEY"] == "VALUE") + } + + @Test("Returns nil when keys are missing in build settings") + func testMissingKeyReturnsNil() { + // Given + let settings: [String: Any] = ["TAGS": "some,tags"] + + // When / Then + // No key for productBundleIdentifier or mergeable, so returns nil + #expect(settings.string(for: .productBundleIdentifier) == nil) + #expect(settings.bool(for: .mergeable) == nil) + } + + @Test("Coerces any array elements to strings, discarding non-string values") + func testCoerceAnyArrayToStringArray() async throws { + // Given + let settings: [String: Any] = ["LAUNCH_ARGUMENTS": ["-flag", 42, true]] + + // When + let args = settings.stringArray(for: .launchArguments) + + // Then + try #require(args != nil) + // Non-string elements are discarded, leaving only ["-flag"] + #expect(args == ["-flag"]) + } + + @Test( + "Extracts the SDKROOT build setting as a string", + arguments: Platform.allCases + ) + func testExtractSDKROOT(platform: Platform) throws { + // Given + let settings: [String: Any] = ["SDKROOT": platform.xcodeSdkRoot] + + // When + let sdkroot = settings.string(for: .sdkroot) + + // Then + try #require(sdkroot != nil) + #expect(sdkroot == platform.xcodeSdkRoot) + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Settings/ConfigurationMatcherTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Settings/ConfigurationMatcherTests.swift new file mode 100644 index 00000000..431f4289 --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Settings/ConfigurationMatcherTests.swift @@ -0,0 +1,78 @@ +import Testing +import XcodeGraph +@testable import XcodeProjMapper + +@Suite +struct ConfigurationMatcherTests { + let configurationMatcher: ConfigurationMatching + + init(configurationMatcher: ConfigurationMatching = ConfigurationMatcher()) { + self.configurationMatcher = configurationMatcher + } + + @Test("Detects 'Debug' variants from configuration names") + func testVariantDetectionForDebug() async throws { + // Given + // The configurationMatcher is already set up by the initializer. + + // When + let variantDebug = configurationMatcher.variant(for: "Debug") + let variantDevelopment = configurationMatcher.variant(for: "development") + let variantDev = configurationMatcher.variant(for: "dev") + + // Then + #expect(variantDebug == .debug) + #expect(variantDevelopment == .debug) + #expect(variantDev == .debug) + } + + @Test("Detects 'Release' variants from configuration names") + func testVariantDetectionForRelease() async throws { + // Given + // The configurationMatcher is already set up by the initializer. + + // When + let variantRelease = configurationMatcher.variant(for: "Release") + let variantProd = configurationMatcher.variant(for: "prod") + let variantProduction = configurationMatcher.variant(for: "production") + + // Then + #expect(variantRelease == .release) + #expect(variantProd == .release) + #expect(variantProduction == .release) + } + + @Test("Falls back to 'Debug' variant for unrecognized configuration names") + func testVariantFallbackToDebug() async throws { + // Given + // The configurationMatcher is already set up by the initializer. + + // When + let variantStaging = configurationMatcher.variant(for: "Staging") + let variantCustom = configurationMatcher.variant(for: "CustomConfig") + + // Then + #expect(variantStaging == .debug) + #expect(variantCustom == .debug) + } + + @Test("Validates configuration names based on allowed patterns") + func testValidateConfigurationName() async throws { + // Given + // The configurationMatcher is already set up by the initializer. + + // When + let validDebug = configurationMatcher.validateConfigurationName("Debug") + let validRelease = configurationMatcher.validateConfigurationName("Release") + let invalidEmpty = configurationMatcher.validateConfigurationName("") + let invalidSpaceInName = configurationMatcher.validateConfigurationName("Debug Config") + let invalidSpaceOnly = configurationMatcher.validateConfigurationName(" ") + + // Then + #expect(validDebug == true) + #expect(validRelease == true) + #expect(invalidEmpty == false) + #expect(invalidSpaceInName == false) + #expect(invalidSpaceOnly == false) + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Settings/XCConfigurationMapperTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Settings/XCConfigurationMapperTests.swift new file mode 100644 index 00000000..8466ad1a --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Settings/XCConfigurationMapperTests.swift @@ -0,0 +1,152 @@ +import Path +import Testing +import XcodeGraph +import XcodeProj +@testable import XcodeProjMapper + +@Suite +struct XCConfigurationMapperTests { + let mapper = XCConfigurationMapper() + + @Test("Returns default settings when configuration list is nil") + func testNilConfigurationListReturnsDefault() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + + // When + let settings = try mapper.map(xcodeProj: xcodeProj, configurationList: nil) + + // Then + #expect(settings == Settings.default) + } + + @Test("Maps a single build configuration correctly") + func testSingleConfigurationMapping() async throws { + // Given + let pbxProj = PBXProj() + let config: XCBuildConfiguration = .testDebug().add(to: pbxProj) + let configList = XCConfigurationList.test( + buildConfigurations: [config], + defaultConfigurationName: "Debug" + ).add(to: pbxProj) + let xcodeProj = try await XcodeProj.test(configurationList: configList, pbxProj: pbxProj) + + // When + let settings = try mapper.map(xcodeProj: xcodeProj, configurationList: configList) + + // Then + #expect(settings.configurations.count == 1) + + let configKey = settings.configurations.keys.first + try #require(configKey != nil) + #expect(configKey?.name == "Debug") + #expect(configKey?.variant == .debug) + + let debugConfig = try #require(settings.configurations[configKey!]) + #expect(debugConfig?.settings["PRODUCT_BUNDLE_IDENTIFIER"] == "com.example.debug") + } + + @Test("Maps multiple build configurations correctly") + func testMultipleConfigurations() async throws { + // Given + let pbxProj = PBXProj() + let debugConfiguration: XCBuildConfiguration = .testDebug().add(to: pbxProj) + let releaseConfiguration: XCBuildConfiguration = .testRelease().add(to: pbxProj) + let configs = [debugConfiguration, releaseConfiguration] + let configList = XCConfigurationList.test(buildConfigurations: configs).add(to: pbxProj) + let xcodeProj = try await XcodeProj.test(configurationList: configList, pbxProj: pbxProj) + + // When + let settings = try mapper.map(xcodeProj: xcodeProj, configurationList: configList) + + // Then + #expect(settings.configurations.count == 2) + + let debugKey = try #require(settings.configurations.keys.first { $0.name == "Debug" }) + let releaseKey = try #require(settings.configurations.keys.first { $0.name == "Release" }) + + #expect(debugKey.variant == .debug) + #expect(releaseKey.variant == .release) + + let debugConfig = try #require(settings.configurations[debugKey]) + let releaseConfig = try #require(settings.configurations[releaseKey]) + + #expect(debugConfig?.settings["PRODUCT_BUNDLE_IDENTIFIER"] == "com.example.debug") + #expect(releaseConfig?.settings["PRODUCT_BUNDLE_IDENTIFIER"] == "com.example.release") + } + + @Test("Coerces non-string values to strings in build settings") + func testCoercionOfNonStringValues() async throws { + // Given + let pbxProj = PBXProj() + let config: XCBuildConfiguration = .testDebug( + buildSettings: ["SOME_NUMBER": 42, "A_BOOL": true] + ).add(to: pbxProj) + let configList = XCConfigurationList.test(buildConfigurations: [config]).add(to: pbxProj) + let xcodeProj = try await XcodeProj.test(configurationList: configList) + + // When + let settings = try mapper.map(xcodeProj: xcodeProj, configurationList: configList) + + // Then + let debugKey = try #require(settings.configurations.keys.first { $0.name == "Debug" }) + let debugConfig = try #require(settings.configurations[debugKey]) + + #expect(debugConfig?.settings["SOME_NUMBER"] == "42") + } + + @Test("Resolves XCConfig file paths correctly") + func testXCConfigPathResolution() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + let baseConfigRef = try PBXFileReference.test( + sourceTree: .sourceRoot, + path: "Config.xcconfig" + ).add(to: pbxProj) + .addToMainGroup(in: pbxProj) + + let buildConfig = XCBuildConfiguration.testDebug( + buildSettings: ["PRODUCT_BUNDLE_IDENTIFIER": "com.example"] + ).add(to: pbxProj) + buildConfig.baseConfiguration = baseConfigRef + + let configList = XCConfigurationList( + buildConfigurations: [buildConfig], + defaultConfigurationName: "Debug", + defaultConfigurationIsVisible: false + ) + + // When + let settings = try mapper.map(xcodeProj: xcodeProj, configurationList: configList) + + // Then + let debugKey = try #require(settings.configurations.keys.first { $0.name == "Debug" }) + let debugConfig = try #require(settings.configurations[debugKey]) + + let expectedPath = "\(xcodeProj.srcPathString)/Config.xcconfig" + #expect(debugConfig?.xcconfig?.pathString == expectedPath) + } + + @Test("Maps array values correctly in build settings") + func testArrayValueMapping() async throws { + // Given + let pbxProj = PBXProj() + let config: XCBuildConfiguration = .testDebug( + buildSettings: ["SOME_ARRAY": ["val1", "val2"]] + ).add(to: pbxProj) + let configList = XCConfigurationList.test(buildConfigurations: [config]).add(to: pbxProj) + let xcodeProj = try await XcodeProj.test(configurationList: configList, pbxProj: pbxProj) + + // When + let settings = try mapper.map(xcodeProj: xcodeProj, configurationList: configList) + + // Then + #expect(settings.configurations.count == 1) + + let debugKey = try #require(settings.configurations.keys.first { $0.name == "Debug" }) + let debugConfig = try #require(settings.configurations[debugKey]) + + #expect(debugConfig?.settings["SOME_ARRAY"] == ["val1", "val2"]) + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Target/PBXBuildRuleMapperTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Target/PBXBuildRuleMapperTests.swift new file mode 100644 index 00000000..963d4658 --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Target/PBXBuildRuleMapperTests.swift @@ -0,0 +1,147 @@ +import Testing +import XcodeGraph +import XcodeProj +@testable import XcodeProjMapper + +@Suite +struct PBXBuildRuleMapperTests { + let mapper = PBXBuildRuleMapper() + + @Test("Maps build rules with known compiler spec and file type successfully") + func testMapBuildRulesWithKnownCompilerSpecAndFileType() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + let knownCompilerSpec = BuildRule.CompilerSpec.appleClang.rawValue + let knownFileType = BuildRule.FileType.cSource.rawValue + + let buildRule = PBXBuildRule.test( + compilerSpec: knownCompilerSpec, + fileType: knownFileType, + filePatterns: "*.c", + name: "C Rule", + outputFiles: ["$(DERIVED_FILE_DIR)/output.c.o"], + inputFiles: ["$(SRCROOT)/main.c"], + outputFilesCompilerFlags: ["-O2"], + script: "echo Building C sources", + runOncePerArchitecture: false + ).add(to: pbxProj) + + try PBXNativeTarget.test(buildRules: [buildRule]) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + // When + let optionalRule = try mapper.map(buildRule) + + // Then + let rule = try #require(optionalRule) + #expect(rule.compilerSpec.rawValue == knownCompilerSpec) + #expect(rule.fileType.rawValue == knownFileType) + #expect(rule.filePatterns == "*.c") + #expect(rule.name == "C Rule") + #expect(rule.outputFiles == ["$(DERIVED_FILE_DIR)/output.c.o"]) + #expect(rule.inputFiles == ["$(SRCROOT)/main.c"]) + #expect(rule.outputFilesCompilerFlags == ["-O2"]) + #expect(rule.script == "echo Building C sources") + #expect(rule.runOncePerArchitecture == false) + } + + @Test("Skips build rules when compiler spec is unknown") + func testMapBuildRulesWithUnknownCompilerSpec() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + let unknownCompilerSpec = "com.apple.compilers.unknown" + let knownFileType = "sourcecode.c.c" + + let buildRule = PBXBuildRule.test( + compilerSpec: unknownCompilerSpec, + fileType: knownFileType + ).add(to: pbxProj) + + try PBXNativeTarget.test(buildRules: [buildRule]) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + // When / Then + #expect(throws: PBXBuildRuleMappingError.unknownCompilerSpec("com.apple.compilers.unknown")) { + try mapper.map(buildRule) + } + } + + @Test("Skips build rules when file type is unknown") + func testMapBuildRulesWithUnknownFileType() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + let knownCompilerSpec = BuildRule.CompilerSpec.appleClang.rawValue + let unknownFileType = "sourcecode.unknown" + + let buildRule = PBXBuildRule.test( + compilerSpec: knownCompilerSpec, + fileType: unknownFileType + ).add(to: pbxProj) + + try PBXNativeTarget.test(buildRules: [buildRule]) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + // When / Then + #expect(throws: PBXBuildRuleMappingError.unknownFileType("sourcecode.unknown")) { + try mapper.map(buildRule) + } + } + + @Test("Individually handles valid and invalid rules, returning nil for invalid ones") + func testMapIndividualValidAndInvalidRules() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + let knownCompilerSpec = BuildRule.CompilerSpec.appleClang.rawValue + let knownFileType = BuildRule.FileType.cSource.rawValue + let unknownCompilerSpec = "com.apple.compilers.unknown" + let unknownFileType = "sourcecode.unknown" + + let validRule = PBXBuildRule.test( + compilerSpec: knownCompilerSpec, + fileType: knownFileType, + name: "Valid Rule" + ).add(to: pbxProj) + + let invalidRuleUnknownCompiler = PBXBuildRule.test( + compilerSpec: unknownCompilerSpec, + fileType: knownFileType, + name: "Invalid Compiler" + ).add(to: pbxProj) + + let invalidRuleUnknownFileType = PBXBuildRule.test( + compilerSpec: knownCompilerSpec, + fileType: unknownFileType, + name: "Invalid FileType" + ).add(to: pbxProj) + + try PBXNativeTarget.test( + buildRules: [validRule, invalidRuleUnknownCompiler, invalidRuleUnknownFileType] + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + // When / Then + // Valid rule + let validResult = try mapper.map(validRule) + #expect(validResult?.name == "Valid Rule") + + // When / Then + // Invalid (unknown compiler) + #expect(throws: PBXBuildRuleMappingError.unknownCompilerSpec("com.apple.compilers.unknown")) { + try mapper.map(invalidRuleUnknownCompiler) + } + + // When / Then + // Invalid (unknown file type) + #expect(throws: PBXBuildRuleMappingError.unknownFileType("sourcecode.unknown")) { + try mapper.map(invalidRuleUnknownFileType) + } + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Target/PBXTargetDependencyMapper.swift b/Tests/XcodeProjMapperTests/MapperTests/Target/PBXTargetDependencyMapper.swift new file mode 100644 index 00000000..341dc80d --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Target/PBXTargetDependencyMapper.swift @@ -0,0 +1,354 @@ +import Path +import Testing +import XcodeGraph +import XcodeProj +@testable import XcodeProjMapper + +@Suite +struct DependencyMapperTests { + let mapper: PBXTargetDependencyMapping + + init() { + mapper = PBXTargetDependencyMapper() + } + + @Test("Maps direct target dependencies correctly") + func testDirectTargetMapping() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + let target = try PBXNativeTarget.test( + name: "DirectTarget", + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let dep = PBXTargetDependency( + name: "DirectTarget", + target: target + ).add(to: pbxProj) + + try PBXNativeTarget.test( + name: "App", + dependencies: [dep], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + // When + let mapped = try mapper.map(dep, xcodeProj: xcodeProj) + + // Then + #expect(mapped == .target(name: "DirectTarget", status: .required, condition: nil)) + } + + @Test("Maps package product dependencies to runtime package targets") + func testPackageProductMapping() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let pbxProj = xcodeProj.pbxproj + let productRef = XCSwiftPackageProductDependency(productName: "MyPackageProduct") + let dep = PBXTargetDependency(name: nil, product: productRef).add(to: pbxProj) + + try PBXNativeTarget.test( + name: "App", + dependencies: [dep], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + // When + let mapped = try mapper.map(dep, xcodeProj: xcodeProj) + + // Then + #expect(mapped == .package(product: "MyPackageProduct", type: .runtime, condition: nil)) + } + + @Test("Maps native target proxies referencing targets in the same project") + func testProxyNativeTarget() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + + let pbxProj = xcodeProj.pbxproj + let project = try #require(pbxProj.rootObject) + let proxy = PBXContainerItemProxy( + containerPortal: .project(project), + remoteGlobalID: .string("GLOBAL_ID"), + proxyType: .nativeTarget, + remoteInfo: "NativeTarget" + ) + .add(to: pbxProj) + + let dep = PBXTargetDependency(name: nil, target: nil, targetProxy: proxy).add(to: pbxProj) + + try PBXNativeTarget.test( + name: "App", + dependencies: [dep], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + // When + let mapped = try mapper.map(dep, xcodeProj: xcodeProj) + + // Then + #expect(mapped == .target(name: "NativeTarget", status: .required, condition: nil)) + } + + @Test("Maps proxy dependencies referencing other projects via file references") + func testProxyProjectReference() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + + let pbxProj = xcodeProj.pbxproj + let fileRef = try PBXFileReference.test(path: "TestProject.xcodeproj") + .add(to: pbxProj) + .addToMainGroup(in: pbxProj) + + let proxy = PBXContainerItemProxy( + containerPortal: .fileReference(fileRef), + remoteGlobalID: .string("GLOBAL_ID"), + proxyType: .nativeTarget, + remoteInfo: "OtherTarget" + ).add(to: pbxProj) + + let dep = PBXTargetDependency(name: nil, target: nil, targetProxy: proxy).add(to: pbxProj) + + try PBXNativeTarget.test( + name: "App", + dependencies: [dep], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + // When + let mapped = try mapper.map(dep, xcodeProj: xcodeProj) + + // Then + let result = try #require(mapped) + let expectedPath = xcodeProj.projectPath + #expect(result == .project(target: "OtherTarget", path: expectedPath, status: .required, condition: nil)) + } + + @Test("Maps reference proxies to libraries when file type is a dylib") + func testProxyReferenceProxyLibrary() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + + let pbxProj = xcodeProj.pbxproj + let referenceProxy = try PBXReferenceProxy( + fileType: "compiled.mach-o.dylib", + path: "libTest.dylib", + remote: nil, + sourceTree: .group + ) + .add(to: pbxProj) + .addToMainGroup(in: pbxProj) + let mainGroup = try #require(pbxProj.projects.first?.mainGroup) + let projectRef = PBXProject.test( + name: "RemoteProject", + buildConfigurationList: .test(), + mainGroup: mainGroup + ).add(to: pbxProj) + + let proxy = PBXContainerItemProxy( + containerPortal: .project(projectRef), + remoteGlobalID: .string("GLOBAL_ID"), + proxyType: .reference, + remoteInfo: "SomeRemoteInfo" + ) + proxy.remoteGlobalID = .object(referenceProxy) + + let dep = PBXTargetDependency(name: nil, target: nil, targetProxy: proxy).add(to: pbxProj) + + try PBXNativeTarget.test( + name: "App", + dependencies: [dep], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + // When + let mapped = try mapper.map(dep, xcodeProj: xcodeProj) + + // Then + let result = try #require(mapped) + let expectedPath = xcodeProj.srcPath.appending(component: "libTest.dylib") + let publicHeaders = xcodeProj.srcPath + #expect( + result == .library( + path: expectedPath, + publicHeaders: publicHeaders, + swiftModuleMap: nil, + condition: nil + ) + ) + } + + @Test("Maps frameworks when encountered as proxy references") + func testProxyReferenceFileFramework() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + + let pbxProj = xcodeProj.pbxproj + let fileRef = try PBXFileReference.test(path: "MyLib.framework") + .add(to: pbxProj) + .addToMainGroup(in: pbxProj) + let mainGroup = try #require(pbxProj.projects.first?.mainGroup) + + let projectRef = PBXProject.test( + name: "RemoteProject", + buildConfigurationList: .test(), + mainGroup: mainGroup + ).add(to: pbxProj) + + let proxy = PBXContainerItemProxy( + containerPortal: .project(projectRef), + remoteGlobalID: .object(fileRef), + proxyType: .reference, + remoteInfo: "SomeFramework" + ) + pbxProj.add(object: proxy) + + let dep = PBXTargetDependency(name: nil, target: nil, targetProxy: proxy).add(to: pbxProj) + + try PBXNativeTarget.test( + name: "App", + dependencies: [dep], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + // When + let mapped = try mapper.map(dep, xcodeProj: xcodeProj) + + // Then + let result = try #require(mapped) + let expectedPath = xcodeProj.srcPath.appending(component: "MyLib.framework") + #expect(result == .framework(path: expectedPath, status: .required, condition: nil)) + } + + @Test("Maps dependencies with platform filters to conditions") + func testPlatformConditions() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + + let pbxProj = xcodeProj.pbxproj + let target = try PBXNativeTarget.test( + name: "ConditionalTarget", + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let dep = PBXTargetDependency(name: "ConditionalTarget", target: target).add(to: pbxProj) + dep.platformFilters = ["macos", "ios"] + + try PBXNativeTarget.test( + name: "App", + dependencies: [dep], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + // When + let mapped = try mapper.map(dep, xcodeProj: xcodeProj) + + // Then + let result = try #require(mapped) + #expect(result == .target(name: "ConditionalTarget", status: .required, condition: .when([.ios, .macos]))) + } + + @Test("Ignores dependencies that cannot be matched to targets, products, or proxies") + func testNoMatches() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + + let pbxProj = xcodeProj.pbxproj + // A dependency with no target, no product, no proxy. + let dep = PBXTargetDependency.test(name: nil).add(to: pbxProj) + + try PBXNativeTarget.test( + name: "App", + dependencies: [dep], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + // When + #expect(throws: TargetDependencyMappingError.unknownDependencyType(name: "Unknown dependency name")) { + try mapper.map(dep, xcodeProj: xcodeProj) + } + } + + @Test("Maps single-platform filter dependencies correctly") + func testSinglePlatformFilter() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + + let pbxProj = xcodeProj.pbxproj + let target = try PBXNativeTarget.test( + name: "SinglePlatform", + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let dep = PBXTargetDependency(name: "SinglePlatform", target: target).add(to: pbxProj) + dep.platformFilter = "tvos" + + try PBXNativeTarget.test( + name: "App", + dependencies: [dep], + productType: .application + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + // When + let mapped = try mapper.map(dep, xcodeProj: xcodeProj) + + // Then + #expect(mapped == .target(name: "SinglePlatform", status: .required, condition: .when([.tvos]))) + } + + @Test("Ignores invalid platform filters, mapping dependency without conditions") + func testInvalidPlatformFilter() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + + let pbxProj = xcodeProj.pbxproj + let target = try PBXNativeTarget.test( + name: "UnknownPlatform", + productType: .framework + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + let dep = PBXTargetDependency(name: "UnknownPlatform", target: target).add(to: pbxProj) + dep.platformFilter = "weirdos" + + try PBXNativeTarget.test( + name: "App", + dependencies: [dep], + productType: .commandLineTool + ) + .add(to: pbxProj) + .add(to: pbxProj.rootObject) + + // When + let mapped = try mapper.map(dep, xcodeProj: xcodeProj) + + // Then + #expect(mapped == .target(name: "UnknownPlatform", status: .required, condition: nil)) + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Target/PBXTargetMapperTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Target/PBXTargetMapperTests.swift new file mode 100644 index 00000000..0d347aa5 --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Target/PBXTargetMapperTests.swift @@ -0,0 +1,355 @@ +import FileSystem +import Foundation +import Path +import Testing +import XcodeGraph +import XcodeProj +@testable import XcodeProjMapper + +@Suite +struct PBXTargetMapperTests { + @Test("Maps a basic target with a product bundle identifier") + func testMapBasicTarget() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let target = createTarget( + name: "App", + xcodeProj: xcodeProj, + productType: .application, + buildSettings: ["PRODUCT_BUNDLE_IDENTIFIER": "com.example.app"] + ) + try xcodeProj.mainPBXProject().targets.append(target) + try xcodeProj.write(path: xcodeProj.path!) + + // When + let mapper = PBXTargetMapper() + + let mapped = try await mapper.map(pbxTarget: target, xcodeProj: xcodeProj) + + // Then + #expect(mapped.name == "App") + #expect(mapped.product == .app) + #expect(mapped.productName == "App") + #expect(mapped.bundleId == "com.example.app") + } + + @Test("Throws an error if the target is missing a bundle identifier") + func testMapTargetWithMissingBundleId() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + + let target = createTarget( + name: "App", + xcodeProj: xcodeProj, + productType: .application, + buildSettings: [:] + ) + let mapper = PBXTargetMapper() + + // When / Then + await #expect(throws: PBXTargetMappingError.missingBundleIdentifier(targetName: "App")) { + _ = try await mapper.map(pbxTarget: target, xcodeProj: xcodeProj) + } + } + + @Test("Maps a target with environment variables") + func testMapTargetWithEnvironmentVariables() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let target = createTarget( + name: "App", + xcodeProj: xcodeProj, + productType: .application, + buildSettings: [ + "PRODUCT_BUNDLE_IDENTIFIER": "com.example.app", + "ENVIRONMENT_VARIABLES": ["TEST_VAR": "test_value"], + ] + ) + let mapper = PBXTargetMapper() + + // When + let mapped = try await mapper.map(pbxTarget: target, xcodeProj: xcodeProj) + + // Then + #expect(mapped.environmentVariables["TEST_VAR"]?.value == "test_value") + #expect(mapped.environmentVariables["TEST_VAR"]?.isEnabled == true) + } + + @Test("Maps a target with launch arguments") + func testMapTargetWithLaunchArguments() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + + let target = createTarget( + name: "App", + xcodeProj: xcodeProj, + productType: .application, + buildSettings: [ + "PRODUCT_BUNDLE_IDENTIFIER": "com.example.app", + "LAUNCH_ARGUMENTS": ["-debug", "--verbose"], + ] + ) + let mapper = PBXTargetMapper() + + // When + let mapped = try await mapper.map(pbxTarget: target, xcodeProj: xcodeProj) + + // Then + let expected = [ + LaunchArgument(name: "-debug", isEnabled: true), + LaunchArgument(name: "--verbose", isEnabled: true), + ] + #expect(mapped.launchArguments == expected) + } + + @Test("Maps a target with source files") + func testMapTargetWithSourceFiles() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + + let pbxProj = xcodeProj.pbxproj + let sourceFile = try PBXFileReference.test( + path: "ViewController.swift", + lastKnownFileType: "sourcecode.swift" + ) + .add(to: pbxProj) + .addToMainGroup(in: pbxProj) + + let buildFile = PBXBuildFile(file: sourceFile).add(to: pbxProj) + let sourcesPhase = PBXSourcesBuildPhase(files: [buildFile]).add(to: pbxProj) + + let target = createTarget( + name: "App", + xcodeProj: xcodeProj, + productType: .application, + buildPhases: [sourcesPhase], + buildSettings: ["PRODUCT_BUNDLE_IDENTIFIER": "com.example.app"] + ) + let mapper = PBXTargetMapper() + + // When + let mapped = try await mapper.map(pbxTarget: target, xcodeProj: xcodeProj) + + // Then + #expect(mapped.sources.count == 1) + #expect(mapped.sources[0].path.basename == "ViewController.swift") + } + + @Test("Maps a target with metadata tags") + func testMapTargetWithMetadata() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let target = createTarget( + name: "App", + xcodeProj: xcodeProj, + productType: .application, + buildSettings: [ + "PRODUCT_BUNDLE_IDENTIFIER": "com.example.app", + "TAGS": "tag1, tag2, tag3", + ] + ) + let mapper = PBXTargetMapper() + + // When + let mapped = try await mapper.map(pbxTarget: target, xcodeProj: xcodeProj) + + // Then + #expect(mapped.metadata.tags == Set(["tag1", "tag2", "tag3"])) + } + + @Test("Maps entitlements when CODE_SIGN_ENTITLEMENTS is set") + func testMapEntitlements() async throws { + // Given + + let xcodeProj = try await XcodeProj.test() + let sourceDirectory = xcodeProj.srcPath + let entitlementsPath = sourceDirectory.appending(component: "App.entitlements") + + let buildSettings: BuildSettings = [ + "PRODUCT_BUNDLE_IDENTIFIER": "com.example.app", + "CODE_SIGN_ENTITLEMENTS": "App.entitlements", + ] + + let debugConfig = XCBuildConfiguration( + name: "Debug", + buildSettings: buildSettings + ) + + let configurationList = XCConfigurationList( + buildConfigurations: [debugConfig], + defaultConfigurationName: "Debug" + ) + + xcodeProj.pbxproj.add(object: debugConfig) + xcodeProj.pbxproj.add(object: configurationList) + + let sourceFile = try PBXFileReference.test( + path: "ViewController.swift", + lastKnownFileType: "sourcecode.swift" + ).add(to: xcodeProj.pbxproj).addToMainGroup(in: xcodeProj.pbxproj) + + let buildFile = PBXBuildFile(file: sourceFile).add(to: xcodeProj.pbxproj).add(to: xcodeProj.pbxproj) + let sourcesPhase = PBXSourcesBuildPhase(files: [buildFile]).add(to: xcodeProj.pbxproj).add(to: xcodeProj.pbxproj) + + // Add targets to each project + let target = try PBXNativeTarget.test( + name: "ATarget", + buildConfigurationList: configurationList, + buildPhases: [sourcesPhase], + productType: .framework + ) + .add(to: xcodeProj.pbxproj) + .add(to: xcodeProj.pbxproj.rootObject) + + let mapper = PBXTargetMapper() + + // When + let mapped = try await mapper.map(pbxTarget: target, xcodeProj: xcodeProj) + + // Then + #expect(mapped.entitlements == .file( + path: entitlementsPath, + configuration: BuildConfiguration(name: "Debug", variant: .debug) + )) + } + + @Test("Throws noProjectsFound when pbxProj has no projects") + func testMapTarget_noProjectsFound() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let target = PBXNativeTarget.test() + + try xcodeProj.mainPBXProject().targets.append(target) + try xcodeProj.write(path: xcodeProj.path!) + + let mapper = PBXTargetMapper() + + // When / Then + + do { + _ = try await mapper.map(pbxTarget: target, xcodeProj: xcodeProj) + Issue.record("Should throw an error") + } catch { + let err = try #require(error as? PBXObjectError) + #expect(err.description == "The PBXObjects instance has been released before saving.") + } + } + + @Test("Parses a valid Info.plist successfully") + func testMapTarget_validPlist() async throws { + // Given + + let xcodeProj = try await XcodeProj.test() + let srcPath = xcodeProj.srcPath + let relativePath = try RelativePath(validating: "Info.plist") + let plistPath = srcPath.appending(relativePath) + + let plistContent: [String: Any] = [ + "CFBundleIdentifier": "com.example.app", + "CFBundleName": "ExampleApp", + "CFVersion": 1.4, + ] + let data = try PropertyListSerialization.data(fromPropertyList: plistContent, format: .xml, options: 0) + try data.write(to: URL(fileURLWithPath: plistPath.pathString)) + + let target = createTarget( + name: "App", + xcodeProj: xcodeProj, + productType: .application, + buildSettings: [ + "PRODUCT_BUNDLE_IDENTIFIER": "com.example.app", + "INFOPLIST_FILE": relativePath.pathString, + ] + ) + + try xcodeProj.write(path: xcodeProj.path!) + let mapper = PBXTargetMapper() + + // When + let mapped = try await mapper.map(pbxTarget: target, xcodeProj: xcodeProj) + + // Then + #expect({ + switch mapped.infoPlist { + case let .dictionary(dict, _): + + return dict["CFBundleIdentifier"] == "com.example.app" + && dict["CFBundleName"] == "ExampleApp" + default: + return false + } + }() == true) + } + + @Test("Throws invalidPlist when Info.plist cannot be parsed") + func testMapTarget_invalidPlist() async throws { + // Given + let xcodeProj = try await XcodeProj.test() + let srcPath = xcodeProj.srcPath + let relativePath = try RelativePath(validating: "Invalid.plist") + let invalidPlistPath = srcPath.appending(relativePath) + try await FileSystem().writeText("Invalid Plist", at: invalidPlistPath) + + let target = createTarget( + name: "App", + xcodeProj: xcodeProj, + productType: .application, + buildSettings: [ + "PRODUCT_BUNDLE_IDENTIFIER": "com.example.app", + "INFOPLIST_FILE": relativePath.pathString, + ] + ) + try xcodeProj.write(path: xcodeProj.path!) + + let mapper = PBXTargetMapper() + + // When / Then + await #expect { + _ = try await mapper.map(pbxTarget: target, xcodeProj: xcodeProj) + } throws: { error in + error.localizedDescription + == "Failed to read a valid plist dictionary from file at: \(invalidPlistPath.pathString)." + } + } + + // MARK: - Helper Methods + + private func createTarget( + name: String, + xcodeProj: XcodeProj, + productType: PBXProductType, + buildPhases: [PBXBuildPhase] = [], + buildSettings: [String: Any] = [:], + dependencies: [PBXTargetDependency] = [] + ) -> PBXNativeTarget { + let debugConfig = XCBuildConfiguration( + name: "Debug", + buildSettings: buildSettings + ) + + let releaseConfig = XCBuildConfiguration( + name: "Release", + buildSettings: buildSettings + ) + + let configurationList = XCConfigurationList( + buildConfigurations: [debugConfig, releaseConfig], + defaultConfigurationName: "Release" + ) + + xcodeProj.pbxproj.add(object: debugConfig) + xcodeProj.pbxproj.add(object: releaseConfig) + xcodeProj.pbxproj.add(object: configurationList) + + let target = PBXNativeTarget.test( + name: name, + buildConfigurationList: configurationList, + buildRules: [], + buildPhases: buildPhases, + dependencies: dependencies, + productType: productType + ) + + return target + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Target/TargetDependencyExtensionsTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Target/TargetDependencyExtensionsTests.swift new file mode 100644 index 00000000..fdda04a3 --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Target/TargetDependencyExtensionsTests.swift @@ -0,0 +1,267 @@ +import Path +import Testing +import XcodeMetadata +import XcodeProj +@testable import XcodeGraph +@testable import XcodeProjMapper + +@Suite +struct TargetDependencyExtensionsTests { + let sourceDirectory = AssertionsTesting.fixturePath() + let target = Target.test(platform: .iOS) + + // A dummy target map for .project dependencies + let allTargetsMap: [String: Target] = [ + "StaticLibrary": Target.test(name: "libStaticLibrary", product: .staticLibrary), + "MyProjectTarget": Target.test(name: "MyProjectTarget", product: .framework), + "MyProjectDynamicLibrary": Target.test(name: "MyProjectDynamicLibrary", product: .dynamicLibrary), + ] + + @Test("Resolves a target dependency into a target graph dependency") + func testTargetGraphDependency_Target() async throws { + // Given + let dependency = TargetDependency.target(name: "App", status: .required, condition: nil) + + // When + let graphDep = try await dependency.graphDependency( + sourceDirectory: sourceDirectory, + allTargetsMap: allTargetsMap, + target: target + ) + + // Then + #expect(graphDep == .target(name: "App", path: sourceDirectory, status: .required)) + } + + @Test("Resolves a project-based framework dependency to a dynamic framework in the graph") + func testTargetGraphDependencyFramework_Project() async throws { + // Given + let dependency = TargetDependency.project( + target: "MyProjectTarget", + path: sourceDirectory, + status: .required, + condition: nil + ) + + // When + let graphDep = try await dependency.graphDependency( + sourceDirectory: sourceDirectory, + allTargetsMap: allTargetsMap, + target: target + ) + + // Then + #expect({ + switch graphDep { + case let .framework(path, binaryPath, _, _, linking, archs, status): + return path == sourceDirectory + && binaryPath == sourceDirectory.appending(component: "MyProjectTarget.framework") + && linking == .dynamic && archs.isEmpty && status == .required + default: + return false + } + }() == true) + } + + @Test("Resolves a project-based dynamic library dependency correctly") + func testTargetGraphDependencyLibrary_Project() async throws { + // Given + let libraryPath = AssertionsTesting.fixturePath(path: try RelativePath(validating: "libStaticLibrary.a")) + + let dependency = TargetDependency.project( + target: "StaticLibrary", + path: sourceDirectory, + status: .required, + condition: nil + ) + + // When + let graphDep = try await dependency.graphDependency( + sourceDirectory: sourceDirectory, + allTargetsMap: allTargetsMap, + target: target + ) + + let expected = GraphDependency.library( + path: libraryPath, + publicHeaders: libraryPath.parentDirectory.appending(component: "include"), + linking: .static, + architectures: [], + swiftModuleMap: nil + ) + + // Then + #expect(expected == graphDep) + } + + @Test("Resolves a framework file dependency into a dynamic framework graph dependency") + func testTargetGraphDependency_Framework() async throws { + // Given + let frameworkPath = sourceDirectory.appending(component: "xpm.framework") + let dependency = TargetDependency.framework(path: frameworkPath, status: .required, condition: nil) + + // When + let graphDep = try await dependency.graphDependency( + sourceDirectory: sourceDirectory, + allTargetsMap: allTargetsMap, + target: target + ) + let expectedBinaryPath = frameworkPath.appending(component: frameworkPath.basenameWithoutExt) + let expectedDsymPath = frameworkPath.parentDirectory.appending(component: "xpm.framework.dSYM") + let expected = GraphDependency.framework( + path: frameworkPath, + binaryPath: expectedBinaryPath, + dsymPath: expectedDsymPath, + bcsymbolmapPaths: [], + linking: .dynamic, + architectures: [.x8664, .arm64], + status: .required + ) + + // Then + #expect(expected == graphDep) + } + + @Test("Resolves an XCFramework dependency to the correct .xcframework graph dependency") + func testTargetGraphDependency_XCFramework() async throws { + // Given + let xcframeworkPath = sourceDirectory.appending(component: "MyFramework.xcframework") + let dependency = TargetDependency.xcframework(path: xcframeworkPath, status: .required, condition: nil) + + // When + let graphDep = try await dependency.graphDependency( + sourceDirectory: sourceDirectory, + allTargetsMap: allTargetsMap, + target: target + ) + + // Then + #expect({ + switch graphDep { + case let .xcframework(info): + return info.path == xcframeworkPath && info.linking == .dynamic && info.status == .required + default: + return false + } + }() == true) + } + + @Test("Resolves a static library dependency to a static library graph dependency") + func testTargetGraphDependency_Library() async throws { + // Given + let libPath = sourceDirectory.appending(component: "libStaticLibrary.a") + let headersPath = sourceDirectory.parentDirectory + let dependency = TargetDependency.library(path: libPath, publicHeaders: headersPath, swiftModuleMap: nil, condition: nil) + + // When + let graphDep = try await dependency.graphDependency( + sourceDirectory: sourceDirectory, + allTargetsMap: allTargetsMap, + target: target + ) + let expected = GraphDependency.library( + path: libPath, + publicHeaders: headersPath, + linking: .static, + architectures: [.x8664], + swiftModuleMap: nil + ) + + // Then + #expect(expected == graphDep) + } + + @Test("Resolves a package product dependency to a package product graph dependency") + func testTargetGraphDependency_Package() async throws { + // Given + let dependency = TargetDependency.package(product: "MyPackageProduct", type: .runtime, condition: nil) + + // When + let graphDep = try await dependency.graphDependency( + sourceDirectory: sourceDirectory, + allTargetsMap: allTargetsMap, + target: target + ) + // Then + #expect(graphDep == .packageProduct(path: sourceDirectory, product: "MyPackageProduct", type: .runtime)) + } + + @Test("Resolves an SDK dependency to the correct SDK graph dependency") + func testTargetGraphDependency_SDK() async throws { + // Given + let dependency = TargetDependency.sdk(name: "MySDK", status: .optional, condition: nil) + + // When + let graphDep = try await dependency.graphDependency( + sourceDirectory: sourceDirectory, + allTargetsMap: allTargetsMap, + target: target + ) + // Then + #expect({ + switch graphDep { + case let .sdk(name, path, status, source): + return name == "MySDK" && path == sourceDirectory && status == .optional && source == .developer + default: + return false + } + }() == true) + } + + @Test("Resolves an XCTest dependency to an XCFramework graph dependency") + func testTargetGraphDependency_XCTest() async throws { + // Given + let dependency = TargetDependency.xctest + let developerDirectory = try await DeveloperDirectoryProvider().developerDirectory() + + let srcPath = "/Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework" + let path = try AbsolutePath( + validating: developerDirectory.pathString + srcPath + ) + // When + let graphDep = try await dependency.graphDependency( + sourceDirectory: sourceDirectory, + allTargetsMap: allTargetsMap, + target: target + ) + let expected = GraphDependency.framework( + path: path, + binaryPath: path.appending(component: "XCTest"), + dsymPath: nil, + bcsymbolmapPaths: [], + linking: .dynamic, + architectures: [.arm64, .arm64e], + status: .required + ) + + // Then + #expect(expected == graphDep) + } + + @Test("Throws a MappingError when a project target does not exist in allTargetsMap") + func testMapProjectGraphDependency_TargetNotFound() async throws { + // Given + let dependency = TargetDependency.project( + target: "NonExistentTarget", + path: sourceDirectory, + status: .required, + condition: nil + ) + + // When / Then + do { + _ = try await dependency.graphDependency(sourceDirectory: sourceDirectory, allTargetsMap: [:], target: target) + Issue.record("Expected to throw TargetDependencyMappingError.targetNotFound") + } catch let error as TargetDependencyMappingError { + switch error { + case let .targetNotFound(targetName, path): + #expect(targetName == "NonExistentTarget") + #expect(path == sourceDirectory) + default: + Issue.record("Unexpected TargetDependencyMappingError: \(error)") + } + } catch { + Issue.record("Unexpected error: \(error)") + } + } +} diff --git a/Tests/XcodeProjMapperTests/MapperTests/Workspace/XCWorkspaceMapperTests.swift b/Tests/XcodeProjMapperTests/MapperTests/Workspace/XCWorkspaceMapperTests.swift new file mode 100644 index 00000000..f889f4d0 --- /dev/null +++ b/Tests/XcodeProjMapperTests/MapperTests/Workspace/XCWorkspaceMapperTests.swift @@ -0,0 +1,214 @@ +import Foundation +import Path +import PathKit +import Testing +import XcodeGraph +import XcodeProj +@testable import XcodeProjMapper + +@Suite +struct XCWorkspaceMapperTests { + @Test("Maps workspace without any projects or schemes") + func testMap_NoProjectsOrSchemes() async throws { + // Given + let workspacePath = try AbsolutePath(validating: "/tmp/MyWorkspace.xcworkspace") + let xcworkspace: XCWorkspace = .test(files: ["ReadMe.md"], path: workspacePath.pathString) + let mapper = XCWorkspaceMapper() + + // When + let workspace = try await mapper.map(xcworkspace: xcworkspace) + + // Then + #expect(workspace.name == "MyWorkspace") + #expect(workspace.projects.isEmpty == true) + #expect(workspace.schemes.isEmpty == true) + } + + @Test("Maps workspace with multiple projects") + func testMap_MultipleProjects() async throws { + // Given + let workspacePath = try AbsolutePath(validating: "/tmp/MyWorkspace.xcworkspace") + let workspaceDir = workspacePath.parentDirectory + let xcworkspace: XCWorkspace = .test( + withElements: [ + .test(relativePath: "ProjectA.xcodeproj"), + .group(XCWorkspaceDataGroup( + location: .group("NestedGroup"), + name: "NestedGroup", + children: [ + .test(relativePath: "ProjectB.xcodeproj"), + .test(relativePath: "Notes.txt"), + ] + )), + ], path: workspacePath.pathString + ) + let mapper = XCWorkspaceMapper() + + // When + let workspace = try await mapper.map(xcworkspace: xcworkspace) + + // Then + #expect(workspace.name == "MyWorkspace") + #expect(workspace.projects.count == 2) + #expect(workspace.projects.contains(workspaceDir.appending(component: "ProjectA.xcodeproj")) == true) + #expect(workspace.projects.contains(workspaceDir.appending(components: ["NestedGroup", "ProjectB.xcodeproj"])) == true) + #expect(workspace.schemes.isEmpty == true) + } + + @Test("Maps workspace with shared schemes") + func testMap_WithSchemes() async throws { + // Given + let tempDirectory = FileManager.default.temporaryDirectory + let path = tempDirectory.appendingPathComponent("MyWorkspace.xcworkspace") + let workspacePath = try AbsolutePath(validating: path.path) + let sharedDataDir = workspacePath.pathString + "/xcshareddata/xcschemes" + try FileManager.default.createDirectory(atPath: sharedDataDir, withIntermediateDirectories: true) + let schemeFile = sharedDataDir + "/MyScheme.xcscheme" + try "dummy scheme content".write(toFile: schemeFile, atomically: true, encoding: .utf8) + + let xcworkspace: XCWorkspace = .test(withElements: [.test(relativePath: "App.xcodeproj")], path: workspacePath.pathString) + let mapper = XCWorkspaceMapper() + + // When / Then + // We expect an XML parser error due to dummy content. + do { + _ = try await mapper.map(xcworkspace: xcworkspace) + } catch { + #expect(error.localizedDescription == "The operation couldn’t be completed. (NSXMLParserErrorDomain error 4.)") + } + } + + @Test("No schemes directory results in no schemes mapped") + func testMap_NoSchemesDirectory() async throws { + // Given + let workspacePath = try AbsolutePath(validating: "/tmp/MyWorkspace.xcworkspace") + let xcworkspace = XCWorkspace.test(withElements: [ + .test(relativePath: "App.xcodeproj"), + ], path: workspacePath.pathString) + let mapper = XCWorkspaceMapper() + + // When + let workspace = try await mapper.map(xcworkspace: xcworkspace) + + // Then + #expect(workspace.schemes.isEmpty == true) + } + + @Test("Workspace name is derived from the .xcworkspace file name") + func testMap_NameDerivation() async throws { + // Given + let workspacePath = try AbsolutePath(validating: "/tmp/AnotherWorkspace.xcworkspace") + let xcworkspace = XCWorkspace.test(withElements: [], path: workspacePath.pathString) + let mapper = XCWorkspaceMapper() + + // When + let workspace = try await mapper.map(xcworkspace: xcworkspace) + + // Then + #expect(workspace.name == "AnotherWorkspace") + } + + @Test("Resolves absolute path in XCWorkspaceDataFileRef") + func testMap_AbsolutePath() async throws { + // Given + let workspacePath = try AbsolutePath(validating: "/tmp/AbsWorkspace.xcworkspace") + let elements: [XCWorkspaceDataElement] = [ + .file(XCWorkspaceDataFileRef(location: .absolute("/Users/SomeUser/ProjectC.xcodeproj"))), + ] + let xcworkspace = XCWorkspace(data: XCWorkspaceData(children: elements), path: .init(workspacePath.pathString)) + let mapper = XCWorkspaceMapper() + + // When + let workspace = try await mapper.map(xcworkspace: xcworkspace) + + // Then + #expect(workspace.projects.isEmpty == false) + } + + @Test("Resolves container path in XCWorkspaceDataFileRef") + func testMap_ContainerPath() async throws { + // Given + let workspacePath = try AbsolutePath(validating: "/tmp/ContainerWorkspace.xcworkspace") + let elements: [XCWorkspaceDataElement] = [ + .file(XCWorkspaceDataFileRef(location: .container("Nested/ProjectD.xcodeproj"))), + ] + let xcworkspace = XCWorkspace(data: XCWorkspaceData(children: elements), path: .init(workspacePath.pathString)) + let mapper = XCWorkspaceMapper() + + // When + let workspace = try await mapper.map(xcworkspace: xcworkspace) + + // Then + #expect(workspace.projects.isEmpty == false) + } + + @Test("Resolves developer path in XCWorkspaceDataFileRef") + func testMap_DeveloperPath() async throws { + // Given + let workspacePath = try AbsolutePath(validating: "/tmp/DevWorkspace.xcworkspace") + let elements: [XCWorkspaceDataElement] = [ + .file(XCWorkspaceDataFileRef(location: .developer("Platforms/iPhoneOS.platform/Developer/ProjectE.xcodeproj"))), + ] + let xcworkspace = XCWorkspace(data: XCWorkspaceData(children: elements), path: .init(workspacePath.pathString)) + let mapper = XCWorkspaceMapper() + + // When + let workspace = try await mapper.map(xcworkspace: xcworkspace) + + // Then + #expect(workspace.projects.isEmpty == false) + } + + @Test("Resolves group path in XCWorkspaceDataFileRef") + func testMap_GroupPath() async throws { + // Given + let workspacePath = try AbsolutePath(validating: "/tmp/GroupWorkspace.xcworkspace") + let elements: [XCWorkspaceDataElement] = [ + .group(XCWorkspaceDataGroup(location: .group("MyGroup"), name: "MyGroup", children: [ + .file(XCWorkspaceDataFileRef(location: .group("Subfolder/ProjectF.xcodeproj"))), + ])), + ] + let xcworkspace = XCWorkspace(data: XCWorkspaceData(children: elements), path: .init(workspacePath.pathString)) + let mapper = XCWorkspaceMapper() + + // When + let workspace = try await mapper.map(xcworkspace: xcworkspace) + + // Then + #expect(workspace.projects.isEmpty == false) + } + + @Test("Resolves current path in XCWorkspaceDataFileRef") + func testMap_CurrentPath() async throws { + // Given + let workspacePath = try AbsolutePath(validating: "/tmp/CurrentWorkspace.xcworkspace") + let elements: [XCWorkspaceDataElement] = [ + .file(XCWorkspaceDataFileRef(location: .current("RelativePath/ProjectG.xcodeproj"))), + ] + let xcworkspace = XCWorkspace(data: XCWorkspaceData(children: elements), path: .init(workspacePath.pathString)) + let mapper = XCWorkspaceMapper() + + // When + let workspace = try await mapper.map(xcworkspace: xcworkspace) + + // Then + #expect(workspace.projects.isEmpty == false) + } + + @Test("Resolves other path in XCWorkspaceDataFileRef") + func testMap_otherPath() async throws { + // Given + let workspacePath = try AbsolutePath(validating: "/tmp/OtherWorkspace.xcworkspace") + let elements: [XCWorkspaceDataElement] = [ + .file(XCWorkspaceDataFileRef(location: .other("customscheme", "Path/ProjectH.xcodeproj"))), + ] + let xcworkspace = XCWorkspace(data: XCWorkspaceData(children: elements), path: .init(workspacePath.pathString)) + let mapper = XCWorkspaceMapper() + + // When + let workspace = try await mapper.map(xcworkspace: xcworkspace) + + // Then + #expect(workspace.projects.isEmpty == false) + } +} diff --git a/Tests/XcodeProjMapperTests/Mocks/AssertionsTesting.swift b/Tests/XcodeProjMapperTests/Mocks/AssertionsTesting.swift new file mode 100644 index 00000000..0b0ece2d --- /dev/null +++ b/Tests/XcodeProjMapperTests/Mocks/AssertionsTesting.swift @@ -0,0 +1,32 @@ +import Foundation +import Path +import ServiceContextModule +import Testing + +enum AssertionsTesting { + static func fixturePath() -> AbsolutePath { + try! AbsolutePath( + validating: #filePath + ) + .parentDirectory + .parentDirectory + .parentDirectory + .appending(components: "Fixtures") + } + + /// Resolves a fixture path relative to the project's root. + static func fixturePath(path: RelativePath) -> AbsolutePath { + fixturePath().appending(path) + } +} + +extension AbsolutePath: Swift.ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + do { + self = try AbsolutePath(validating: value) + } catch { + Issue.record("Invalid path at: \(value) - Error: \(error)") + self = AbsolutePath("/") + } + } +} diff --git a/Tests/XcodeProjMapperTests/Mocks/MockDefaults.swift b/Tests/XcodeProjMapperTests/Mocks/MockDefaults.swift new file mode 100644 index 00000000..75ff48b6 --- /dev/null +++ b/Tests/XcodeProjMapperTests/Mocks/MockDefaults.swift @@ -0,0 +1,22 @@ +import Foundation +import Path +import XcodeGraph +@testable import XcodeProj + +enum MockDefaults { + nonisolated(unsafe) static let defaultDebugSettings: [String: Any] = [ + "PRODUCT_NAME": "$(TARGET_NAME)", + "ENABLE_STRICT_OBJC_MSGSEND": "YES", + "PRODUCT_BUNDLE_IDENTIFIER": "com.example.debug", + ] + + nonisolated(unsafe) static let defaultReleaseSettings: [String: Any] = [ + "PRODUCT_NAME": "$(TARGET_NAME)", + "VALIDATE_PRODUCT": "YES", + "PRODUCT_BUNDLE_IDENTIFIER": "com.example.release", + ] + + nonisolated(unsafe) static let defaultProjectAttributes: [String: Any] = [ + "BuildIndependentTargetsInParallel": "YES", + ] +} diff --git a/Tests/XcodeProjMapperTests/TestData/.swift b/Tests/XcodeProjMapperTests/TestData/.swift new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/.swift @@ -0,0 +1 @@ + diff --git a/Tests/XcodeProjMapperTests/TestData/PBXBuildRule+TestData.swift b/Tests/XcodeProjMapperTests/TestData/PBXBuildRule+TestData.swift new file mode 100644 index 00000000..739571e4 --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/PBXBuildRule+TestData.swift @@ -0,0 +1,32 @@ +import XcodeGraph +import XcodeProj + +extension PBXBuildRule { + static func test( + compilerSpec: String = BuildRule.CompilerSpec.appleClang.rawValue, + fileType: String = BuildRule.FileType.cSource.rawValue, + isEditable: Bool = true, + filePatterns: String? = "*.cpp;*.cxx;*.cc", + name: String = "Default Build Rule", + dependencyFile: String? = nil, + outputFiles: [String] = ["$(DERIVED_FILE_DIR)/$(INPUT_FILE_BASE).o"], + inputFiles: [String] = [], + outputFilesCompilerFlags: [String]? = nil, + script: String? = nil, + runOncePerArchitecture: Bool? = nil + ) -> PBXBuildRule { + PBXBuildRule( + compilerSpec: compilerSpec, + fileType: fileType, + isEditable: isEditable, + filePatterns: filePatterns, + name: name, + dependencyFile: dependencyFile, + outputFiles: outputFiles, + inputFiles: inputFiles, + outputFilesCompilerFlags: outputFilesCompilerFlags, + script: script, + runOncePerArchitecture: runOncePerArchitecture + ) + } +} diff --git a/Tests/XcodeProjMapperTests/TestData/PBXFileReference+TestData.swift b/Tests/XcodeProjMapperTests/TestData/PBXFileReference+TestData.swift new file mode 100644 index 00000000..893ecfb9 --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/PBXFileReference+TestData.swift @@ -0,0 +1,21 @@ +import XcodeProj + +extension PBXFileReference { + static func test( + sourceTree: PBXSourceTree = .group, + name: String? = nil, + explicitFileType: String? = nil, + path: String = "AppDelegate.swift", + lastKnownFileType: String? = "sourcecode.swift", + includeInIndex: Bool? = nil + ) -> PBXFileReference { + PBXFileReference( + sourceTree: sourceTree, + name: name, + explicitFileType: explicitFileType, + lastKnownFileType: lastKnownFileType, + path: path, + includeInIndex: includeInIndex + ) + } +} diff --git a/Tests/XcodeProjMapperTests/TestData/PBXGroup+TestData.swift b/Tests/XcodeProjMapperTests/TestData/PBXGroup+TestData.swift new file mode 100644 index 00000000..136bcb07 --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/PBXGroup+TestData.swift @@ -0,0 +1,17 @@ +import XcodeProj + +extension PBXGroup { + static func test( + children: [PBXFileElement] = [], + sourceTree: PBXSourceTree = .group, + name: String? = "MainGroup", + path: String? = "/tmp/TestProject" + ) -> PBXGroup { + PBXGroup( + children: children, + sourceTree: sourceTree, + name: name, + path: path + ) + } +} diff --git a/Tests/XcodeProjMapperTests/TestData/PBXNativeTarget+TestData.swift b/Tests/XcodeProjMapperTests/TestData/PBXNativeTarget+TestData.swift new file mode 100644 index 00000000..9bf7b033 --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/PBXNativeTarget+TestData.swift @@ -0,0 +1,36 @@ +import XcodeProj + +extension PBXNativeTarget { + static func test( + name: String = "App", + buildConfigurationList: XCConfigurationList? = nil, + buildRules: [PBXBuildRule] = [PBXBuildRule.test()], + buildPhases: [PBXBuildPhase] = [ + PBXSourcesBuildPhase(files: []), + PBXResourcesBuildPhase(files: []), + PBXFrameworksBuildPhase(files: []), + ], + dependencies: [PBXTargetDependency] = [], + productInstallPath: String? = nil, + productType: PBXProductType = .application, + product: PBXFileReference? = PBXFileReference.test( + sourceTree: .buildProductsDir, + explicitFileType: "wrapper.application", + path: "App.app", + lastKnownFileType: nil, + includeInIndex: false + ) + ) -> PBXNativeTarget { + PBXNativeTarget( + name: name, + buildConfigurationList: buildConfigurationList, + buildPhases: buildPhases, + buildRules: buildRules, + dependencies: dependencies, + productInstallPath: productInstallPath, + productName: name, + product: product, + productType: productType + ) + } +} diff --git a/Tests/XcodeProjMapperTests/TestData/PBXProj+TestData.swift b/Tests/XcodeProjMapperTests/TestData/PBXProj+TestData.swift new file mode 100644 index 00000000..956939af --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/PBXProj+TestData.swift @@ -0,0 +1,61 @@ +import Testing +@testable import XcodeProj + +extension PBXProj { + func add(objects: [PBXObject]) { + objects.forEach { add(object: $0) } + } +} + +extension PBXObject { + @discardableResult + func add(to pbxProj: PBXProj) -> Self { + pbxProj.add(object: self) + + return self + } +} + +extension PBXFileElement { + @discardableResult + func addToMainGroup(in pbxProj: PBXProj) throws -> Self { + let project = try #require(pbxProj.projects.first) + project.mainGroup.children.append(self) + return self + } +} + +extension PBXTarget { + @discardableResult + func add(to pbxProject: PBXProject?) throws -> Self { + let project = try #require(pbxProject) + project.targets.append(self) + return self + } +} + +extension PBXProj { + /// Adds a PBXObject to the project and returns it. + @discardableResult + func addObject(_ object: T) -> T { + add(object: object) + return object + } + + /// Adds a PBXFileReference and optionally attaches it to the main group. + @discardableResult + func addFileReference( + _ file: PBXFileReference, + addToMainGroup: Bool = true + ) -> PBXFileReference { + addObject(file) + + if addToMainGroup, let project = projects.first, + let mainGroup = project.mainGroup + { + mainGroup.children.append(file) + } + + return file + } +} diff --git a/Tests/XcodeProjMapperTests/TestData/PBXProject+TestData.swift b/Tests/XcodeProjMapperTests/TestData/PBXProject+TestData.swift new file mode 100644 index 00000000..4311c332 --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/PBXProject+TestData.swift @@ -0,0 +1,114 @@ +import XcodeProj + +extension PBXProject { + static func test( + name: String = "MainApp", + buildConfigurationList: XCConfigurationList, + compatibilityVersion: String = "Xcode 14.0", + preferredProjectObjectVersion: Int? = nil, + minimizedProjectReferenceProxies: Int? = nil, + mainGroup: PBXGroup, + developmentRegion: String = "en", + hasScannedForEncodings: Int = 0, + knownRegions: [String] = ["Base", "en"], + productsGroup: PBXGroup? = nil, + projectDirPath: String = "", + projects: [[String: PBXFileElement]] = [ + ["B900DB68213936CC004AEC3E": PBXFileReference.test(name: "App", path: "App.xcodeproj")], + ], + projectRoots: [String] = [""], + targets: [PBXTarget] = [], + attributes: [String: Any] = MockDefaults.defaultProjectAttributes, + packageReferences: [XCRemoteSwiftPackageReference] = [], + targetAttributes: [PBXTarget: [String: Any]] = [:] + ) -> PBXProject { + PBXProject( + name: name, + buildConfigurationList: buildConfigurationList, + compatibilityVersion: compatibilityVersion, + preferredProjectObjectVersion: preferredProjectObjectVersion, + minimizedProjectReferenceProxies: minimizedProjectReferenceProxies, + mainGroup: mainGroup, + developmentRegion: developmentRegion, + hasScannedForEncodings: hasScannedForEncodings, + knownRegions: knownRegions, + productsGroup: productsGroup, + projectDirPath: projectDirPath, + projects: projects, + projectRoots: projectRoots, + targets: targets, + packages: packageReferences, + attributes: attributes, + targetAttributes: targetAttributes + ) + } +} + +// +// +// import XcodeProj +// +// extension PBXProject { +// static func test( +// name: String = "MainApp", +// buildConfigurationList: XCConfigurationList? = nil, +// compatibilityVersion: String = "Xcode 14.0", +// mainGroup: PBXGroup? = nil, +// developmentRegion: String = "en", +// knownRegions: [String] = ["Base", "en"], +// productsGroup: PBXGroup? = nil, +// targets: [PBXTarget]? = nil, +// attributes: [String: Any] = MockDefaults.defaultProjectAttributes, +// packageReferences: [XCRemoteSwiftPackageReference] = [], +// pbxProj: PBXProj +// ) -> PBXProject { +// let resolvedMainGroup = +// mainGroup +// ?? PBXGroup.test( +// children: [], +// sourceTree: .group, +// name: "MainGroup", +// path: "/tmp/TestProject", +// pbxProj: pbxProj, +// addToMainGroup: false +// ) +// +// let resolvedBuildConfigList = buildConfigurationList ?? XCConfigurationList.test(proj: pbxProj) +// pbxProj.add(object: resolvedBuildConfigList) +// +// if let productsGroup { +// pbxProj.add(object: productsGroup) +// } +// +// let projectRef = PBXFileReference.test( +// name: "App", +// path: "App.xcodeproj", +// pbxProj: pbxProj, +// addToMainGroup: false +// ).add(to: pbxProj).addToMainGroup(in: pbxProj) +// +// let proj = PBXProject( +// name: name, +// buildConfigurationList: resolvedBuildConfigList, +// compatibilityVersion: compatibilityVersion, +// preferredProjectObjectVersion: nil, +// minimizedProjectReferenceProxies: nil, +// mainGroup: resolvedMainGroup, +// developmentRegion: developmentRegion, +// hasScannedForEncodings: 0, +// knownRegions: knownRegions, +// productsGroup: productsGroup, +// projectDirPath: "", +// projects: [["B900DB68213936CC004AEC3E": projectRef]], +// projectRoots: [""], +// targets: targets ?? [], +// packages: packageReferences, +// attributes: attributes, +// targetAttributes: [:] +// ) +// +// pbxProj.add(object: proj) +// pbxProj.rootObject = proj +// return proj +// } +// } diff --git a/Tests/XcodeProjMapperTests/TestData/PBXShellScriptBuildPhase+TestData.swift b/Tests/XcodeProjMapperTests/TestData/PBXShellScriptBuildPhase+TestData.swift new file mode 100644 index 00000000..1e37ba28 --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/PBXShellScriptBuildPhase+TestData.swift @@ -0,0 +1,35 @@ +import XcodeProj + +extension PBXShellScriptBuildPhase { + static func test( + files: [PBXBuildFile] = [], + name: String? = "Embed Precompiled Frameworks", + shellScript: String = "#!/bin/sh\necho 'Mock Shell Script'", + inputPaths: [String] = [], + outputPaths: [String] = [], + inputFileListPaths: [String]? = nil, + outputFileListPaths: [String]? = nil, + shellPath: String = "/bin/sh", + buildActionMask: UInt = PBXBuildPhase.defaultBuildActionMask, + runOnlyForDeploymentPostprocessing: Bool = false, + showEnvVarsInLog: Bool = true, + alwaysOutOfDate: Bool = false, + dependencyFile: String? = nil + ) -> PBXShellScriptBuildPhase { + PBXShellScriptBuildPhase( + files: files, + name: name, + inputPaths: inputPaths, + outputPaths: outputPaths, + inputFileListPaths: inputFileListPaths, + outputFileListPaths: outputFileListPaths, + shellPath: shellPath, + shellScript: shellScript, + buildActionMask: buildActionMask, + runOnlyForDeploymentPostprocessing: runOnlyForDeploymentPostprocessing, + showEnvVarsInLog: showEnvVarsInLog, + alwaysOutOfDate: alwaysOutOfDate, + dependencyFile: dependencyFile + ) + } +} diff --git a/Tests/XcodeProjMapperTests/TestData/PBXTargetDependency+TestData.swift b/Tests/XcodeProjMapperTests/TestData/PBXTargetDependency+TestData.swift new file mode 100644 index 00000000..c65e9f3d --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/PBXTargetDependency+TestData.swift @@ -0,0 +1,19 @@ +import XcodeProj + +extension PBXTargetDependency { + static func test( + name: String? = "App", + target: PBXTarget? = nil, + targetProxy: PBXContainerItemProxy? = nil, + platformFilter: String? = nil, + platformFilters: [String]? = nil + ) -> PBXTargetDependency { + PBXTargetDependency( + name: name, + platformFilter: platformFilter, + platformFilters: platformFilters, + target: target, + targetProxy: targetProxy + ) + } +} diff --git a/Tests/XcodeProjMapperTests/TestData/PBXVariantGroup+TestData.swift b/Tests/XcodeProjMapperTests/TestData/PBXVariantGroup+TestData.swift new file mode 100644 index 00000000..b2507820 --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/PBXVariantGroup+TestData.swift @@ -0,0 +1,17 @@ +import XcodeProj + +extension PBXVariantGroup { + static func mockVariant( + children: [PBXFileElement] = [], + sourceTree: PBXSourceTree = .group, + name: String? = "MainGroup", + path: String? = "/tmp/TestProject" + ) -> PBXVariantGroup { + PBXVariantGroup( + children: children, + sourceTree: sourceTree, + name: name, + path: path + ) + } +} diff --git a/Tests/XcodeProjMapperTests/TestData/XCBuildConfiguration+TestData.swift b/Tests/XcodeProjMapperTests/TestData/XCBuildConfiguration+TestData.swift new file mode 100644 index 00000000..cb3251fe --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/XCBuildConfiguration+TestData.swift @@ -0,0 +1,25 @@ +import XcodeProj + +extension XCBuildConfiguration { + static func testDebug( + baseConfiguration: PBXFileReference? = nil, + buildSettings: BuildSettings = MockDefaults.defaultDebugSettings + ) -> XCBuildConfiguration { + XCBuildConfiguration( + name: "Debug", + baseConfiguration: baseConfiguration, + buildSettings: buildSettings + ) + } + + static func testRelease( + baseConfiguration: PBXFileReference? = nil, + buildSettings: BuildSettings = MockDefaults.defaultReleaseSettings + ) -> XCBuildConfiguration { + XCBuildConfiguration( + name: "Release", + baseConfiguration: baseConfiguration, + buildSettings: buildSettings + ) + } +} diff --git a/Tests/XcodeProjMapperTests/TestData/XCConfigurationList+TestData.swift b/Tests/XcodeProjMapperTests/TestData/XCConfigurationList+TestData.swift new file mode 100644 index 00000000..0ad7a201 --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/XCConfigurationList+TestData.swift @@ -0,0 +1,15 @@ +import XcodeProj + +extension XCConfigurationList { + static func test( + buildConfigurations: [XCBuildConfiguration] = [], + defaultConfigurationName: String = "Release", + defaultConfigurationIsVisible: Bool = false + ) -> XCConfigurationList { + XCConfigurationList( + buildConfigurations: buildConfigurations, + defaultConfigurationName: defaultConfigurationName, + defaultConfigurationIsVisible: defaultConfigurationIsVisible + ) + } +} diff --git a/Tests/XcodeProjMapperTests/TestData/XCScheme+TestData.swift b/Tests/XcodeProjMapperTests/TestData/XCScheme+TestData.swift new file mode 100644 index 00000000..e97dbfc2 --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/XCScheme+TestData.swift @@ -0,0 +1,29 @@ +import XcodeProj + +extension XCScheme { + static func test( + name: String = "DefaultScheme", + lastUpgradeVersion: String = "1.3", + version: String = "1.3", + buildAction: BuildAction? = nil, + testAction: TestAction? = nil, + launchAction: LaunchAction? = nil, + archiveAction: ArchiveAction? = nil, + profileAction: ProfileAction? = nil, + analyzeAction: AnalyzeAction? = nil, + wasCreatedForAppExtension: Bool? = nil + ) -> XCScheme { + XCScheme( + name: name, + lastUpgradeVersion: lastUpgradeVersion, + version: version, + buildAction: buildAction, + testAction: testAction, + launchAction: launchAction, + profileAction: profileAction, + analyzeAction: analyzeAction, + archiveAction: archiveAction, + wasCreatedForAppExtension: wasCreatedForAppExtension + ) + } +} diff --git a/Tests/XcodeProjMapperTests/TestData/XCSchemeTestableReference+TestData.swift b/Tests/XcodeProjMapperTests/TestData/XCSchemeTestableReference+TestData.swift new file mode 100644 index 00000000..6c62c91a --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/XCSchemeTestableReference+TestData.swift @@ -0,0 +1,28 @@ +import Foundation +import Path +import XcodeGraph +@testable import XcodeProj + +extension XCScheme.TestableReference { + static func test( + skipped: Bool, + parallelization: XCScheme.TestParallelization = .none, + randomExecutionOrdering: Bool = false, + buildableReference: XCScheme.BuildableReference, + locationScenarioReference: XCScheme.LocationScenarioReference? = nil, + skippedTests: [XCScheme.TestItem] = [], + selectedTests: [XCScheme.TestItem] = [], + useTestSelectionWhitelist: Bool? = nil + ) -> XCScheme.TestableReference { + XCScheme.TestableReference( + skipped: skipped, + parallelization: parallelization, + randomExecutionOrdering: randomExecutionOrdering, + buildableReference: buildableReference, + locationScenarioReference: locationScenarioReference, + skippedTests: skippedTests, + selectedTests: selectedTests, + useTestSelectionWhitelist: useTestSelectionWhitelist + ) + } +} diff --git a/Tests/XcodeProjMapperTests/TestData/XCUserData+TestData.swift b/Tests/XcodeProjMapperTests/TestData/XCUserData+TestData.swift new file mode 100644 index 00000000..863352c5 --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/XCUserData+TestData.swift @@ -0,0 +1,27 @@ +import XcodeProj + +// Tests? +extension XCUserData { + static func test( + userName: String = "user", + schemes: [XCScheme] = [], + schemeManagement: XCSchemeManagement? = XCSchemeManagement( + schemeUserState: [ + XCSchemeManagement.UserStateScheme( + name: "App.xcscheme", + shared: true, + orderHint: 0, + isShown: true + ), + ], + suppressBuildableAutocreation: nil + ) + ) -> XCUserData { + XCUserData( + userName: userName, + schemes: schemes, + breakpoints: nil, + schemeManagement: schemeManagement + ) + } +} diff --git a/Tests/XcodeProjMapperTests/TestData/XCVersionGroup+TestData.swift b/Tests/XcodeProjMapperTests/TestData/XCVersionGroup+TestData.swift new file mode 100644 index 00000000..46391d35 --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/XCVersionGroup+TestData.swift @@ -0,0 +1,34 @@ +import XcodeProj + +extension XCVersionGroup { + static func test( + currentVersion: PBXFileReference? = nil, + children: [PBXFileElement] = [], + path: String = "DefaultGroup", + sourceTree: PBXSourceTree = .group, + versionGroupType: String? = nil, + name: String? = nil, + includeInIndex: Bool? = nil, + wrapsLines: Bool? = nil, + usesTabs: Bool? = nil, + indentWidth: UInt? = nil, + tabWidth: UInt? = nil, + pbxProj _: PBXProj + ) -> XCVersionGroup { + let group = XCVersionGroup( + currentVersion: currentVersion, + path: path, + name: name, + sourceTree: sourceTree, + versionGroupType: versionGroupType, + includeInIndex: includeInIndex, + wrapsLines: wrapsLines, + usesTabs: usesTabs, + indentWidth: indentWidth, + tabWidth: tabWidth + ) + + group.children = children + return group + } +} diff --git a/Tests/XcodeProjMapperTests/TestData/XCWorkspace+TestData.swift b/Tests/XcodeProjMapperTests/TestData/XCWorkspace+TestData.swift new file mode 100644 index 00000000..a334247e --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/XCWorkspace+TestData.swift @@ -0,0 +1,22 @@ +import XcodeProj + +extension XCWorkspace { + static func test( + files: [String] = [ + "App/MainApp.xcodeproj", + "Framework1/Framework1.xcodeproj", + "StaticFramework1/StaticFramework1.xcodeproj", + ], + path: String + ) -> XCWorkspace { + let children = files.map { path in + XCWorkspaceDataElement.file(XCWorkspaceDataFileRef(location: .group(path))) + } + return XCWorkspace(data: XCWorkspaceData(children: children), path: .init(path)) + } + + static func test(withElements elements: [XCWorkspaceDataElement], path: String) -> XCWorkspace { + let data = XCWorkspaceData(children: elements) + return XCWorkspace(data: data, path: .init(path)) + } +} diff --git a/Tests/XcodeProjMapperTests/TestData/XCWorkspaceDataElement+TestData.swift b/Tests/XcodeProjMapperTests/TestData/XCWorkspaceDataElement+TestData.swift new file mode 100644 index 00000000..e01821fd --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/XCWorkspaceDataElement+TestData.swift @@ -0,0 +1,7 @@ +import XcodeProj + +extension XCWorkspaceDataElement { + static func test(relativePath: String) -> XCWorkspaceDataElement { + .file(XCWorkspaceDataFileRef(location: .group(relativePath))) + } +} diff --git a/Tests/XcodeProjMapperTests/TestData/XCWorkspaceDataGroup+TestData.swift b/Tests/XcodeProjMapperTests/TestData/XCWorkspaceDataGroup+TestData.swift new file mode 100644 index 00000000..8414f008 --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/XCWorkspaceDataGroup+TestData.swift @@ -0,0 +1,9 @@ +import XcodeProj + +import XcodeProj + +extension XCWorkspaceDataElement { + static func test(name: String, children: [XCWorkspaceDataElement]) -> XCWorkspaceDataElement { + .group(XCWorkspaceDataGroup(location: .group(name), name: name, children: children)) + } +} diff --git a/Tests/XcodeProjMapperTests/TestData/XcodeProj+TestData.swift b/Tests/XcodeProjMapperTests/TestData/XcodeProj+TestData.swift new file mode 100644 index 00000000..ff173781 --- /dev/null +++ b/Tests/XcodeProjMapperTests/TestData/XcodeProj+TestData.swift @@ -0,0 +1,58 @@ +import FileSystem +import Foundation +import Path +import XcodeGraph +import XcodeProj + +extension XcodeProj { + static func test( + projectName: String = "TestProject", + configurationList: XCConfigurationList = XCConfigurationList.test( + buildConfigurations: [.testDebug(), .testRelease()] + ), + targets: [PBXTarget] = [], + pbxProj: PBXProj = PBXProj() + ) async throws -> XcodeProj { + pbxProj.add(object: configurationList) + for config in configurationList.buildConfigurations { + pbxProj.add(object: config) + } + + let sourceDirectory = try await FileSystem().makeTemporaryDirectory(prefix: "test") + + // Minimal project setup: + let mainGroup = PBXGroup.test( + children: [], + sourceTree: .group, + name: "MainGroup", + path: "/tmp/TestProject" + ).add(to: pbxProj) + + let projectRef = PBXFileReference + .test(name: "App", path: "App.xcodeproj") + .add(to: pbxProj) + mainGroup.children.append(projectRef) + + let projects = [ + ["B900DB68213936CC004AEC3E": projectRef], + ] + + let pbxProject = PBXProject.test( + name: projectName, + buildConfigurationList: configurationList, + mainGroup: mainGroup, + projects: projects, + targets: targets + ).add(to: pbxProj) + + pbxProject.mainGroup = mainGroup + pbxProj.add(object: pbxProject) + pbxProj.rootObject = pbxProject + + return XcodeProj( + workspace: XCWorkspace(), + pbxproj: pbxProj, + path: .init("\(sourceDirectory)/\(projectName).xcodeproj") + ) + } +} diff --git a/Tuist.swift b/Tuist.swift index 3aca8a77..537bccfc 100644 --- a/Tuist.swift +++ b/Tuist.swift @@ -6,5 +6,5 @@ let config = Config( url: "https://cloud.tuist.io", options: [.optional] ), - swiftVersion: .init("5.9") + swiftVersion: .init("5.10") ) diff --git a/Tuist/ProjectDescriptionHelpers/Module.swift b/Tuist/ProjectDescriptionHelpers/Module.swift index c6c1c37e..12a1e344 100644 --- a/Tuist/ProjectDescriptionHelpers/Module.swift +++ b/Tuist/ProjectDescriptionHelpers/Module.swift @@ -3,6 +3,8 @@ import ProjectDescription public enum Module: String, CaseIterable { case xcodeGraph = "XcodeGraph" + case xcodeProjMapper = "XcodeProjMapper" + case xcodeMetadata = "XcodeMetadata" public var isRunnable: Bool { switch self { @@ -78,14 +80,14 @@ public enum Module: String, CaseIterable { public var unitTestsTargetName: String? { switch self { - default: + case .xcodeGraph, .xcodeProjMapper, .xcodeMetadata: return "\(rawValue)Tests" } } public var integrationTestsTargetName: String? { switch self { - case .xcodeGraph: + case .xcodeGraph, .xcodeProjMapper, .xcodeMetadata: return nil } } @@ -123,13 +125,27 @@ public enum Module: String, CaseIterable { .external(name: "AnyCodable"), .external(name: "Path"), ] + case .xcodeProjMapper: + [ + .target(name: Module.xcodeGraph.rawValue), + .target(name: Module.xcodeMetadata.rawValue), + .external(name: "Path"), + .external(name: "Command"), + .external(name: "XcodeProj"), + ] + case .xcodeMetadata: + [ + .external(name: "FileSystem"), + .external(name: "Mockable"), + .external(name: "ServiceContextModule"), + ] } return dependencies } public var unitTestDependencies: [TargetDependency] { var dependencies: [TargetDependency] = switch self { - case .xcodeGraph: + case .xcodeGraph, .xcodeMetadata, .xcodeProjMapper: [ ] } @@ -139,7 +155,7 @@ public enum Module: String, CaseIterable { public var testingDependencies: [TargetDependency] { let dependencies: [TargetDependency] = switch self { - case .xcodeGraph: + case .xcodeGraph, .xcodeProjMapper, .xcodeMetadata: [ ] } @@ -148,7 +164,7 @@ public enum Module: String, CaseIterable { public var integrationTestsDependencies: [TargetDependency] { var dependencies: [TargetDependency] = switch self { - case .xcodeGraph: + case .xcodeGraph, .xcodeProjMapper, .xcodeMetadata: [] } dependencies.append(.target(name: targetName)) @@ -167,6 +183,10 @@ public enum Module: String, CaseIterable { default: rootFolder = "Sources" } + let resources: ResourceFileElements = switch self { + case .xcodeGraph, .xcodeProjMapper, .xcodeMetadata: + [] + } var debugSettings: ProjectDescription.SettingsDictionary = ["SWIFT_ACTIVE_COMPILATION_CONDITIONS": "$(inherited) MOCKING"] var releaseSettings: ProjectDescription.SettingsDictionary = [:] @@ -194,9 +214,9 @@ public enum Module: String, CaseIterable { destinations: [.mac], product: product, bundleId: "io.tuist.\(name)", - deploymentTargets: .macOS("12.0"), + deploymentTargets: .macOS("13.0"), infoPlist: .default, - sources: ["\(rootFolder)/\(name)/**/*.swift"], + sources: [.glob("\(rootFolder)/\(name)/**/*.swift", excluding: ["**/Fixtures/**"])], dependencies: dependencies, settings: settings )