Skip to content
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
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,10 @@ let package = Package(
.product(name: "SwiftParser", package: "swift-syntax"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
// Depend on `SwiftCompilerPlugin` and `SwiftSyntaxMacros` so the modules are built before running tests and can
// be used by test cases that test macros (see `SwiftPMTestProject.macroPackageManifest`).
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
]
),
]
Expand Down
67 changes: 56 additions & 11 deletions Sources/SKTestSupport/SkipUnless.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,11 @@ public actor SkipUnless {
line: UInt,
featureCheck: () async throws -> Bool
) async throws {
let checkResult: FeatureCheckResult
if let cachedResult = checkCache[featureName] {
checkResult = cachedResult
} else if ProcessEnv.block["SWIFTCI_USE_LOCAL_DEPS"] != nil {
// Never skip tests in CI. Toolchain should be up-to-date
checkResult = .featureSupported
} else {
return try await skipUnlessSupported(featureName: featureName, file: file, line: line) {
let toolchainSwiftVersion = try await unwrap(ToolchainRegistry.forTesting.default).swiftVersion
let requiredSwiftVersion = SwiftVersion(swiftVersion.major, swiftVersion.minor)
if toolchainSwiftVersion < requiredSwiftVersion {
checkResult = .featureUnsupported(
return .featureUnsupported(
skipMessage: """
Skipping because toolchain has Swift version \(toolchainSwiftVersion) \
but test requires at least \(requiredSwiftVersion)
Expand All @@ -77,15 +71,32 @@ public actor SkipUnless {
} else if toolchainSwiftVersion == requiredSwiftVersion {
logger.info("Checking if feature '\(featureName)' is supported")
if try await !featureCheck() {
checkResult = .featureUnsupported(skipMessage: "Skipping because toolchain doesn't contain \(featureName)")
return .featureUnsupported(skipMessage: "Skipping because toolchain doesn't contain \(featureName)")
} else {
checkResult = .featureSupported
return .featureSupported
}
logger.info("Done checking if feature '\(featureName)' is supported")
} else {
checkResult = .featureSupported
return .featureSupported
}
}
}

private func skipUnlessSupported(
featureName: String = #function,
file: StaticString,
line: UInt,
featureCheck: () async throws -> FeatureCheckResult
) async throws {
let checkResult: FeatureCheckResult
if let cachedResult = checkCache[featureName] {
checkResult = cachedResult
} else if ProcessEnv.block["SWIFTCI_USE_LOCAL_DEPS"] != nil {
// Never skip tests in CI. Toolchain should be up-to-date
checkResult = .featureSupported
} else {
checkResult = try await featureCheck()
}
checkCache[featureName] = checkResult

if case .featureUnsupported(let skipMessage) = checkResult {
Expand Down Expand Up @@ -272,6 +283,40 @@ public actor SkipUnless {
}
#endif
}

/// Check if we can use the build artifacts in the sourcekit-lsp build directory to build a macro package without
/// re-building swift-syntax.
public static func canBuildMacroUsingSwiftSyntaxFromSourceKitLSPBuild(
file: StaticString = #filePath,
line: UInt = #line
) async throws {
return try await shared.skipUnlessSupported(file: file, line: line) {
do {
let project = try await SwiftPMTestProject(
files: [
"MyMacros/MyMacros.swift": #"""
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
"""#,
"MyMacroClient/MyMacroClient.swift": """
""",
],
manifest: SwiftPMTestProject.macroPackageManifest
)
try await SwiftPMTestProject.build(at: project.scratchDirectory)
return .featureSupported
} catch {
return .featureUnsupported(
skipMessage: """
Skipping because macro could not be built using build artifacts in the sourcekit-lsp build directory. \
This usually happens if sourcekit-lsp was built using a different toolchain than the one used at test-time.
"""
)
}
}
}
}

// MARK: - Parsing Swift compiler version
Expand Down
110 changes: 110 additions & 0 deletions Sources/SKTestSupport/SwiftPMTestProject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import LanguageServerProtocol
import SourceKitLSP
import TSCBasic

private struct SwiftSyntaxCShimsModulemapNotFoundError: Error {}

public class SwiftPMTestProject: MultiFileTestProject {
enum Error: Swift.Error {
/// The `swift` executable could not be found.
Expand All @@ -33,6 +35,114 @@ public class SwiftPMTestProject: MultiFileTestProject {
)
"""

/// A manifest that defines two targets:
/// - A macro target named `MyMacro`
/// - And executable target named `MyMacroClient`
///
/// It builds the macro using the swift-syntax that was already built as part of the SourceKit-LSP build.
/// Re-using the SwiftSyntax modules that are already built is significantly faster than building swift-syntax in
/// each test case run and does not require internet access.
public static var macroPackageManifest: String {
get async throws {
// Directories that we should search for the swift-syntax package.
// We prefer a checkout in the build folder. If that doesn't exist, we are probably using local dependencies
// (SWIFTCI_USE_LOCAL_DEPS), so search next to the sourcekit-lsp source repo
let swiftSyntaxSearchPaths = [
productsDirectory
.deletingLastPathComponent() // arm64-apple-macosx
.deletingLastPathComponent() // debug
.appendingPathComponent("checkouts"),
URL(fileURLWithPath: #filePath)
.deletingLastPathComponent() // SwiftPMTestProject.swift
.deletingLastPathComponent() // SKTestSupport
.deletingLastPathComponent() // Sources
.deletingLastPathComponent(), // sourcekit-lsp
]

let swiftSyntaxCShimsModulemap =
swiftSyntaxSearchPaths.map { swiftSyntaxSearchPath in
swiftSyntaxSearchPath
.appendingPathComponent("swift-syntax")
.appendingPathComponent("Sources")
.appendingPathComponent("_SwiftSyntaxCShims")
.appendingPathComponent("include")
.appendingPathComponent("module.modulemap")
}
.first { FileManager.default.fileExists(atPath: $0.path) }

guard let swiftSyntaxCShimsModulemap else {
throw SwiftSyntaxCShimsModulemapNotFoundError()
}

let swiftSyntaxModulesToLink = [
"SwiftBasicFormat",
"SwiftCompilerPlugin",
"SwiftCompilerPluginMessageHandling",
"SwiftDiagnostics",
"SwiftOperators",
"SwiftParser",
"SwiftParserDiagnostics",
"SwiftSyntax",
"SwiftSyntaxBuilder",
"SwiftSyntaxMacroExpansion",
"SwiftSyntaxMacros",
]

var objectFiles: [String] = []
for moduleName in swiftSyntaxModulesToLink {
let dir = productsDirectory.appendingPathComponent("\(moduleName).build")
let enumerator = FileManager.default.enumerator(at: dir, includingPropertiesForKeys: nil)
while let file = enumerator?.nextObject() as? URL {
if file.pathExtension == "o" {
objectFiles.append(file.path)
}
}
}

let linkerFlags = objectFiles.map {
"""
"-l", "\($0)",
"""
}.joined(separator: "\n")

let moduleSearchPath: String
if let toolchainVersion = try await ToolchainRegistry.forTesting.default?.swiftVersion,
toolchainVersion < SwiftVersion(6, 0)
{
moduleSearchPath = productsDirectory.path
} else {
moduleSearchPath = "\(productsDirectory.path)/Modules"
}

return """
// swift-tools-version: 5.10

import PackageDescription
import CompilerPluginSupport

let package = Package(
name: "MyMacro",
platforms: [.macOS(.v10_15)],
targets: [
.macro(
name: "MyMacros",
swiftSettings: [.unsafeFlags([
"-I", "\(moduleSearchPath)",
"-Xcc", "-fmodule-map-file=\(swiftSyntaxCShimsModulemap.path)"
])],
linkerSettings: [
.unsafeFlags([
\(linkerFlags)
])
]
),
.executableTarget(name: "MyMacroClient", dependencies: ["MyMacros"]),
]
)
"""
}
}

/// Create a new SwiftPM package with the given files.
///
/// If `index` is `true`, then the package will be built, indexing all modules within the package.
Expand Down
34 changes: 34 additions & 0 deletions Sources/SKTestSupport/TestBundle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation

/// The bundle of the currently executing test.
public let testBundle: Bundle = {
#if os(macOS)
if let bundle = Bundle.allBundles.first(where: { $0.bundlePath.hasSuffix(".xctest") }) {
return bundle
}
fatalError("couldn't find the test bundle")
#else
return Bundle.main
#endif
}()

/// The path to the built products directory, ie. `.build/debug/arm64-apple-macosx` or the platform-specific equivalent.
public let productsDirectory: URL = {
#if os(macOS)
return testBundle.bundleURL.deletingLastPathComponent()
#else
return testBundle.bundleURL
#endif
}()
21 changes: 0 additions & 21 deletions Tests/SKCoreTests/BuildServerBuildSystemTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,6 @@ import SKTestSupport
import TSCBasic
import XCTest

/// The bundle of the currently executing test.
private let testBundle: Bundle = {
#if os(macOS)
if let bundle = Bundle.allBundles.first(where: { $0.bundlePath.hasSuffix(".xctest") }) {
return bundle
}
fatalError("couldn't find the test bundle")
#else
return Bundle.main
#endif
}()

/// The path to the built products directory.
private let productsDirectory: URL = {
#if os(macOS)
return testBundle.bundleURL.deletingLastPathComponent()
#else
return testBundle.bundleURL
#endif
}()

/// The path to the INPUTS directory of shared test projects.
private let skTestSupportInputsDirectory: URL = {
#if os(macOS)
Expand Down
47 changes: 47 additions & 0 deletions Tests/SKSwiftPMWorkspaceTests/SwiftPMBuildSystemTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,53 @@ final class SwiftPMBuildSystemTests: XCTestCase {
assertArgumentsContain(aswift.pathString, arguments: arguments)
}
}

func testBuildMacro() async throws {
try await SkipUnless.canBuildMacroUsingSwiftSyntaxFromSourceKitLSPBuild()
// This test is just a dummy to show how to create a `SwiftPMTestProject` that builds a macro using the SwiftSyntax
// modules that were already built during the build of SourceKit-LSP.
// It should be removed once we have a real test that tests macros (like macro expansion).
let project = try await SwiftPMTestProject(
files: [
"MyMacros/MyMacros.swift": #"""
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

public struct StringifyMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
guard let argument = node.argumentList.first?.expression else {
fatalError("compiler bug: the macro does not have any arguments")
}

return "(\(argument), \(literal: argument.description))"
}
}

@main
struct MyMacroPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
StringifyMacro.self,
]
}
"""#,
"MyMacroClient/MyMacroClient.swift": """
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyMacros", type: "StringifyMacro")

func test() {
#stringify(1 + 2)
}
""",
],
manifest: SwiftPMTestProject.macroPackageManifest
)
try await SwiftPMTestProject.build(at: project.scratchDirectory)
}
}

private func assertArgumentsDoNotContain(
Expand Down