Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Swift 6 #91

Merged
merged 9 commits into from
Jul 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions Package@swift-6.0.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import CompilerPluginSupport
import PackageDescription

let package = Package(
name: "SafeDI",
platforms: [
.macOS(.v10_15),
.iOS(.v13),
.tvOS(.v13),
.watchOS(.v6),
.macCatalyst(.v13),
.visionOS(.v1),
],
products: [
/// A library containing SafeDI macros, property wrappers, and types.
.library(
name: "SafeDI",
targets: ["SafeDI"]
),
/// A SafeDI plugin that must be run on the root source module in a project.
.plugin(
name: "SafeDIGenerator",
targets: ["SafeDIGenerator"]
),
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.0"),
// TODO: Bump to 600.0.0 once it's available.
.package(url: "https://github.com/apple/swift-syntax.git", from: "510.0.0"),
dfed marked this conversation as resolved.
Show resolved Hide resolved
.package(url: "https://github.com/michaeleisel/ZippyJSON.git", from: "1.2.0"),
.package(url: "https://github.com/pointfreeco/swift-macro-testing.git", from: "0.4.0"),
],
targets: [
// Macros
.target(
name: "SafeDI",
dependencies: ["SafeDIMacros"],
swiftSettings: [
.swiftLanguageVersion(.v6),
]
),
.testTarget(
name: "SafeDITests",
dependencies: [
"SafeDI",
"SafeDICore",
],
swiftSettings: [
.swiftLanguageVersion(.v6),
]
),
.macro(
name: "SafeDIMacros",
dependencies: [
"SafeDICore",
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
.product(name: "SwiftDiagnostics", package: "swift-syntax"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
],
swiftSettings: [
.swiftLanguageVersion(.v6),
]
),
.testTarget(
name: "SafeDIMacrosTests",
dependencies: [
"SafeDIMacros",
"SafeDICore",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
.product(name: "MacroTesting", package: "swift-macro-testing"),
],
swiftSettings: [
.swiftLanguageVersion(.v6),
]
),

// Plugins
.plugin(
name: "SafeDIGenerator",
capability: .buildTool(),
dependencies: ["SafeDITool"]
),
.executableTarget(
name: "SafeDITool",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftParser", package: "swift-syntax"),
.byNameItem(name: "ZippyJSON", condition: .when(platforms: [.iOS, .tvOS, .macOS])),
"SafeDICore",
],
swiftSettings: [
.swiftLanguageVersion(.v6),
]
),
.testTarget(
name: "SafeDIToolTests",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.byNameItem(name: "ZippyJSON", condition: .when(platforms: [.iOS, .tvOS, .macOS])),
"SafeDITool",
],
swiftSettings: [
.swiftLanguageVersion(.v6),
]
),

// Core
.target(
name: "SafeDICore",
dependencies: [
.product(name: "Collections", package: "swift-collections"),
.product(name: "SwiftDiagnostics", package: "swift-syntax"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
],
swiftSettings: [
.swiftLanguageVersion(.v6),
]
),
.testTarget(
name: "SafeDICoreTests",
dependencies: ["SafeDICore"],
swiftSettings: [
.swiftLanguageVersion(.v6),
]
),
]
)
163 changes: 109 additions & 54 deletions Plugins/SafeDIGenerator/SafeDIGenerateDependencyTree.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,45 +11,85 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin {
return []
}

let outputSwiftFile = context.pluginWorkDirectory.appending(subpath: "SafeDI.swift")
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changes to this file are best viewed with whitespace changes turned off. Apple deprecated a ton of the path-based APIs in Swift 6 / Xcode 16.

// Swift Package Plugins do not (as of Swift 5.9) allow for
// creating dependencies between plugin output. Since our
// current build system does not support depending on the
// output of other plugins, we must forgo searching for
// `.safeDI` files and instead parse the entire project at once.
let targetSwiftFiles = sourceTarget.sourceFiles(withSuffix: ".swift").map(\.path)
let dependenciesSourceFiles = sourceTarget
.sourceModuleRecursiveDependencies
.flatMap {
$0
.sourceFiles(withSuffix: ".swift")
.map(\.path)
#if compiler(>=6.0)
let outputSwiftFile = context.pluginWorkDirectoryURL.appending(path: "SafeDI.swift")
// Swift Package Plugins do not (as of Swift 5.9) allow for
// creating dependencies between plugin output. Since our
// current build system does not support depending on the
// output of other plugins, we must forgo searching for
// `.safeDI` files and instead parse the entire project at once.
let targetSwiftFiles = sourceTarget.sourceFiles(withSuffix: ".swift").map(\.url)
let dependenciesSourceFiles = sourceTarget
.sourceModuleRecursiveDependencies
.flatMap {
$0
.sourceFiles(withSuffix: ".swift")
.map(\.url)
}
let inputSourcesFilePath = context.pluginWorkDirectoryURL.appending(path: "InputSwiftFiles.csv").path()
try Data(
(targetSwiftFiles.map { $0.path() } + dependenciesSourceFiles.map { $0.path() })
.joined(separator: ",")
.utf8
)
.write(toPath: inputSourcesFilePath)
let arguments = [
inputSourcesFilePath,
"--dependency-tree-output",
outputSwiftFile.path(),
]

let toolPath: URL = if FileManager.default.fileExists(atPath: Self.armMacBrewInstallLocation) {
// SafeDITool has been installed via homebrew on an ARM Mac.
URL(filePath: Self.armMacBrewInstallLocation)
} else if FileManager.default.fileExists(atPath: Self.intelMacBrewInstallLocation) {
// SafeDITool has been installed via homebrew on an Intel Mac.
URL(filePath: Self.intelMacBrewInstallLocation)
} else {
// Fall back to the just-in-time built tool.
try context.tool(named: "SafeDITool").url
}
#else
let outputSwiftFile = context.pluginWorkDirectory.appending(subpath: "SafeDI.swift")
// Swift Package Plugins do not (as of Swift 5.9) allow for
// creating dependencies between plugin output. Since our
// current build system does not support depending on the
// output of other plugins, we must forgo searching for
// `.safeDI` files and instead parse the entire project at once.
let targetSwiftFiles = sourceTarget.sourceFiles(withSuffix: ".swift").map(\.path)
let dependenciesSourceFiles = sourceTarget
.sourceModuleRecursiveDependencies
.flatMap {
$0
.sourceFiles(withSuffix: ".swift")
.map(\.path)
}

let inputSourcesFilePath = context.pluginWorkDirectory.appending(subpath: "InputSwiftFiles.csv").string
try Data(
(targetSwiftFiles + dependenciesSourceFiles)
.map(\.string)
.joined(separator: ",")
.utf8
)
.write(toPath: inputSourcesFilePath)
let arguments = [
inputSourcesFilePath,
"--dependency-tree-output",
outputSwiftFile.string,
]
let inputSourcesFilePath = context.pluginWorkDirectory.appending(subpath: "InputSwiftFiles.csv").string
try Data(
(targetSwiftFiles + dependenciesSourceFiles)
.map(\.string)
.joined(separator: ",")
.utf8
)
.write(toPath: inputSourcesFilePath)
let arguments = [
inputSourcesFilePath,
"--dependency-tree-output",
outputSwiftFile.string,
]

let toolPath: PackagePlugin.Path = if FileManager.default.fileExists(atPath: Self.armMacBrewInstallLocation) {
// SafeDITool has been installed via homebrew on an ARM Mac.
PackagePlugin.Path(Self.armMacBrewInstallLocation)
} else if FileManager.default.fileExists(atPath: Self.intelMacBrewInstallLocation) {
// SafeDITool has been installed via homebrew on an Intel Mac.
PackagePlugin.Path(Self.intelMacBrewInstallLocation)
} else {
// Fall back to the just-in-time built tool.
try context.tool(named: "SafeDITool").path
}
let toolPath: PackagePlugin.Path = if FileManager.default.fileExists(atPath: Self.armMacBrewInstallLocation) {
// SafeDITool has been installed via homebrew on an ARM Mac.
PackagePlugin.Path(Self.armMacBrewInstallLocation)
} else if FileManager.default.fileExists(atPath: Self.intelMacBrewInstallLocation) {
// SafeDITool has been installed via homebrew on an Intel Mac.
PackagePlugin.Path(Self.intelMacBrewInstallLocation)
} else {
// Fall back to the just-in-time built tool.
try context.tool(named: "SafeDITool").path
}
#endif

return [
.buildCommand(
Expand All @@ -69,26 +109,43 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin {

extension Target {
var sourceModuleRecursiveDependencies: [SwiftSourceModuleTarget] {
recursiveTargetDependencies.compactMap {
recursiveTargetDependencies.compactMap { target in
// Since we only understand Swift files, we only care about SwiftSourceModuleTargets.
guard let swiftModule = $0 as? SwiftSourceModuleTarget else {
guard let swiftModule = target as? SwiftSourceModuleTarget else {
return nil
}

// We only care about first-party code. Ignore third-party dependencies.
guard
swiftModule
.directory
// Removing the module name.
.removingLastComponent()
// Removing 'Sources'.
.removingLastComponent()
// Removing the package name.
.removingLastComponent()
.lastComponent != "checkouts"
else {
return nil
}
#if compiler(>=6.0)
guard
swiftModule
.directoryURL
.pathComponents
// Removing the module name.
.dropLast()
// Removing 'Sources'.
.dropLast()
// Removing the package name.
.dropLast()
.last != "checkouts"
else {
return nil
}
#else
guard
swiftModule
.directory
// Removing the module name.
.removingLastComponent()
// Removing 'Sources'.
.removingLastComponent()
// Removing the package name.
.removingLastComponent()
.lastComponent != "checkouts"
else {
return nil
}
#endif
return swiftModule
}
}
Expand All @@ -101,9 +158,7 @@ extension Target {
func createBuildCommands(
context: XcodeProjectPlugin.XcodePluginContext,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

somewhat annoyingly, XcodeProjectPlugin.XcodePluginContext has not been updated at all, and all of its APIs are deprecated. Hopefully an oversight that'll be fixed before Xcode 16.0 goes live.

target: XcodeProjectPlugin.XcodeTarget
)
throws -> [PackagePlugin.Command]
{
) throws -> [PackagePlugin.Command] {
// As of Xcode 15.0.1, Swift Package Plugins in Xcode are unable
// to inspect target dependencies. As a result, this Xcode plugin
// only works if it is running on a single-module project, or if
Expand Down
6 changes: 3 additions & 3 deletions Sources/SafeDICore/Generators/ScopeGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import Collections

/// A model capable of generating code for a scope’s dependency tree.
actor ScopeGenerator: CustomStringConvertible {
actor ScopeGenerator: CustomStringConvertible, Sendable {
// MARK: Initialization

init(
Expand Down Expand Up @@ -303,7 +303,7 @@ actor ScopeGenerator: CustomStringConvertible {

// MARK: Private

private enum ScopeData {
private enum ScopeData: Sendable {
case root(instantiable: Instantiable)
case property(
instantiable: Instantiable,
Expand Down Expand Up @@ -421,7 +421,7 @@ actor ScopeGenerator: CustomStringConvertible {

private enum GenerationError: Error, CustomStringConvertible {
case erasedInstantiatorGenericDoesNotMatch(property: Property, instantiable: Instantiable)
case dependencyCycleDetected(any Collection<Property>, scope: ScopeGenerator)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any Collection is not Sendable, but Array is

case dependencyCycleDetected([Property], scope: ScopeGenerator)

var description: String {
switch self {
Expand Down
4 changes: 2 additions & 2 deletions Sources/SafeDICore/Models/Dependency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import SwiftSyntax

/// A representation of a dependency.
/// e.g. `@Instantiated let myService: MyService`
public struct Dependency: Codable, Hashable {
public struct Dependency: Codable, Hashable, Sendable {
// MARK: Initialization

public init(
Expand Down Expand Up @@ -50,7 +50,7 @@ public struct Dependency: Codable, Hashable {
/// The receiver’s type description as an `@Instantiable`-decorated type.
public let asInstantiatedType: TypeDescription

public enum Source: Codable, Hashable {
public enum Source: Codable, Hashable, Sendable {
case instantiated(fulfillingTypeDescription: TypeDescription?, erasedToConcreteExistential: Bool)
case received
case aliased(fulfillingProperty: Property, erasedToConcreteExistential: Bool)
Expand Down
2 changes: 1 addition & 1 deletion Sources/SafeDICore/Models/ImportStatement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

public struct ImportStatement: Codable, Hashable {
public struct ImportStatement: Codable, Hashable, Sendable {
// MARK: Initialization

public init(
Expand Down
Loading