Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
NinjaLikesCheez committed Jul 27, 2023
1 parent abac5df commit 338122a
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 177 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// Collection+Extensions.swift
//
//
// Created by Thomas Hedderwick on 30/03/2023.
//

import Foundation

// Largely based off the Collection.map impl in Swift
public extension Collection {
func asyncMap<T>(_ transform: (Element) async throws -> T) async rethrows -> [T] {
let count = self.count
if count == 0 {
return []
}

var result = [T]()
result.reserveCapacity(count)

for element in self {
try await result.append(transform(element))
}

return result
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// DecodingExtensions.swift
//
//
//
// Created by Thomas Hedderwick on 03/02/2023.
//
Expand Down
2 changes: 1 addition & 1 deletion PBXProjParser/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ 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(url: "https://github.com/apple/swift-package-manager.git", branch: "release/5.7"),
.package(url: "https://github.com/apple/swift-package-manager.git", branch: "release/5.8"),
.package(path: "../GenIRLogging"),
.package(path: "../GenIRExtensions")
],
Expand Down
234 changes: 101 additions & 133 deletions PBXProjParser/Sources/PBXProjParser/PackageParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import GenIRExtensions
import Workspace
import PackageGraph
import TSCBasic
import PackageModel
import Basics

/// Parses SPM packages for a given Xcode Project
Expand All @@ -23,9 +24,11 @@ struct PackageParser {

// private var packages = [XCSwiftPackageProductDependency]()
private let packageCheckoutPath: URL
private let localPackagePaths: [URL]
private let remotePackagePaths: [URL]

private let remotePackages: [String: URL]
private let packageFiles: [String: URL]
// private let remotePackages: [String: URL]
// private let packageFiles: [String: URL]

enum Error: Swift.Error {
case xcodebuildError(String)
Expand All @@ -38,18 +41,18 @@ struct PackageParser {
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)
localPackagePaths = model.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")
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)
Expand All @@ -58,9 +61,9 @@ struct PackageParser {
.appendingPath(component: "SourcePackages", isDirectory: true)
.appendingPath(component: "checkouts", isDirectory: true)

let remotePackagePaths = try FileManager.default.directories(at: packageCheckoutPath)
remotePackagePaths = try FileManager.default.directories(at: packageCheckoutPath)
.filter { path in
return FileManager.default.fileExists(atPath: path.appendingPath(component: "Package.swift").filePath)
FileManager.default.fileExists(atPath: path.appendingPath(component: "Package.swift").filePath)
}

/* TODO: Notes
Expand All @@ -69,151 +72,116 @@ struct PackageParser {
.... 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) }
func parse() async throws {
let locals = try await localPackagePaths.asyncMap { try await PackageParser.parsePackageFile($0) }
let remotes = try await remotePackagePaths.asyncMap { try await PackageParser.parsePackageFile($0) }
}

private static func parsePackageFile(_ path: URL) throws -> SwiftPackage? {
private static func parsePackageFile(_ path: URL) async 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 manifests = try await manifests(for: path)
print(manifests)



// 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)")
manifests.forEach { (path, manifest) in
print(manifest.products)
}

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<String>()) { $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)")
}
private static func manifests(for path: URL) async throws -> [URL: Manifest] {
let workspace = try Workspace(forRootPackage: try .init(validating: path.filePath))
let root = PackageGraphRootInput(packages: [try .init(validating: path.filePath)])
let observabilityHandler = SPMObservabilityHandler(level: .info)
let observabilitySystem = ObservabilitySystem(observabilityHandler)
let observabilityScope = observabilitySystem.topScope

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 try await withCheckedThrowingContinuation { continuation in
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):
let returnValues = manifests.reduce(into: [URL: Manifest]()) { partialResult, pair in
partialResult[pair.key.pathString.fileURL] = pair.value
}

continuation.resume(returning: returnValues)
case .failure(let error):
continuation.resume(throwing: error)
}
}
)
}

return .init(path: projectPath.appendingPath(component: path).absoluteURL, type: .local, reference: dependency)
}

// 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<String>()) { $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:
Expand Down
6 changes: 3 additions & 3 deletions PBXProjParser/Sources/PBXProjParser/ProjectParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,16 @@ public struct ProjectParser {
case invalidPath(String)
}

public init(path: URL, logLevel level: Logger.Level) throws {
public init(path: URL, logLevel level: Logger.Level) async throws {
self.path = path
logger.logLevel = level

switch path.pathExtension {
case "xcodeproj":
let project = try XcodeProject(path: path)
let project = try await XcodeProject(path: path)
type = .project(project)
case "xcworkspace":
let workspace = try XcodeWorkspace(path: path)
let workspace = try await XcodeWorkspace(path: path)
type = .workspace(workspace)
default:
throw Error.invalidPath("Path should be a xcodeproj or xcworkspace, got: \(path.lastPathComponent)")
Expand Down
4 changes: 2 additions & 2 deletions PBXProjParser/Sources/PBXProjParser/XcodeProject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public struct XcodeProject {
case invalidPBXProj(String)
}

public init(path: URL) throws {
public init(path: URL) async throws {
self.path = path
model = try PBXProj.contentsOf(path.appendingPath(component: "project.pbxproj"))
project = try model.project()
Expand All @@ -53,7 +53,7 @@ public struct XcodeProject {
*/

let packageParser = try PackageParser(projectPath: path, model: model)
try packageParser.parse()
try await packageParser.parse()

packages = model.objects(of: .swiftPackageProductDependency, as: XCSwiftPackageProductDependency.self)

Expand Down
4 changes: 2 additions & 2 deletions PBXProjParser/Sources/PBXProjParser/XcodeWorkspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class XcodeWorkspace {
/// A mapping of targets to the projects that define them
let targetsToProject: [String: XcodeProject]

init(path: URL) throws {
init(path: URL) async throws {
self.path = path

// Parse the `contents.xcworkspacedata` (XML) file and get the list of projects
Expand All @@ -32,7 +32,7 @@ class XcodeWorkspace {
projectPaths = parser.projects
.map { baseFolder.appendingPath(component: $0, isDirectory: true) }

projects = try projectPaths.map(XcodeProject.init(path:))
projects = try await projectPaths.asyncMap(XcodeProject.init(path:))

targetsToProject = projects.reduce(into: [String: XcodeProject](), { partialResult, project in
project.targets.forEach { (target) in
Expand Down
Loading

0 comments on commit 338122a

Please sign in to comment.