From abac5dfb60dd8b5e075a994e381dcbb666b8d714 Mon Sep 17 00:00:00 2001 From: thedderwick Date: Thu, 23 Mar 2023 09:50:38 +0100 Subject: [PATCH] WIP --- .swiftlint.yml | 7 +- GenIRExtensions/.gitignore | 9 + GenIRExtensions/Package.swift | 29 ++ GenIRExtensions/README.md | 3 + .../GenIRExtensions}/DecodingExtensions.swift | 4 +- .../FileManager+Extension.swift | 12 +- .../GenIRExtensions}/Process+Extension.swift | 8 +- .../GenIRExtensions}/String+Extension.swift | 4 +- .../GenIRExtensions/URL+Extensions.swift | 23 ++ .../GenIRExtensionsTests.swift | 2 + GenIRLogging/Package.swift | 8 +- .../GenIRLoggingTests/GenIRLoggingTests.swift | 9 - PBXProjParser/Package.resolved | 81 +++++ PBXProjParser/Package.swift | 8 +- .../Models/PBXFileReference.swift | 12 +- .../Sources/PBXProjParser/PackageParser.swift | 309 ++++++++++++++++++ .../Sources/PBXProjParser/XcodeProject.swift | 19 +- .../PBXProjParser/XcodeWorkspace.swift | 4 +- Package.resolved | 72 ++++ Package.swift | 6 +- Sources/GenIR/CompilerCommandRunner.swift | 10 +- Sources/GenIR/Extensions/URL+Extensions.swift | 15 - Sources/GenIR/GenIR.swift | 3 +- Sources/GenIR/OutputPostprocessor.swift | 7 +- Sources/GenIR/XcodeLogParser.swift | 2 + 25 files changed, 600 insertions(+), 66 deletions(-) create mode 100644 GenIRExtensions/.gitignore create mode 100644 GenIRExtensions/Package.swift create mode 100644 GenIRExtensions/README.md rename {PBXProjParser/Sources/PBXProjParser/Extensions => GenIRExtensions/Sources/GenIRExtensions}/DecodingExtensions.swift (97%) rename {Sources/GenIR/Extensions => GenIRExtensions/Sources/GenIRExtensions}/FileManager+Extension.swift (94%) rename {Sources/GenIR/Extensions => GenIRExtensions/Sources/GenIRExtensions}/Process+Extension.swift (96%) rename {Sources/GenIR/Extensions => GenIRExtensions/Sources/GenIRExtensions}/String+Extension.swift (98%) create mode 100644 GenIRExtensions/Sources/GenIRExtensions/URL+Extensions.swift create mode 100644 GenIRExtensions/Tests/GenIRExtensionsTests/GenIRExtensionsTests.swift create mode 100644 PBXProjParser/Sources/PBXProjParser/PackageParser.swift delete mode 100644 Sources/GenIR/Extensions/URL+Extensions.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index addd3ae..dc99f3d 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -4,7 +4,10 @@ excluded: - .vscode/ - PBXProjParser/.build/ # https://github.com/realm/SwiftLint/issues/2329 doesn't support recursive globs yet - GenIRLogging/.build/ + - GenIRExtensions/.build/ line_length: - warning: 150 - ignores_comments: true + ignores_function_declarations: True + ignores_comments: True + warning: 200 + error: 250 diff --git a/GenIRExtensions/.gitignore b/GenIRExtensions/.gitignore new file mode 100644 index 0000000..3b29812 --- /dev/null +++ b/GenIRExtensions/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/GenIRExtensions/Package.swift b/GenIRExtensions/Package.swift new file mode 100644 index 0000000..d9a1914 --- /dev/null +++ b/GenIRExtensions/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "GenIRExtensions", + platforms: [.macOS(.v12)], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "GenIRExtensions", + targets: ["GenIRExtensions"]) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "GenIRExtensions", + dependencies: []), + .testTarget( + name: "GenIRExtensionsTests", + dependencies: ["GenIRExtensions"]) + ] +) diff --git a/GenIRExtensions/README.md b/GenIRExtensions/README.md new file mode 100644 index 0000000..01c0534 --- /dev/null +++ b/GenIRExtensions/README.md @@ -0,0 +1,3 @@ +# GenIRExtensions + +A description of this package. diff --git a/PBXProjParser/Sources/PBXProjParser/Extensions/DecodingExtensions.swift b/GenIRExtensions/Sources/GenIRExtensions/DecodingExtensions.swift similarity index 97% rename from PBXProjParser/Sources/PBXProjParser/Extensions/DecodingExtensions.swift rename to GenIRExtensions/Sources/GenIRExtensions/DecodingExtensions.swift index a9ed543..62fb12b 100644 --- a/PBXProjParser/Sources/PBXProjParser/Extensions/DecodingExtensions.swift +++ b/GenIRExtensions/Sources/GenIRExtensions/DecodingExtensions.swift @@ -24,7 +24,7 @@ struct PlistCodingKeys: CodingKey { } } -extension KeyedDecodingContainer { +public extension KeyedDecodingContainer { func decode(_ type: [String: Any].Type, forKey key: K) throws -> [String: Any] { let container = try self.nestedContainer(keyedBy: PlistCodingKeys.self, forKey: key) return try container.decode(type) @@ -69,7 +69,7 @@ extension KeyedDecodingContainer { } } -extension UnkeyedDecodingContainer { +public extension UnkeyedDecodingContainer { mutating func decode(_ type: [Any].Type) throws -> [Any] { var array: [Any] = [] diff --git a/Sources/GenIR/Extensions/FileManager+Extension.swift b/GenIRExtensions/Sources/GenIRExtensions/FileManager+Extension.swift similarity index 94% rename from Sources/GenIR/Extensions/FileManager+Extension.swift rename to GenIRExtensions/Sources/GenIRExtensions/FileManager+Extension.swift index 5f1a4b8..8d644b7 100644 --- a/Sources/GenIR/Extensions/FileManager+Extension.swift +++ b/GenIRExtensions/Sources/GenIRExtensions/FileManager+Extension.swift @@ -7,7 +7,7 @@ import Foundation -extension FileManager { +public extension FileManager { /// Returns a Boolean value that indicates whether a directory exists at the specified url /// - Parameter url: The url of the directory. This is tilde expanded /// - Returns: true if a directory exists at the specified path exists, or false if it doesn't exist or it does exist, but is not a directory @@ -60,7 +60,7 @@ extension FileManager { /// - path: The path of the directory to search in /// - suffix: The suffix to match against file names /// - recursive: A Boolean value to indicate whether a recursive search should be performed - /// - Returns: An array of URL file paths matching the suffix found in the specifed path + /// - Returns: An array of URL file paths matching the suffix found in the specified path func files(at path: URL, withSuffix suffix: String, recursive: Bool = true) throws -> [URL] { try filteredContents(of: path, recursive: recursive) { url in let attributes = try url.resourceValues(forKeys: [.isRegularFileKey]) @@ -111,7 +111,7 @@ extension FileManager { let sourceFiles = try contentsOfDirectory(at: source, includingPropertiesForKeys: nil) for sourceFile in sourceFiles { - let path = destination.appendingPathComponent(sourceFile.lastPathComponent) + let path = destination.appendingPath(component: sourceFile.lastPathComponent) if replacing && fileExists(atPath: path.filePath) { try removeItem(at: path) @@ -129,16 +129,16 @@ extension FileManager { /// - filename: the name of the file /// - Returns: a URL to a unique file in the given directory func uniqueFilename(directory: URL, filename: String) -> URL { - var path = directory.appendingPathComponent(filename) + var path = directory.appendingPath(component: filename) var index = 2 while fileExists(atPath: path.filePath) { let splitName = filename.split(separator: ".") if splitName.count == 2 { - path = directory.appendingPathComponent("\(splitName[0]) \(index).\(splitName[1])") + path = directory.appendingPath(component: "\(splitName[0]) \(index).\(splitName[1])") } else { - path = directory.appendingPathComponent("\(filename) \(index)") + path = directory.appendingPath(component: "\(filename) \(index)") } index += 1 diff --git a/Sources/GenIR/Extensions/Process+Extension.swift b/GenIRExtensions/Sources/GenIRExtensions/Process+Extension.swift similarity index 96% rename from Sources/GenIR/Extensions/Process+Extension.swift rename to GenIRExtensions/Sources/GenIRExtensions/Process+Extension.swift index a5df895..fb3df1b 100644 --- a/Sources/GenIR/Extensions/Process+Extension.swift +++ b/GenIRExtensions/Sources/GenIRExtensions/Process+Extension.swift @@ -7,15 +7,15 @@ import Foundation -extension Process { +public extension Process { /// Presents the result of a Process struct ReturnValue { /// The stdout output of the process, if there was any - let stdout: String? + public let stdout: String? /// The stderr output of the process, if there was any - let stderr: String? + public let stderr: String? /// The return code of the process - let code: Int32 + public let code: Int32 init(stdout: String?, stderr: String?, code: Int32) { if let stdout, stdout.isEmpty { diff --git a/Sources/GenIR/Extensions/String+Extension.swift b/GenIRExtensions/Sources/GenIRExtensions/String+Extension.swift similarity index 98% rename from Sources/GenIR/Extensions/String+Extension.swift rename to GenIRExtensions/Sources/GenIRExtensions/String+Extension.swift index 1eb1774..b88f866 100644 --- a/Sources/GenIR/Extensions/String+Extension.swift +++ b/GenIRExtensions/Sources/GenIRExtensions/String+Extension.swift @@ -7,7 +7,7 @@ import Foundation -extension String { +public extension String { /// Replacing double escapes with singles /// - Returns: the unescaped string func unescaped() -> String { @@ -105,7 +105,7 @@ extension Substring { } } -extension [String] { +public extension [String] { /// Finds the next index of a given item after a given index /// - Parameters: /// - item: the element to search for diff --git a/GenIRExtensions/Sources/GenIRExtensions/URL+Extensions.swift b/GenIRExtensions/Sources/GenIRExtensions/URL+Extensions.swift new file mode 100644 index 0000000..3400f69 --- /dev/null +++ b/GenIRExtensions/Sources/GenIRExtensions/URL+Extensions.swift @@ -0,0 +1,23 @@ +// +// URL+Extension.swift +// +// +// Created by Thomas Hedderwick on 02/08/2022. +// + +import Foundation + +public extension URL { + /// Returns the path component of a URL + var filePath: String { + return self.path + } + + func appendingPath(component: String, isDirectory: Bool = false) -> URL { + if #available(macOS 13.0, *) { + return self.appending(component: component, directoryHint: isDirectory ? .isDirectory : .inferFromPath) + } else { + return self.appendingPathComponent(component, isDirectory: isDirectory) + } + } +} diff --git a/GenIRExtensions/Tests/GenIRExtensionsTests/GenIRExtensionsTests.swift b/GenIRExtensions/Tests/GenIRExtensionsTests/GenIRExtensionsTests.swift new file mode 100644 index 0000000..cde9d4c --- /dev/null +++ b/GenIRExtensions/Tests/GenIRExtensionsTests/GenIRExtensionsTests.swift @@ -0,0 +1,2 @@ +import XCTest +@testable import GenIRExtensions diff --git a/GenIRLogging/Package.swift b/GenIRLogging/Package.swift index 3f9844b..8a5bcb6 100644 --- a/GenIRLogging/Package.swift +++ b/GenIRLogging/Package.swift @@ -9,12 +9,12 @@ let package = Package( // Products define the executables and libraries a package produces, and make them visible to other packages. .library( name: "GenIRLogging", - targets: ["GenIRLogging"]), + targets: ["GenIRLogging"]) ], dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), - .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -22,10 +22,10 @@ let package = Package( .target( name: "GenIRLogging", dependencies: [ - .product(name: "Logging", package: "swift-log"), + .product(name: "Logging", package: "swift-log") ]), .testTarget( name: "GenIRLoggingTests", - dependencies: ["GenIRLogging"]), + dependencies: ["GenIRLogging"]) ] ) diff --git a/GenIRLogging/Tests/GenIRLoggingTests/GenIRLoggingTests.swift b/GenIRLogging/Tests/GenIRLoggingTests/GenIRLoggingTests.swift index aca6a60..87e867a 100644 --- a/GenIRLogging/Tests/GenIRLoggingTests/GenIRLoggingTests.swift +++ b/GenIRLogging/Tests/GenIRLoggingTests/GenIRLoggingTests.swift @@ -1,11 +1,2 @@ import XCTest @testable import GenIRLogging - -//final class GenIRLoggingTests: XCTestCase { -// func testExample() throws { -// // This is an example of a functional test case. -// // Use XCTAssert and related functions to verify your tests produce the correct -// // results. -// XCTAssertEqual(GenIRLogging().text, "Hello, World!") -// } -//} diff --git a/PBXProjParser/Package.resolved b/PBXProjParser/Package.resolved index 40da984..6eff98d 100644 --- a/PBXProjParser/Package.resolved +++ b/PBXProjParser/Package.resolved @@ -1,5 +1,50 @@ { "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "e394bf350e38cb100b6bc4172834770ede1b7232", + "version" : "1.0.3" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version" : "1.0.4" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "ddb07e896a2a8af79512543b1c7eb9797f8898a5", + "version" : "1.1.7" + } + }, + { + "identity" : "swift-driver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-driver.git", + "state" : { + "branch" : "release/5.7", + "revision" : "82b274af66cfbb8f3131677676517b34d01e30fd" + } + }, + { + "identity" : "swift-llbuild", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-llbuild.git", + "state" : { + "branch" : "release/5.7", + "revision" : "564424db5fdb62dcb5d863bdf7212500ef03a87b" + } + }, { "identity" : "swift-log", "kind" : "remoteSourceControl", @@ -8,6 +53,42 @@ "revision" : "32e8d724467f8fe623624570367e3d50c5638e46", "version" : "1.5.2" } + }, + { + "identity" : "swift-package-manager", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-package-manager.git", + "state" : { + "branch" : "release/5.7", + "revision" : "c6e40adbfc78acc60ca464ae482b56442f9f34f4" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "836bc4557b74fe6d2660218d56e3ce96aff76574", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-tools-support-core", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-tools-support-core.git", + "state" : { + "branch" : "release/5.7", + "revision" : "286b48b1d73388e1d49b2bb33aabf995838104e3" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams.git", + "state" : { + "revision" : "9ff1cc9327586db4e0c8f46f064b6a82ec1566fa", + "version" : "4.0.6" + } } ], "version" : 2 diff --git a/PBXProjParser/Package.swift b/PBXProjParser/Package.swift index 46f6736..96c8cad 100644 --- a/PBXProjParser/Package.swift +++ b/PBXProjParser/Package.swift @@ -16,7 +16,9 @@ let package = Package( // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), - .package(path: "../GenIRLogging") + .package(url: "https://github.com/apple/swift-package-manager.git", branch: "release/5.7"), + .package(path: "../GenIRLogging"), + .package(path: "../GenIRExtensions") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -25,7 +27,9 @@ let package = Package( name: "PBXProjParser", dependencies: [ .product(name: "Logging", package: "swift-log"), - .product(name: "GenIRLogging", package: "GenIRLogging") + .product(name: "SwiftPMDataModel-auto", package: "swift-package-manager"), + .product(name: "GenIRLogging", package: "GenIRLogging"), + .product(name: "GenIRExtensions", package: "GenIRExtensions") ]), .testTarget( name: "PBXProjParserTests", diff --git a/PBXProjParser/Sources/PBXProjParser/Models/PBXFileReference.swift b/PBXProjParser/Sources/PBXProjParser/Models/PBXFileReference.swift index 1f3385c..fee93f4 100644 --- a/PBXProjParser/Sources/PBXProjParser/Models/PBXFileReference.swift +++ b/PBXProjParser/Sources/PBXProjParser/Models/PBXFileReference.swift @@ -12,10 +12,10 @@ public class PBXFileReference: PBXObject { public let fileEncoding: String? public let explicitFileType: String? public let includeInIndex: String? - public let lastKnownFileType: String? - public let name: String? public let sourceTree: String #endif + public let name: String? + public let lastKnownFileType: String? public let path: String private enum CodingKeys: String, CodingKey { @@ -23,10 +23,10 @@ public class PBXFileReference: PBXObject { case fileEncoding case explicitFileType case includeInIndex - case lastKnownFileType - case name case sourceTree #endif + case lastKnownFileType + case name case path } @@ -36,10 +36,10 @@ public class PBXFileReference: PBXObject { fileEncoding = try container.decodeIfPresent(String.self, forKey: .fileEncoding) explicitFileType = try container.decodeIfPresent(String.self, forKey: .explicitFileType) includeInIndex = try container.decodeIfPresent(String.self, forKey: .includeInIndex) - lastKnownFileType = try container.decodeIfPresent(String.self, forKey: .lastKnownFileType) - name = try container.decodeIfPresent(String.self, forKey: .name) sourceTree = try container.decode(String.self, forKey: .sourceTree) #endif + lastKnownFileType = try container.decodeIfPresent(String.self, forKey: .lastKnownFileType) + name = try container.decodeIfPresent(String.self, forKey: .name) path = try container.decode(String.self, forKey: .path) try super.init(from: decoder) diff --git a/PBXProjParser/Sources/PBXProjParser/PackageParser.swift b/PBXProjParser/Sources/PBXProjParser/PackageParser.swift new file mode 100644 index 0000000..790d477 --- /dev/null +++ b/PBXProjParser/Sources/PBXProjParser/PackageParser.swift @@ -0,0 +1,309 @@ +// +// PackageParser.swift +// +// +// Created by Thomas Hedderwick on 22/03/2023. +// + +import Foundation +import GenIRExtensions + +// SPM imports +import Workspace +import PackageGraph +import TSCBasic +import Basics + +/// Parses SPM packages for a given Xcode Project +struct PackageParser { + /// Path to the Xcode Project to parse SPM packages for + private let projectPath: URL + /// Model of the pbxproj file for this Xcode Project + private let project: PBXProj + + // private var packages = [XCSwiftPackageProductDependency]() + private let packageCheckoutPath: URL + + private let remotePackages: [String: URL] + private let packageFiles: [String: URL] + + enum Error: Swift.Error { + case xcodebuildError(String) + case swiftPackageError(String) + case resourceNotFound(String) + } + + init(projectPath: URL, model: PBXProj) throws { + self.projectPath = projectPath + self.project = model + + // Attempt to find a list of local packages - can't use the SPM dependency from pbxproj here as that includes the target name, not the package name + let localPackagePaths = project.objects(of: .fileReference, as: PBXFileReference.self) + .filter { $0.lastKnownFileType == "wrapper" } + .map { $0.path } + .filter { path in + let packagePath = projectPath.deletingLastPathComponent().appendingPath(component: path).appendingPath(component: "Package.swift") + return FileManager.default.fileExists(atPath: packagePath.filePath) + } + .map { $0.fileURL } + + // TODO: Test + try localPackagePaths.forEach { try PackageParser.parsePackageFile($0) } + + // Attempt to find a list of the remote packages, this involves finding the checkout location in 'derived data' + // which is two folders up from the build root, and then `SourcePackages/checkout/` + packageCheckoutPath = try PackageParser.fetchBuildRoot(for: projectPath) + .deletingLastPathComponent() + .deletingLastPathComponent() + .appendingPath(component: "SourcePackages", isDirectory: true) + .appendingPath(component: "checkouts", isDirectory: true) + + let remotePackagePaths = try FileManager.default.directories(at: packageCheckoutPath) + .filter { path in + return FileManager.default.fileExists(atPath: path.appendingPath(component: "Package.swift").filePath) + } + + /* TODO: Notes + * SPM Product Deps productName may not align with name of the package. (i.e. SecondLib vs MySecondLibrary) + .... so we might have to parse every PBXFileReference looking for wrappers that _might_ be SPM packages, as well as + .... all checkouts in derived data to get all the product names for a given package... + Essentially - parse all packages _first_ then determine what's local vs remote if that even matters at that point + */ + + + + + + + + + // Get a mapping of Package Name to URLs + remotePackages = try FileManager.default.directories(at: packageCheckoutPath, recursive: false) + .reduce(into: [String: URL]()) { partialResult, path in + partialResult[path.lastPathComponent] = path + } + + // Look inside the folder for Package.swift files - they should be at the top-level of the directory + packageFiles = remotePackages.reduce(into: [String: URL]()) { partialResult, pair in + let path = pair.value.appendingPath(component: "Package.swift") + if FileManager.default.fileExists(atPath: path.filePath) { + partialResult[pair.key] = path + } else { + logger.debug("Attempted to parse \(pair.key) but didn't find a Package.swift file at: \(path).") + } + } + } + + func parse() throws { + let packages = project.objects(of: .swiftPackageProductDependency, as: XCSwiftPackageProductDependency.self) + .compactMap { package(for: $0) } + } + + private static func parsePackageFile(_ path: URL) throws -> SwiftPackage? { + // TODO: Use SPM to parse out the manifest. + // Needed: + // * workspace -- SwiftTool.getActiveWorkspace() + // * root -- Swifttool.getWorkspaceRoot() + // * observability -- SwiftTool.observabilityScope + // Then: + // * workspace.loadRootManifests() + let rootPackage = path.deletingLastPathComponent() + let workspace = try Workspace(forRootPackage: try .init(validating: rootPackage.filePath)) + let root = PackageGraphRootInput(packages: [try .init(validating: rootPackage.filePath)]) + let observabilityHandler = SPMObservabilityHandler(level: .info) + let observabilitySystem = ObservabilitySystem(observabilityHandler) + let observabilityScope = observabilitySystem.topScope + + workspace.loadRootManifests( + packages: root.packages, + observabilityScope: observabilityScope, // TODO: Double check this.... + //(Result<[AbsolutePath : Manifest], Error>) -> Void + completion: { result in + switch result { + case .success(let manifests): + print(manifests) + manifests.values.forEach { manifest in + print("manifest: \(manifest)") + } + case .failure(let error): + print(error) + } + } + ) + + + + return nil + + + + + + // let result: Process.ReturnValue + + // do { + // result = try Process.runShell("/usr/bin/swift", arguments: ["package", "dump-package"], runInDirectory: path.deletingLastPathComponent()) + // } catch { + // throw Error.swiftPackageError("Failed to invoke swift package dump-package on package path: \(path)") + // } + + // guard result.code == 0, let stdout = result.stdout?.data(using: .utf8) else { + // throw Error.xcodebuildError("Failed to run swift package dump-package for project: \(path). Error: \(result.stdout ?? "nil"), \(result.stderr ?? "nil")") + // } + + // Decode JSON response + // let package = try JSONDecoder().decode(Manifest.self, from: stdout) + // return package + // return nil + } + + private func package(for dependency: XCSwiftPackageProductDependency) -> Package? { + if + let reference = dependency.package, + let object = project.object(forKey: reference, as: PBXObject.self), + object.isa == .remoteSwiftPackageReference + { + return remotePackage(for: dependency) + } else { + return localPackage(for: dependency) + } + } + + private func remotePackage(for dependency: XCSwiftPackageProductDependency) -> Package? { + // Remote packages are a little tricky - we need to get the Package.swift location for them, + // This normally resides in DerivedData in the products build folder. + + // If we have a dependency named the same as a remote package in the checkout folder, return it + if let url = remotePackages[dependency.productName] { + return .init(path: url, type: .remote, reference: dependency) + } + + // Now it's entirely possible we have a dependency who's name doesn't match a checkout - for example, a package may have one or more targets. + // In this case, we have to parse the Package.swift for each of these and determine the product names they contain. + return findPackage(for: dependency) + + // NOTE: We can do this with `swift package dump-package`, capturing the JSON, parsing the following paths: `products[].name` + // We can then use `targets.name` to match these to targets, and use `targets.dependencies` to get a list of target dependencies + } + + private func findPackage(for dependency: XCSwiftPackageProductDependency) -> Package? { + if let packageFile = packageFiles[dependency.productName] { + print("packageFile: \(packageFile)") + } + + return nil + } + + private func localPackage(for dependency: XCSwiftPackageProductDependency) -> Package? { + // For local packages, we want to use the `productName` to look up a File Reference with the same name, + // and get the path for it. This path will be relative to the xcode project we're operating on. + let paths = project.objects(of: .fileReference, as: PBXFileReference.self) + .filter { $0.name == dependency.productName } + .map { $0.path } + .reduce(into: Set()) { $0.insert($1) } + + if paths.count > 1 { + logger.warning("Expected 1 unique path for local package (\(dependency.productName)), got \(paths.count). Using \(paths.first!) from \(paths)") + } + + guard let path = paths.first else { + logger.error("Didn't find any paths for local package \(dependency.productName). Please report this error.") + return nil + } + + return .init(path: projectPath.appendingPath(component: path).absoluteURL, type: .local, reference: dependency) + } + + /// Determines the BUILD_ROOT of the project. + /// - Parameter projectPath: the project to determine the path for + /// - Returns: + private static func fetchBuildRoot(for projectPath: URL) throws -> URL { + let result: Foundation.Process.ReturnValue + + do { + result = try Process.runShell("/usr/bin/xcodebuild", arguments: ["-showBuildSettings", "-project", projectPath.filePath]) + } catch { + throw Error.xcodebuildError("xcodebuild process failure: \(error)") + } + + guard result.code == 0, let stdout = result.stdout else { + throw Error.xcodebuildError("Failed to run xcodebuild -showBuildSettings for project: \(projectPath). Error: \(result.stdout ?? "nil"), \(result.stderr ?? "nil")") + } + + // Parse the output looking for BUILD_ROOT, this will give us almost exactly the path that we want + guard let buildRoot = stdout.split(separator: "\n").first(where: { $0.contains("BUILD_ROOT = ") }) else { + throw Error.xcodebuildError("Failed to find BUILD_ROOT variable in Xcode Build settings. Output: \(stdout)") + } + + // Return the path part of the variable + return buildRoot + .trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: "BUILD_ROOT = ", with: "") + .fileURL + } +} + +/* Packages come in two variants: remote & local. + * + * - Remotes are checked-out into DerivedData where we can find their Package.swift. + * - In addition, there's a Package.resolved for _the project_ in the xcodeproj or xcworkspace folders + * - `*.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved` + * - `*.xcworkspace/xcshareddata/swiftpm/Package.resolved` + * - Determining how we can accurately find the derived data path will need to be investigated. + * - Locals are referenced _only_ by the pbxproj (as far as I can see), and there's no direct link between the XCSwiftPackageProductDependency & the filepath + * - However, we could use the product name to look up a PBXFileReference and get the path from there... This is relative to the project file path. + */ +struct Package { + enum ReferenceType { + case local + case remote + } + + /// The path **on disk** where the package resides. + /// For a local reference, this will be anywhere on disk. + /// For a remote reference, this will be in `DerivedData//SourcePackages/checkouts/` + let path: URL + + /// The type of the package + let type: ReferenceType + + /// The pbxproj object this package represents + let reference: XCSwiftPackageProductDependency + + init(path: URL, type: ReferenceType, reference: XCSwiftPackageProductDependency) { + self.path = path + self.type = type + self.reference = reference + } +} + +struct SwiftPackage: Codable { + let dependencies: [[String: [[String: String]]]] + let name: String + let products: [PackageProduct] +} + +struct PackageProduct: Codable { + let name: String + let targets: [String] +} + +public struct SPMObservabilityHandler: ObservabilityHandlerProvider { + private let handler: SPMOutputHandler + + public var diagnosticsHandler: DiagnosticsHandler { self.handler } + + init(level: Basics.Diagnostic.Severity) { + handler = .init(logLevel: level, outputStream: TSCBasic.stderrStream) + } +} + +public struct SPMOutputHandler: DiagnosticsHandler { + public func handleDiagnostic(scope: Basics.ObservabilityScope, diagnostic: Basics.Diagnostic) { + print("[swiftpm] \(diagnostic.message)") + } + + init(logLevel: Basics.Diagnostic.Severity, outputStream: ThreadSafeOutputByteStream) { + // TODO: lol + } +} diff --git a/PBXProjParser/Sources/PBXProjParser/XcodeProject.swift b/PBXProjParser/Sources/PBXProjParser/XcodeProject.swift index 34f7219..aa7e8a1 100644 --- a/PBXProjParser/Sources/PBXProjParser/XcodeProject.swift +++ b/PBXProjParser/Sources/PBXProjParser/XcodeProject.swift @@ -9,7 +9,7 @@ import Foundation /// Represents an xcodeproj bundle public struct XcodeProject { - /// Path to the Workspace + /// Path to the Xcode Project public let path: URL /// The underlying pbxproj model @@ -30,7 +30,7 @@ public struct XcodeProject { public init(path: URL) throws { self.path = path - model = try PBXProj.contentsOf(path.appendingPathComponent("project.pbxproj")) + model = try PBXProj.contentsOf(path.appendingPath(component: "project.pbxproj")) project = try model.project() targets = model.objects(for: project.targets) @@ -40,6 +40,21 @@ public struct XcodeProject { $0.productType != "com.apple.product-type.bundle" } + // TODO: Implement the following + /* Packages come in two variants: remote & local. + * + * - Remotes are checked-out into DerivedData where we can find their Package.swift. + * - In addition, there's a Package.resolved for _the project_ in the xcodeproj or xcworkspace folders + * - `*.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved` + * - `*.xcworkspace/xcshareddata/swiftpm/Package.resolved` + * - Determining how we can accurately find the derived data path will need to be investigated. + * - Locals are referenced _only_ by the pbxproj (as far as I can see), and there's no direct link between the XCSwiftPackageProductDependency & the filepath + * - However, we could use the product name to look up a PBXFileReference and get the path from there... This is relative to the project file path. + */ + + let packageParser = try PackageParser(projectPath: path, model: model) + try packageParser.parse() + packages = model.objects(of: .swiftPackageProductDependency, as: XCSwiftPackageProductDependency.self) // First pass - get all the direct dependencies diff --git a/PBXProjParser/Sources/PBXProjParser/XcodeWorkspace.swift b/PBXProjParser/Sources/PBXProjParser/XcodeWorkspace.swift index 40cfbd8..4e859a1 100644 --- a/PBXProjParser/Sources/PBXProjParser/XcodeWorkspace.swift +++ b/PBXProjParser/Sources/PBXProjParser/XcodeWorkspace.swift @@ -23,14 +23,14 @@ class XcodeWorkspace { self.path = path // Parse the `contents.xcworkspacedata` (XML) file and get the list of projects - let contentsPath = path.appendingPathComponent("contents.xcworkspacedata") + let contentsPath = path.appendingPath(component: "contents.xcworkspacedata") let data = try Data(contentsOf: contentsPath) let parser = XCWorkspaceDataParser(data: data) let baseFolder = path.deletingLastPathComponent() projectPaths = parser.projects - .map { baseFolder.appendingPathComponent($0, isDirectory: true) } + .map { baseFolder.appendingPath(component: $0, isDirectory: true) } projects = try projectPaths.map(XcodeProject.init(path:)) diff --git a/Package.resolved b/Package.resolved index e444e2b..d192212 100644 --- a/Package.resolved +++ b/Package.resolved @@ -9,6 +9,24 @@ "version" : "1.0.3" } }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version" : "1.0.4" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "ddb07e896a2a8af79512543b1c7eb9797f8898a5", + "version" : "1.1.7" + } + }, { "identity" : "swift-docc-plugin", "kind" : "remoteSourceControl", @@ -27,6 +45,24 @@ "version" : "1.0.0" } }, + { + "identity" : "swift-driver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-driver.git", + "state" : { + "branch" : "release/5.7", + "revision" : "82b274af66cfbb8f3131677676517b34d01e30fd" + } + }, + { + "identity" : "swift-llbuild", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-llbuild.git", + "state" : { + "branch" : "release/5.7", + "revision" : "564424db5fdb62dcb5d863bdf7212500ef03a87b" + } + }, { "identity" : "swift-log", "kind" : "remoteSourceControl", @@ -35,6 +71,42 @@ "revision" : "6fe203dc33195667ce1759bf0182975e4653ba1c", "version" : "1.4.4" } + }, + { + "identity" : "swift-package-manager", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-package-manager.git", + "state" : { + "branch" : "release/5.7", + "revision" : "c6e40adbfc78acc60ca464ae482b56442f9f34f4" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "836bc4557b74fe6d2660218d56e3ce96aff76574", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-tools-support-core", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-tools-support-core.git", + "state" : { + "branch" : "release/5.7", + "revision" : "286b48b1d73388e1d49b2bb33aabf995838104e3" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams.git", + "state" : { + "revision" : "9ff1cc9327586db4e0c8f46f064b6a82ec1566fa", + "version" : "4.0.6" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index c4615a5..49a3989 100644 --- a/Package.swift +++ b/Package.swift @@ -15,7 +15,8 @@ let package = Package( .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), .package(path: "PBXProjParser"), - .package(path: "GenIRLogging") + .package(path: "GenIRLogging"), + .package(path: "GenIRExtensions") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -26,7 +27,8 @@ let package = Package( .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "Logging", package: "swift-log"), .product(name: "PBXProjParser", package: "PBXProjParser"), - .product(name: "GenIRLogging", package: "GenIRLogging") + .product(name: "GenIRLogging", package: "GenIRLogging"), + .product(name: "GenIRExtensions", package: "GenIRExtensions") ], path: "Sources/GenIR" ), diff --git a/Sources/GenIR/CompilerCommandRunner.swift b/Sources/GenIR/CompilerCommandRunner.swift index bec6c29..ec599a8 100644 --- a/Sources/GenIR/CompilerCommandRunner.swift +++ b/Sources/GenIR/CompilerCommandRunner.swift @@ -8,6 +8,7 @@ import Foundation import Logging import PBXProjParser +import GenIRExtensions /// A model of the contents of an output file map json typealias OutputFileMap = [String: [String: String]] @@ -73,8 +74,9 @@ struct CompilerCommandRunner { /// - name: The name this command relates to, used to create the product folder /// - directory: The directory to run these commands in /// - Returns: The total amount of modules produced for this target - private func run(commands: [CompilerCommand], for name: String, at directory: URL) throws -> Int { - let targetDirectory = directory.appendingPathComponent(name) + private func run(commands: [CompilerCommand], for product: String, at directory: URL) throws -> Int { + let directoryName = product + let targetDirectory = directory.appendingPath(component: directoryName) try fileManager.createDirectory(at: targetDirectory, withIntermediateDirectories: true) logger.debug("Created target directory: \(targetDirectory)") @@ -252,7 +254,7 @@ extension CompilerCommandRunner { let bitcodePaths = bitcodeFiles(from: outputMap) for bitcodePath in bitcodePaths { - let destination = targetDirectory.appendingPathComponent(bitcodePath.lastPathComponent) + let destination = targetDirectory.appendingPath(component: bitcodePath.lastPathComponent) try fileManager.moveItemReplacingExisting(from: bitcodePath, to: destination) } @@ -305,7 +307,7 @@ extension CompilerCommandRunner { let files = try fileManager.files(at: source, withSuffix: ".s") for file in files { - let destinationPath = destination.appendingPathComponent( + let destinationPath = destination.appendingPath(component: file.lastPathComponent.replacingOccurrences(of: ".s", with: ".bc") ) diff --git a/Sources/GenIR/Extensions/URL+Extensions.swift b/Sources/GenIR/Extensions/URL+Extensions.swift deleted file mode 100644 index 77fd0e4..0000000 --- a/Sources/GenIR/Extensions/URL+Extensions.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// URL+Extension.swift -// -// -// Created by Thomas Hedderwick on 02/08/2022. -// - -import Foundation - -extension URL { - /// Returns the path component of a URL - var filePath: String { - return self.path - } -} diff --git a/Sources/GenIR/GenIR.swift b/Sources/GenIR/GenIR.swift index 2d1f13c..d5ae77d 100644 --- a/Sources/GenIR/GenIR.swift +++ b/Sources/GenIR/GenIR.swift @@ -3,6 +3,7 @@ import ArgumentParser import Logging import PBXProjParser import GenIRLogging +import GenIRExtensions /// Global logger object var logger = Logger(label: Bundle.main.bundleIdentifier ?? "com.veracode.gen-ir", factory: StdOutLogHandler.init) @@ -61,7 +62,7 @@ struct IREmitterCommand: ParsableCommand { var dryRun = false /// Path to write IR to - private lazy var outputPath: URL = xcarchivePath.appendingPathComponent("IR") + private lazy var outputPath: URL = xcarchivePath.appendingPath(component: "IR") mutating func validate() throws { if debug { diff --git a/Sources/GenIR/OutputPostprocessor.swift b/Sources/GenIR/OutputPostprocessor.swift index 74dfc48..2548161 100644 --- a/Sources/GenIR/OutputPostprocessor.swift +++ b/Sources/GenIR/OutputPostprocessor.swift @@ -7,6 +7,7 @@ import Foundation import PBXProjParser +import GenIRExtensions /// The `OutputPostprocessor` is responsible for trying to match the IR output of the `CompilerCommandRunner` with the products in the `xcarchive`. /// The `CompilerCommandRunner` will output IR with it's product name, but doesn't take into account the linking of products into each other. @@ -78,9 +79,9 @@ struct OutputPostprocessor { /// - Parameter path: the original path, should be an xcarchive /// - Returns: the path to start a dependency search from static func baseSearchPath(startingAt path: URL) -> URL { - let productsPath = path.appendingPathComponent("Products") - let applicationsPath = productsPath.appendingPathComponent("Applications") - let frameworkPath = productsPath.appendingPathComponent("Library").appendingPathComponent("Framework") + let productsPath = path.appendingPath(component: "Products") + let applicationsPath = productsPath.appendingPath(component: "Applications") + let frameworkPath = productsPath.appendingPath(component: "Library").appendingPath(component: "Framework") func firstDirectory(at path: URL) -> URL? { guard diff --git a/Sources/GenIR/XcodeLogParser.swift b/Sources/GenIR/XcodeLogParser.swift index 8a3368f..78ecfaf 100644 --- a/Sources/GenIR/XcodeLogParser.swift +++ b/Sources/GenIR/XcodeLogParser.swift @@ -7,6 +7,7 @@ import Foundation import Logging +import GenIRExtensions /// An XcodeLogParser extracts targets and their compiler commands from a given Xcode build log class XcodeLogParser { @@ -121,6 +122,7 @@ class XcodeLogParser { let slice = lines[startIndex..