Skip to content

Commit

Permalink
Allow multiple workspaces.
Browse files Browse the repository at this point in the history
Check modification time to grab the most recent one.
  • Loading branch information
NinjaLikesCheez committed May 24, 2024
1 parent 9d9a335 commit 9b86fed
Show file tree
Hide file tree
Showing 52 changed files with 3,683 additions and 40 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ on:

jobs:
build:
runs-on: macos-13
runs-on: macos-latest
steps:
- uses: actions/checkout@v3

- name: 🔨 Build
run: |
sudo xcode-select -s /Applications/Xcode_14.2.app/
sudo xcode-select -s /Applications/Xcode_15.4.app/
swift build
29 changes: 25 additions & 4 deletions PIF/Sources/PIFSupport/PIFSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class PIFParser {

public enum Error: Swift.Error {
case workspaceNotFound
case filesystemError(Swift.Error)
}

public init(cachePath: URL, logger log: Logger) throws {
Expand All @@ -25,13 +26,33 @@ public class PIFParser {
let workspaces = try FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: [.isRegularFileKey])
.filter { $0.lastPathComponent.starts(with: "WORKSPACE@") }

precondition(workspaces.count == 1, "Encountered more than one workspace - it is expected that a single workspace exists: \(workspaces)")

guard workspaces.count > 0 else {
guard !workspaces.isEmpty else {
throw Error.workspaceNotFound
}

return workspaces[0]
if workspaces.count == 1 {
return workspaces[0]
}

// If multiple workspaces exist, it's because the something in the project changed between builds. Sort workspaces by the most recent.
func modificationDate(_ path: URL) -> Date {
(try? FileManager.default.attributesOfItem(atPath: path.path)[.modificationDate] as? Date) ?? Date()
}

logger.debug("Found multiple workspaces, sorting by modification date and returning most recently modified workspace")

let workspacesAndDates = workspaces
.map {
(modificationDate($0), $0)
}

logger.debug("Comparing dates and workspaces: ")
workspacesAndDates.forEach { logger.debug("\($0) - \($1)") }

return workspacesAndDates
.sorted {
$0.0 > $1.0
}[0].1
}
}

Expand Down
59 changes: 25 additions & 34 deletions Sources/GenIR/PIFCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,6 @@ class PIFCache {
workspace.projects
}

// private lazy var projectsByGUID: [GUID: PIF.Project] = {
// workspace
// .projects
// .reduce(into: [GUID: PIF.Project]()) { result, element in
// result[element.guid] = element
// }
// }()

// func project(for guid: GUID) -> PIF.Project? {
// projectsByGUID[guid]
// }

// TODO: We cab possibly filter out some targets here for performance
var targets: [PIF.BaseTarget] {
workspace
.projects
Expand Down Expand Up @@ -110,35 +97,22 @@ class PIFCache {
let frameworkFileReferences = projects
.flatMap { fileReferences(for: $0) }
.filter { $0.fileType == "wrapper.framework" }
// TODO: do we filter on sourceTree == "BUILT_PRODUCTS_DIR" here too?

// Now, stupidly, we have to do a name lookup on the path and use that to look up a target
let frameworks = targets
.compactMap { $0 as? PIF.Target }
.filter { $0.productType == .framework }
.reduce(into: [String: PIF.Target]()) { partial, target in
partial[target.productName] = target
let key = target.productName.isEmpty ? target.guid : target.productName
partial[key] = target
}

return frameworkFileReferences
// .compactMap { frameworks[$0.path] } // TODO: I think we should get the last path component as the key here - check that
.reduce(into: [PIF.GUID: PIF.Target]()) { partial, fileReference in
// partial[target.guid] = target
// Use the _file reference_ GUID as the key here - we're looking up frameworks by their file reference and not target GUID!
partial[fileReference.guid] = frameworks[fileReference.path]
}
}()

// private lazy var targetsByGUID: [GUID: PIF.BaseTarget] = {
// targets
// .reduce(into: [GUID: PIF.BaseTarget]()) { result, element in
// result[element.guid] = element
// }
// }()

// func target(for guid: GUID) -> PIF.BaseTarget? {
// targetsByGUID[guid]
// }
}

extension PIF.BaseTarget: Hashable {
Expand Down Expand Up @@ -166,20 +140,33 @@ struct PIFDependencyProvider: DependencyProviding {
}
}

private func resolveSwiftPackage(_ packageGUID: PIF.GUID) -> PIF.GUID {
private func resolveSwiftPackage(_ packageGUID: PIF.GUID) -> PIF.GUID? {
let productToken = "PACKAGE-PRODUCT:"
let targetToken = "PACKAGE-TARGET:"
guard packageGUID.starts(with: productToken), let product = guidToTargets[packageGUID] else { return packageGUID }

let productName = String(packageGUID.dropFirst(productToken.count))

// TODO: should this also use the framework build phase to determine a dependency?
let packageTargetDependencies = product
// TODO: should this also use the framework build phase to determine a dependency? Currently not needed because Gen IR doesn't care about prebuilt frameworks
// but for the completeness of the graph this could be a nice to have...
let dependencies = product
.baseTarget
.dependencies
.filter { $0.targetGUID.starts(with: targetToken) }

let packageTargetDependencies = dependencies
.filter { $0.targetGUID.dropFirst(targetToken.count) == productName }

if packageTargetDependencies.isEmpty && !dependencies.isEmpty {
// We likely have a stub target here (i.e. a precompiled framework)
// see https://github.com/apple/swift-package-manager/issues/6069 for more
logger.debug("Resolving Swift Package (\(productName) - \(packageGUID)) resulted in no targets. Possible stub target in: \(dependencies)")
return nil
} else if packageTargetDependencies.isEmpty && dependencies.isEmpty {
logger.debug("Resolving Swift Package (\(productName) - \(packageGUID)) resulted in no targets.")
return nil
}

precondition(packageTargetDependencies.count == 1, "Expecting one matching package target - found \(packageTargetDependencies.count): \(packageTargetDependencies). Returning first match")

return packageTargetDependencies.first?.targetGUID ?? packageGUID
Expand All @@ -191,23 +178,27 @@ struct PIFDependencyProvider: DependencyProviding {
.baseTarget
.dependencies
.map { $0.targetGUID }
.map { resolveSwiftPackage($0) }
.compactMap { resolveSwiftPackage($0) }

// Framework build phase dependencies
let frameworkBuildPhases = value
.baseTarget
.buildPhases
.compactMap { $0 as? PIF.FrameworksBuildPhase }

let frameworkGUIDs = frameworkBuildPhases
let referenceGUIDs = frameworkBuildPhases
.flatMap { $0.buildFiles }
.compactMap {
switch $0.reference {
case .file(let guid): return guid
case .target: return nil // TODO: is this fine? I think so since we're looking for .framework file references here not targets which should be a dependency
}
}
.compactMap { cache.frameworks[$0]?.guid }

let frameworkGUIDs = referenceGUIDs
.compactMap {
cache.frameworks[$0]?.guid
}

let dependencyTargets = (dependencyTargetGUIDs + frameworkGUIDs).compactMap { guidToTargets[$0] }

Expand Down
8 changes: 8 additions & 0 deletions TestAssets/PIFCaches/SPMTest/MyBinaryDependency/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
25 changes: 25 additions & 0 deletions TestAssets/PIFCaches/SPMTest/MyBinaryDependency/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let lottieXCFramework = Target.binaryTarget(
name: "MyBinaryDependency",
url: "https://github.com/airbnb/lottie-ios/releases/download/4.4.3/Lottie-Xcode-15.2.xcframework.zip",
checksum: "546b7e718ed806646b84645ecfb1e1d6a65ac0387ff3f8ecb92dbaf2116cd62c")

let package = Package(
name: "MyBinaryDependency",
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "MyBinaryDependency",
targets: ["MyBinaryDependency"/*, "_Stub"*/]),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
lottieXCFramework,
// .target(name: "_Stub"),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import XCTest
@testable import MyBinaryDependency

final class MyBinaryDependencyTests: XCTestCase {
func testExample() throws {
// XCTest Documentation
// https://developer.apple.com/documentation/xctest

// Defining Test Cases and Test Methods
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
}
}
8 changes: 8 additions & 0 deletions TestAssets/PIFCaches/SPMTest/MyCommonLibrary/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
23 changes: 23 additions & 0 deletions TestAssets/PIFCaches/SPMTest/MyCommonLibrary/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "MyCommonLibrary",
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "MyCommonLibrary",
targets: ["MyCommonLibrary"]),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "MyCommonLibrary"),
.testTarget(
name: "MyCommonLibraryTests",
dependencies: ["MyCommonLibrary"]),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book

public struct MyCommonLibrary {
public static let common = "CommonLibrary"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import XCTest
@testable import MyCommonLibrary

final class MyCommonLibraryTests: XCTestCase {
func testExample() throws {
// XCTest Documentation
// https://developer.apple.com/documentation/xctest

// Defining Test Cases and Test Methods
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
}
}
8 changes: 8 additions & 0 deletions TestAssets/PIFCaches/SPMTest/MyLibrary/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
33 changes: 33 additions & 0 deletions TestAssets/PIFCaches/SPMTest/MyLibrary/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "MyLibrary",
platforms: [.iOS(.v14)],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "MyLibrary",
targets: ["MyLibrary"]),
],
dependencies: [
.package(path: "../MyTransitiveLibrary"),
.package(path: "../MyCommonLibrary")
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "MyLibrary",
dependencies: [
.product(name: "MyTransitiveLibrary", package: "MyTransitiveLibrary"),
.product(name: "MyCommonLibrary", package: "MyCommonLibrary")
]
),
.testTarget(
name: "MyLibraryTests",
dependencies: ["MyLibrary"]),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book

import MyTransitiveLibrary
import MyCommonLibrary

public struct MyLibrary {
public static let version = "1.0.0, \(MyTransitiveLibrary.test) - \(MyCommonLibrary.common)"
public static let view = MyTransitiveLibrary.lottie
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import XCTest
@testable import MyLibrary

final class MyLibraryTests: XCTestCase {
func testExample() throws {
// XCTest Documentation
// https://developer.apple.com/documentation/xctest

// Defining Test Cases and Test Methods
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
}
}
8 changes: 8 additions & 0 deletions TestAssets/PIFCaches/SPMTest/MyTransitiveLibrary/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Loading

0 comments on commit 9b86fed

Please sign in to comment.