diff --git a/Package.swift b/Package.swift index 758cba2e..bec98729 100644 --- a/Package.swift +++ b/Package.swift @@ -263,6 +263,10 @@ let package = Package( name: "SwiftJavaShared" ), + .target( + name: "CodePrinting" + ), + .target( name: "SwiftJavaToolLib", dependencies: [ @@ -277,6 +281,7 @@ let package = Package( "JavaNet", "SwiftJavaShared", "SwiftJavaConfigurationShared", + "CodePrinting", .product(name: "Subprocess", package: "swift-subprocess"), ], swiftSettings: [ @@ -324,6 +329,7 @@ let package = Package( .product(name: "SwiftJavaJNICore", package: "swift-java-jni-core"), "SwiftJavaShared", "SwiftJavaConfigurationShared", + "CodePrinting", ], swiftSettings: [ .swiftLanguageMode(.v5) @@ -363,7 +369,11 @@ let package = Package( .testTarget( name: "SwiftJavaToolLibTests", dependencies: [ - "SwiftJavaToolLib" + "SwiftJavaToolLib", + "SwiftJavaConfigurationShared", + ], + exclude: [ + "SimpleJavaProject" ], swiftSettings: [ .swiftLanguageMode(.v5) @@ -381,7 +391,8 @@ let package = Package( .testTarget( name: "JExtractSwiftTests", dependencies: [ - "JExtractSwiftLib" + "JExtractSwiftLib", + "CodePrinting", ], swiftSettings: [ .swiftLanguageMode(.v5) diff --git a/Sources/JExtractSwiftLib/CodePrinter.swift b/Sources/CodePrinting/CodePrinter.swift similarity index 79% rename from Sources/JExtractSwiftLib/CodePrinter.swift rename to Sources/CodePrinting/CodePrinter.swift index a5b339d6..8a6f2e40 100644 --- a/Sources/JExtractSwiftLib/CodePrinter.swift +++ b/Sources/CodePrinting/CodePrinter.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,21 +12,21 @@ // //===----------------------------------------------------------------------===// -import Foundation - -#if os(Windows) -let PATH_SEPARATOR = "\\" +#if canImport(FoundationEssentials) +import FoundationEssentials #else -let PATH_SEPARATOR = "/" +import Foundation #endif +// ==== ----------------------------------------------------------------------- +// MARK: CodePrinter + public struct CodePrinter { - var contents: String = "" + public var contents: String = "" - var verbose: Bool = false - let log = Logger(label: "printer", logLevel: .info) + public var verbose: Bool = false - var indentationDepth: Int = 0 { + public var indentationDepth: Int = 0 { didSet { indentationText = String(repeating: indentationPart, count: indentationDepth) } @@ -40,7 +40,7 @@ public struct CodePrinter { } public var indentationText: String = "" /// If true, next print() should starts with indentation. - var atNewline = true + public var atNewline = true public static func toString(_ block: (inout CodePrinter) throws -> Void) rethrows -> String { var printer = CodePrinter() @@ -48,7 +48,7 @@ public struct CodePrinter { return printer.finalize() } - var mode: PrintMode + public var mode: PrintMode public enum PrintMode { case accumulateAll case flushToFileOnWrite @@ -57,14 +57,14 @@ public struct CodePrinter { self.mode = mode } - mutating func append(_ text: String) { + public mutating func append(_ text: String) { contents.append(text) if self.verbose { Swift.print(text, terminator: "") } } - mutating func append(contentsOf text: S) + public mutating func append(contentsOf text: S) where S: Sequence, S.Element == Character { contents.append(contentsOf: text) if self.verbose { @@ -115,7 +115,7 @@ public struct CodePrinter { line: UInt = #line ) { for part in parts { - guard part.trimmingCharacters(in: .whitespacesAndNewlines).count != 0 else { + guard !part.allSatisfy(\.isWhitespace) else { continue } @@ -168,7 +168,6 @@ public struct CodePrinter { print(text, .continue) } - // TODO: remove this in real mode, this just helps visually while working on it public mutating func printSeparator(_ text: String) { assert(!text.contains(where: \.isNewline)) print( @@ -182,20 +181,16 @@ public struct CodePrinter { } public mutating func finalize() -> String { - // assert(indentationDepth == 0, "Finalize CodePrinter with non-zero indentationDepth. Text was: \(contents)") // FIXME: do this defer { contents = "" } - return contents } public mutating func indent(file: String = #fileID, line: UInt = #line, function: String = #function) { indentationDepth += 1 - log.trace("Indent => \(indentationDepth)", file: file, line: line, function: function) } public mutating func outdent(file: String = #fileID, line: UInt = #line, function: String = #function) { indentationDepth -= 1 - log.trace("Outdent => \(indentationDepth)", file: file, line: line, function: function) assert(indentationDepth >= 0, "Outdent beyond zero at [\(file):\(line)](\(function))") } @@ -207,9 +202,11 @@ public struct CodePrinter { Swift.print("// CodePrinter.dump @ \(file):\(line)") Swift.print(contents) } - } +// ==== ----------------------------------------------------------------------- +// MARK: PrinterTerminator + public enum PrinterTerminator: String { case newLine = "\n" case space = " " @@ -235,24 +232,37 @@ public enum PrinterTerminator: String { } } +// ==== ----------------------------------------------------------------------- +// MARK: PATH_SEPARATOR + +#if os(Windows) +public let PATH_SEPARATOR = "\\" +#else +public let PATH_SEPARATOR = "/" +#endif + +// ==== ----------------------------------------------------------------------- +// MARK: CodePrinter + writeContents + extension CodePrinter { - /// - Returns: the output path of the generated file, if any (i.e. not in accumulate in memory mode) - package mutating func writeContents( + /// Write the accumulated contents to a file in the given output directory. + /// + /// - Returns: the output path of the generated file, if any (i.e. not in accumulate-in-memory mode) + public mutating func writeContents( outputDirectory _outputDirectory: String, javaPackagePath: String?, filename _filename: String ) throws -> URL? { - - // We handle 'filename' that has a path, since that simplifies passing paths from root output directory enourmously. - // This just moves the directory parts into the output directory part in order for us to create the sub-directories. + // We handle 'filename' that has a path, since that simplifies passing + // paths from root output directory enormously. This just moves the + // directory parts into the output directory part in order for us to + // create the sub-directories. let outputDirectory: String let filename: String if _filename.contains(PATH_SEPARATOR) { let parts = _filename.split(separator: PATH_SEPARATOR) - outputDirectory = _outputDirectory.appending(PATH_SEPARATOR).appending( - parts.dropLast().joined(separator: PATH_SEPARATOR) - ) + outputDirectory = _outputDirectory + PATH_SEPARATOR + parts.dropLast().joined(separator: PATH_SEPARATOR) filename = "\(parts.last!)" } else { outputDirectory = _outputDirectory @@ -260,8 +270,9 @@ extension CodePrinter { } guard self.mode != .accumulateAll else { - // if we're accumulating everything, we don't want to finalize/flush any contents - // let's mark that this is where a write would have happened though: + // if we're accumulating everything, we don't want to finalize/flush + // any contents; let's mark that this is where a write would have + // happened though: print("// ^^^^ Contents of: \(outputDirectory)\(PATH_SEPARATOR)\(filename)") return nil } @@ -281,19 +292,16 @@ extension CodePrinter { } let targetDirectory = [outputDirectory, javaPackagePath].compactMap { $0 }.joined(separator: PATH_SEPARATOR) - log.debug("Prepare target directory: '\(targetDirectory)' for file \(filename.bold)") do { try FileManager.default.createDirectory( atPath: targetDirectory, withIntermediateDirectories: true ) } catch { - // log and throw since it can be confusing what the reason for failing the write was otherwise - log.warning("Failed to create directory: \(targetDirectory)") throw error } - let outputPath = Foundation.URL(fileURLWithPath: targetDirectory).appendingPathComponent(filename) + let outputPath = URL(fileURLWithPath: targetDirectory).appendingPathComponent(filename) try contents.write( to: outputPath, atomically: true, diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift index 0c96eaf7..7dd054f2 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import SwiftJavaConfigurationShared import SwiftJavaJNICore import SwiftSyntax diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift index 047ac3c0..15089b9c 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import SwiftJavaJNICore extension FFMSwift2JavaGenerator { diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index 9dbc3832..8880d309 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import SwiftSyntax import SwiftSyntaxBuilder diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index e0bed7a0..57ec4e40 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import SwiftJavaConfigurationShared import SwiftJavaJNICore import SwiftSyntax diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift index e1f5887c..1cac3a5b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import SwiftJavaConfigurationShared import SwiftJavaJNICore import SwiftSyntax diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 833fce16..c27b6f30 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import Foundation import OrderedCollections import SwiftJavaJNICore diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index fe72f2dd..5a573105 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import SwiftJavaConfigurationShared import SwiftJavaJNICore diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index dc0ecde9..514d88e8 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import SwiftJavaConfigurationShared import SwiftJavaJNICore diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 0f2ab299..8139ef28 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import SwiftJavaJNICore import SwiftSyntax diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 2acaab6e..3d0c97b7 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import SwiftJavaConfigurationShared import SwiftJavaJNICore diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift index e5f5cca5..abac0920 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftSymbolTable.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import SwiftSyntax package protocol SwiftSymbolTableProtocol { diff --git a/Sources/JExtractSwiftLib/TranslatedDocumentation.swift b/Sources/JExtractSwiftLib/TranslatedDocumentation.swift index 456df699..54c00cde 100644 --- a/Sources/JExtractSwiftLib/TranslatedDocumentation.swift +++ b/Sources/JExtractSwiftLib/TranslatedDocumentation.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import SwiftSyntax enum TranslatedDocumentation { diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index b68c0d39..d573cf81 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -100,6 +100,10 @@ public struct Configuration: Codable { // Java dependencies we need to fetch for this target. public var dependencies: [JavaDependencyDescriptor]? + /// Custom Maven repositories to use when resolving dependencies. + /// If not set, defaults to mavenCentral(). + public var mavenRepositories: [MavenRepositoryDescriptor]? + public init() { } @@ -154,6 +158,101 @@ public struct JavaDependencyDescriptor: Hashable, Codable { } } +// ==== ----------------------------------------------------------------------- +// MARK: MavenRepositoryDescriptor + +/// Describes a Maven-style repository for dependency resolution. +/// +/// Supported types based on https://docs.gradle.org/current/userguide/supported_repository_types.html: +/// - `maven(url:artifactUrls:)` — A custom Maven repository at the given URL +/// - `mavenCentral` — Maven Central repository +/// - `mavenLocal(includeGroups:)` — Local Maven cache (~/.m2/repository) +/// - `google` — Google's Maven repository +public enum MavenRepositoryDescriptor: Hashable, Codable { + case maven(url: String, artifactUrls: [String]? = nil) + case mavenCentral + case mavenLocal(includeGroups: [String]? = nil) + case google + + enum CodingKeys: String, CodingKey { + case type + case url + case artifactUrls + case includeGroups + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + switch type { + case "maven": + let url = try container.decode(String.self, forKey: .url) + let artifactUrls = try container.decodeIfPresent([String].self, forKey: .artifactUrls) + self = .maven(url: url, artifactUrls: artifactUrls) + case "mavenCentral": + self = .mavenCentral + case "mavenLocal": + let includeGroups = try container.decodeIfPresent([String].self, forKey: .includeGroups) + self = .mavenLocal(includeGroups: includeGroups) + case "google": + self = .google + default: + throw DecodingError.dataCorruptedError( + forKey: .type, + in: container, + debugDescription: "Unknown repository type: '\(type)'. Supported: maven, mavenCentral, mavenLocal, google" + ) + } + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .maven(let url, let artifactUrls): + try container.encode("maven", forKey: .type) + try container.encode(url, forKey: .url) + try container.encodeIfPresent(artifactUrls, forKey: .artifactUrls) + case .mavenCentral: + try container.encode("mavenCentral", forKey: .type) + case .mavenLocal(let includeGroups): + try container.encode("mavenLocal", forKey: .type) + try container.encodeIfPresent(includeGroups, forKey: .includeGroups) + case .google: + try container.encode("google", forKey: .type) + } + } + + /// Render this repository as Gradle DSL. + public var gradleDSL: String { + switch self { + case .maven(let url, let artifactUrls): + var result = "maven {\n" + result += " url = uri(\"\(url)\")\n" + if let artifactUrls, !artifactUrls.isEmpty { + result += " artifactUrls = [\(artifactUrls.map { "\"\($0)\"" }.joined(separator: ", "))]\n" + } + result += "}" + return result + case .mavenCentral: + return "mavenCentral()" + case .mavenLocal(let includeGroups): + if let includeGroups, !includeGroups.isEmpty { + var result = "mavenLocal {\n" + result += " content {\n" + for group in includeGroups { + result += " includeGroup(\"\(group)\")\n" + } + result += " }\n" + result += "}" + return result + } + return "mavenLocal()" + case .google: + return "google()" + } + } +} + public func readConfiguration(sourceDir: String, file: String = #fileID, line: UInt = #line) throws -> Configuration? { // Workaround since filePath is macOS 13 let sourcePath = diff --git a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift index e233f535..8f8f1a82 100644 --- a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift +++ b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift @@ -15,18 +15,11 @@ import ArgumentParser import Foundation import JavaUtilJar -import Subprocess import SwiftJava import SwiftJavaConfigurationShared import SwiftJavaShared import SwiftJavaToolLib -#if canImport(System) -import System -#else -@preconcurrency import SystemPackage -#endif - typealias Configuration = SwiftJavaConfigurationShared.Configuration extension SwiftJava { @@ -71,9 +64,7 @@ extension SwiftJava.ResolveCommand { } if dependenciesToResolve.isEmpty { - print( - "[warn][swift-java] Attempted to 'resolve' dependencies but no dependencies specified in swift-java.config or command input!" - ) + log.warning("Attempted to 'resolve' dependencies but no dependencies specified in swift-java.config or command input!") return } @@ -107,7 +98,7 @@ extension SwiftJava.ResolveCommand { dependencies: [JavaDependencyDescriptor] ) async throws -> ResolvedDependencyClasspath { let deps = dependencies.map { $0.descriptionGradleStyle } - print("[debug][swift-java] Resolve and fetch dependencies for: \(deps)") + log.debug("Resolve and fetch dependencies for: \(deps)") let workDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) .appendingPathComponent(".build") @@ -115,14 +106,10 @@ extension SwiftJava.ResolveCommand { let dependenciesClasspath = await resolveDependencies(workDir: workDir, dependencies: dependencies) let classpathEntries = dependenciesClasspath.split(separator: ":") - print( - "[info][swift-java] Resolved classpath for \(deps.count) dependencies of '\(swiftModule)', classpath entries: \(classpathEntries.count), ", - terminator: "" - ) - print("done.".green) + log.info("Resolved classpath for \(deps.count) dependencies of '\(swiftModule)', classpath entries: \(classpathEntries.count)") for entry in classpathEntries { - print("[info][swift-java] Classpath entry: \(entry)") + log.info("Classpath entry: \(entry)") } return ResolvedDependencyClasspath(for: dependencies, classpath: dependenciesClasspath) @@ -132,100 +119,22 @@ extension SwiftJava.ResolveCommand { /// /// - Parameter dependencies: maven-style dependencies to resolve /// - Returns: Colon-separated classpath - func resolveDependencies(workDir: URL, dependencies: [JavaDependencyDescriptor]) async -> String { - print("Create directory: \(workDir.absoluteString)") + func resolveDependencies( + workDir: URL, + dependencies: [JavaDependencyDescriptor], + mavenRepositories: [MavenRepositoryDescriptor]? = nil + ) async -> String { + log.debug("Create directory: \(workDir.absoluteString)") + + var resolveConfig = SwiftJavaConfigurationShared.Configuration() + resolveConfig.dependencies = dependencies + resolveConfig.mavenRepositories = mavenRepositories - let resolverDir: URL do { - resolverDir = try createTemporaryDirectory(in: workDir) + return try await JavaDependencyResolver.resolve(config: resolveConfig, workDir: workDir) } catch { - fatalError("Unable to create temp directory at: \(workDir.absoluteString)! \(error)") - } - defer { - try? FileManager.default.removeItem(at: resolverDir) + fatalError("Failed to resolve dependencies: \(error)") } - - // We try! because it's easier to track down errors like this than when we bubble up the errors, - // and don't get great diagnostics or backtraces due to how swiftpm plugin tools are executed. - - try! copyGradlew(to: resolverDir) - - try! printGradleProject(directory: resolverDir, dependencies: dependencies) - - if #available(macOS 15, *) { - let process = try! await Subprocess.run( - .path(FilePath(resolverDir.appendingPathComponent("gradlew").path)), - arguments: [ - "--no-daemon", - "--rerun-tasks", - "\(printRuntimeClasspathTaskName)", - ], - workingDirectory: Optional(FilePath(resolverDir.path)), - // TODO: we could move to stream processing the outputs - output: .string(limit: Int.max, encoding: UTF8.self), // Don't limit output, we know it will be reasonable size - error: .string(limit: Int.max, encoding: UTF8.self) // Don't limit output, we know it will be reasonable size - ) - - let outString = process.standardOutput ?? "" - let errString = process.standardError ?? "" - - let classpathOutput: String - if let found = outString.split(separator: "\n").first(where: { $0.hasPrefix(self.SwiftJavaClasspathPrefix) }) { - classpathOutput = String(found) - } else if let found = errString.split(separator: "\n").first(where: { - $0.hasPrefix(self.SwiftJavaClasspathPrefix) - }) { - classpathOutput = String(found) - } else { - let suggestDisablingSandbox = - "It may be that the Sandbox has prevented dependency fetching, please re-run with '--disable-sandbox'." - fatalError( - "Gradle output had no SWIFT_JAVA_CLASSPATH! \(suggestDisablingSandbox). \n" - + "Command was: \(CommandLine.arguments.joined(separator: " ").bold)\n" - + "Output was: <<<\(outString)>>>;\n" + "Err was: <<<\(errString)>>>" - ) - } - - return String(classpathOutput.dropFirst(SwiftJavaClasspathPrefix.count)) - } else { - // Subprocess is unavailable - fatalError("Subprocess is unavailable yet required to execute `gradlew` subprocess. Please update to macOS 15+") - } - } - - /// Creates Gradle project files (build.gradle, settings.gradle.kts) in temporary directory. - func printGradleProject(directory: URL, dependencies: [JavaDependencyDescriptor]) throws { - let buildGradle = - directory - .appendingPathComponent("build.gradle", isDirectory: false) - - let buildGradleText = - """ - plugins { id 'java-library' } - repositories { mavenCentral() } - - dependencies { - \(dependencies.map({ dep in "implementation(\"\(dep.descriptionGradleStyle)\")" }).joined(separator: ",\n")) - } - - tasks.register("printRuntimeClasspath") { - def runtimeClasspath = sourceSets.main.runtimeClasspath - inputs.files(runtimeClasspath) - doLast { - println("\(SwiftJavaClasspathPrefix)${runtimeClasspath.asPath}") - } - } - """ - try buildGradleText.write(to: buildGradle, atomically: true, encoding: .utf8) - - let settingsGradle = - directory - .appendingPathComponent("settings.gradle.kts", isDirectory: false) - let settingsGradleText = - """ - rootProject.name = "swift-java-resolve-temp-project" - """ - try settingsGradleText.write(to: settingsGradle, atomically: true, encoding: .utf8) } /// Creates {MySwiftModule}.swift.classpath in the --output-directory. @@ -247,7 +156,7 @@ extension SwiftJava.ResolveCommand { let contents = resolvedClasspath.classpath let filename = "\(swiftModule).swift-java.classpath" - print("[debug][swift-java] Write resolved dependencies to: \(outputDirectory)/\(filename)") + log.debug("Write resolved dependencies to: \(outputDirectory)/\(filename)") // Write the file try writeContents( @@ -264,60 +173,6 @@ extension SwiftJava.ResolveCommand { return camelCased } - // copy gradlew & gradle.bat from root, throws error if there is no gradle setup. - func copyGradlew(to resolverWorkDirectory: URL) throws { - var searchDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) - - while searchDir.pathComponents.count > 1 { - let gradlewFile = searchDir.appendingPathComponent("gradlew") - let gradlewExists = FileManager.default.fileExists(atPath: gradlewFile.path) - guard gradlewExists else { - searchDir = searchDir.deletingLastPathComponent() - continue - } - - let gradlewBatFile = searchDir.appendingPathComponent("gradlew.bat") - let gradlewBatExists = FileManager.default.fileExists(atPath: gradlewFile.path) - - let gradleDir = searchDir.appendingPathComponent("gradle") - let gradleDirExists = FileManager.default.fileExists(atPath: gradleDir.path) - guard gradleDirExists else { - searchDir = searchDir.deletingLastPathComponent() - continue - } - - // TODO: gradle.bat as well - try? FileManager.default.copyItem( - at: gradlewFile, - to: resolverWorkDirectory.appendingPathComponent("gradlew") - ) - if gradlewBatExists { - try? FileManager.default.copyItem( - at: gradlewBatFile, - to: resolverWorkDirectory.appendingPathComponent("gradlew.bat") - ) - } - try? FileManager.default.copyItem( - at: gradleDir, - to: resolverWorkDirectory.appendingPathComponent("gradle") - ) - return - } - } - - func createTemporaryDirectory(in directory: URL) throws -> URL { - let uuid = UUID().uuidString - let resolverDirectoryURL = directory.appendingPathComponent("swift-java-dependencies-\(uuid)") - - try FileManager.default.createDirectory( - at: resolverDirectoryURL, - withIntermediateDirectories: true, - attributes: nil - ) - - return resolverDirectoryURL - } - } struct ResolvedDependencyClasspath: CustomStringConvertible { diff --git a/Sources/SwiftJavaToolLib/JavaDependencyResolver.swift b/Sources/SwiftJavaToolLib/JavaDependencyResolver.swift new file mode 100644 index 00000000..5a7336bf --- /dev/null +++ b/Sources/SwiftJavaToolLib/JavaDependencyResolver.swift @@ -0,0 +1,229 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import CodePrinting +import Foundation +import Subprocess +import SwiftJavaConfigurationShared + +#if canImport(System) +import System +#else +@preconcurrency import SystemPackage +#endif + +// ==== ----------------------------------------------------------------------- +// MARK: JavaDependencyResolver + +/// Resolves Java/Maven dependencies using Gradle, with support for custom repositories. +/// +/// The resolver creates a temporary Gradle project, runs dependency resolution, +/// and returns the resulting classpath. +public struct JavaDependencyResolver { + + static let SwiftJavaClasspathPrefix = "SWIFT_JAVA_CLASSPATH:" + static let printRuntimeClasspathTaskName = "printRuntimeClasspath" + + // ==== ------------------------------------------------------------------- + // MARK: API + + /// Resolve dependencies and return the classpath string. + /// + /// - Parameters: + /// - config: Configuration containing dependencies and optional repositories. + /// - workDir: Working directory for creating the temporary Gradle project. + /// - Returns: Colon-separated classpath string of resolved dependencies. + public static func resolve( + config: SwiftJavaConfigurationShared.Configuration, + workDir: URL + ) async throws -> String { + let dependencies = config.dependencies ?? [] + guard !dependencies.isEmpty else { + throw JavaDependencyResolverError.noDependencies + } + + let resolverDir = try createTemporaryDirectory(in: workDir) + defer { + try? FileManager.default.removeItem(at: resolverDir) + } + + try copyGradlew(to: resolverDir) + try writeGradleProject( + directory: resolverDir, + dependencies: dependencies, + repositories: config.mavenRepositories ?? [.mavenCentral] + ) + + return try await runGradle(in: resolverDir) + } + + // ==== ------------------------------------------------------------------- + // MARK: Gradle project generation + + /// Write build.gradle and settings.gradle.kts into the given directory. + static func writeGradleProject( + directory: URL, + dependencies: [JavaDependencyDescriptor], + repositories: [MavenRepositoryDescriptor] = [.mavenCentral] + ) throws { + let buildGradleText = printBuildGradle(dependencies: dependencies, repositories: repositories) + let buildGradle = directory.appendingPathComponent("build.gradle", isDirectory: false) + try buildGradleText.write(to: buildGradle, atomically: true, encoding: .utf8) + + let settingsGradle = directory.appendingPathComponent("settings.gradle.kts", isDirectory: false) + let settingsGradleText = """ + rootProject.name = "swift-java-resolve-temp-project" + """ + try settingsGradleText.write(to: settingsGradle, atomically: true, encoding: .utf8) + } + + /// Generate the Gradle build file content as a string. + public static func printBuildGradle( + dependencies: [JavaDependencyDescriptor], + repositories: [MavenRepositoryDescriptor] + ) -> String { + var p = CodePrinter() + p.indentationPart = " " + + p.print("plugins { id 'java-library' }") + + p.printBraceBlock("repositories") { p in + for repo in repositories { + p.print(repo.gradleDSL) + } + } + + p.println() + + p.printBraceBlock("dependencies") { p in + for dep in dependencies { + p.print("implementation(\"\(dep.descriptionGradleStyle)\")") + } + } + + p.println() + + p.printBraceBlock("tasks.register(\"\(printRuntimeClasspathTaskName)\")") { p in + p.print("def runtimeClasspath = sourceSets.main.runtimeClasspath") + p.print("inputs.files(runtimeClasspath)") + p.printBraceBlock("doLast") { p in + p.print("println(\"\(SwiftJavaClasspathPrefix)${runtimeClasspath.asPath}\")") + } + } + + return p.finalize() + } + + // ==== ------------------------------------------------------------------- + // MARK: Gradle execution + + static func runGradle(in resolverDir: URL) async throws -> String { + let process = try await Subprocess.run( + .path(FilePath(resolverDir.appendingPathComponent("gradlew").path)), + arguments: [ + "--no-daemon", + "--rerun-tasks", + printRuntimeClasspathTaskName, + ], + workingDirectory: Optional(FilePath(resolverDir.path)), + output: .string(limit: Int.max, encoding: UTF8.self), + error: .string(limit: Int.max, encoding: UTF8.self) + ) + + let outString = process.standardOutput ?? "" + let errString = process.standardError ?? "" + + if let found = outString.split(separator: "\n").first(where: { $0.hasPrefix(SwiftJavaClasspathPrefix) }) { + return String(found.dropFirst(SwiftJavaClasspathPrefix.count)) + } else if let found = errString.split(separator: "\n").first(where: { $0.hasPrefix(SwiftJavaClasspathPrefix) }) { + return String(found.dropFirst(SwiftJavaClasspathPrefix.count)) + } + + throw JavaDependencyResolverError.gradleFailed( + message: "Gradle output had no SWIFT_JAVA_CLASSPATH. " + + "It may be that the Sandbox has prevented dependency fetching, please re-run with '--disable-sandbox'.\n" + + "Output: \(outString)\nErr: \(errString)" + ) + } + + // ==== ------------------------------------------------------------------- + // MARK: File utilities + + static func createTemporaryDirectory(in directory: URL) throws -> URL { + let uuid = UUID().uuidString + let resolverDirectoryURL = directory.appendingPathComponent("swift-java-dependencies-\(uuid)") + try FileManager.default.createDirectory( + at: resolverDirectoryURL, + withIntermediateDirectories: true, + attributes: nil + ) + return resolverDirectoryURL + } + + static func copyGradlew(to resolverWorkDirectory: URL) throws { + var searchDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + + while searchDir.pathComponents.count > 1 { + let gradlewFile = searchDir.appendingPathComponent("gradlew") + let gradlewExists = FileManager.default.fileExists(atPath: gradlewFile.path) + guard gradlewExists else { + searchDir = searchDir.deletingLastPathComponent() + continue + } + + let gradlewBatFile = searchDir.appendingPathComponent("gradlew.bat") + let gradlewBatExists = FileManager.default.fileExists(atPath: gradlewBatFile.path) + + let gradleDir = searchDir.appendingPathComponent("gradle") + let gradleDirExists = FileManager.default.fileExists(atPath: gradleDir.path) + guard gradleDirExists else { + searchDir = searchDir.deletingLastPathComponent() + continue + } + + try? FileManager.default.copyItem( + at: gradlewFile, + to: resolverWorkDirectory.appendingPathComponent("gradlew") + ) + if gradlewBatExists { + try? FileManager.default.copyItem( + at: gradlewBatFile, + to: resolverWorkDirectory.appendingPathComponent("gradlew.bat") + ) + } + try? FileManager.default.copyItem( + at: gradleDir, + to: resolverWorkDirectory.appendingPathComponent("gradle") + ) + return + } + } +} + +// ==== ----------------------------------------------------------------------- +// MARK: Errors + +public enum JavaDependencyResolverError: Error, CustomStringConvertible { + case noDependencies + case gradleFailed(message: String) + + public var description: String { + switch self { + case .noDependencies: + return "No dependencies specified in swift-java.config" + case .gradleFailed(let message): + return message + } + } +} diff --git a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift index d559187f..f0e1e72a 100644 --- a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import JExtractSwiftLib import SwiftJavaConfigurationShared import Testing diff --git a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift index 79b51c19..07b60c60 100644 --- a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift +++ b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import JExtractSwiftLib import SwiftJavaConfigurationShared import Testing diff --git a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift index b6ae6f6c..c514a12f 100644 --- a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift +++ b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import JExtractSwiftLib import SwiftJavaConfigurationShared import Testing diff --git a/Tests/JExtractSwiftTests/MethodImportTests.swift b/Tests/JExtractSwiftTests/MethodImportTests.swift index 90c108ec..93dd4ed3 100644 --- a/Tests/JExtractSwiftTests/MethodImportTests.swift +++ b/Tests/JExtractSwiftTests/MethodImportTests.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import CodePrinting import JExtractSwiftLib import SwiftJavaConfigurationShared import Testing diff --git a/Tests/SwiftJavaToolLibTests/JavaDependencyResolverTests.swift b/Tests/SwiftJavaToolLibTests/JavaDependencyResolverTests.swift new file mode 100644 index 00000000..b7dd2a1b --- /dev/null +++ b/Tests/SwiftJavaToolLibTests/JavaDependencyResolverTests.swift @@ -0,0 +1,186 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import SwiftJavaConfigurationShared +import SwiftJavaToolLib +import Testing + +// ==== ----------------------------------------------------------------------- +// MARK: TestFixtureError + +enum TestFixtureError: Error, CustomStringConvertible { + case gradlewNotFound(searchedFrom: String) + case gradlePublishFailed(exitCode: Int32, output: String) + + var description: String { + switch self { + case .gradlewNotFound(let path): + return "Could not find gradlew in parent directories of \(path)" + case .gradlePublishFailed(let exitCode, let output): + return "Gradle publish failed (exit code \(exitCode)): \(output)" + } + } +} + +// ==== ----------------------------------------------------------------------- +// MARK: JavaDependencyResolverTests + +@Suite +struct JavaDependencyResolverTests { + + /// The path to the SimpleJavaProject test fixture. + static var simpleJavaProjectDir: URL { + let thisFile = URL(fileURLWithPath: #filePath) + return thisFile.deletingLastPathComponent().appendingPathComponent("SimpleJavaProject") + } + + /// Search parent directories for a `gradlew` wrapper script. + private static func findGradlew(startingFrom directory: URL) throws -> URL { + var searchDir = directory + while searchDir.pathComponents.count > 1 { + let candidate = searchDir.appendingPathComponent("gradlew") + if FileManager.default.fileExists(atPath: candidate.path) { + return candidate + } + searchDir = searchDir.deletingLastPathComponent() + } + throw TestFixtureError.gradlewNotFound(searchedFrom: directory.path) + } + + /// Publish the SimpleJavaProject to a local maven repo. + static func publishSampleJavaProject(to repoDir: URL) throws { + let fm = FileManager.default + if fm.fileExists(atPath: repoDir.appendingPathComponent("com/example/hello-world/1.0.0").path) { + return // Already published + } + + try fm.createDirectory(at: repoDir, withIntermediateDirectories: true) + + let gradlew = try findGradlew(startingFrom: simpleJavaProjectDir) + + let process = Process() + process.executableURL = gradlew + process.arguments = [ + "--no-daemon", + "-p", simpleJavaProjectDir.path, + "publishMavenPublicationToLocalRepository", + "-PrepoDir=\(repoDir.path)", + ] + process.currentDirectoryURL = simpleJavaProjectDir + + let pipe = Pipe() + process.standardOutput = pipe + process.standardError = pipe + + try process.run() + process.waitUntilExit() + + if process.terminationStatus != 0 { + let output = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" + throw TestFixtureError.gradlePublishFailed(exitCode: process.terminationStatus, output: output) + } + } + + // ==== ------------------------------------------------------------------- + // MARK: Tests + + @Test(.tags(.slow, .gradle, .integration)) + func resolveFromLocalRepo() async throws { + let tempDir = FileManager.default.temporaryDirectory + .appendingPathComponent("swift-java-test-\(UUID().uuidString)") + defer { try? FileManager.default.removeItem(at: tempDir) } + + let repoDir = tempDir.appendingPathComponent("local-repo") + try Self.publishSampleJavaProject(to: repoDir) + + var config = Configuration() + config.dependencies = [ + JavaDependencyDescriptor(groupID: "com.example", artifactID: "hello-world", version: "1.0.0") + ] + config.mavenRepositories = [ + .maven(url: repoDir.path) + ] + + let workDir = tempDir.appendingPathComponent("work") + try FileManager.default.createDirectory(at: workDir, withIntermediateDirectories: true) + + let classpath = try await JavaDependencyResolver.resolve(config: config, workDir: workDir) + #expect(!classpath.isEmpty, "Classpath should not be empty") + #expect( + classpath.contains("hello-world"), + "Classpath should contain hello-world artifact, got: \(classpath)" + ) + } + + @Test(.tags(.slow, .gradle, .integration)) + func resolveNonExistentDependency_fails() async throws { + let tempDir = FileManager.default.temporaryDirectory + .appendingPathComponent("swift-java-test-\(UUID().uuidString)") + defer { try? FileManager.default.removeItem(at: tempDir) } + + let repoDir = tempDir.appendingPathComponent("local-repo") + try Self.publishSampleJavaProject(to: repoDir) + + var config = Configuration() + config.dependencies = [ + JavaDependencyDescriptor(groupID: "com.nonexistent", artifactID: "missing-lib", version: "1.0.0") + ] + // Only look in our local repo, should fail since com.nonexistent doesn't exist + config.mavenRepositories = [ + .maven(url: repoDir.path) + ] + + let workDir = tempDir.appendingPathComponent("work") + try FileManager.default.createDirectory(at: workDir, withIntermediateDirectories: true) + + await #expect(throws: JavaDependencyResolverError.self) { + _ = try await JavaDependencyResolver.resolve(config: config, workDir: workDir) + } + } + + @Test(.tags(.slow, .gradle, .integration)) + func resolveFromLocalRepo_withIncludeGroupsFilter() async throws { + let tempDir = FileManager.default.temporaryDirectory + .appendingPathComponent("swift-java-test-\(UUID().uuidString)") + defer { try? FileManager.default.removeItem(at: tempDir) } + + let repoDir = tempDir.appendingPathComponent("local-repo") + try Self.publishSampleJavaProject(to: repoDir) + + var config = Configuration() + config.dependencies = [ + JavaDependencyDescriptor(groupID: "com.example", artifactID: "hello-world", version: "1.0.0") + ] + config.mavenRepositories = [ + .maven(url: repoDir.path) + ] + + let workDir = tempDir.appendingPathComponent("work") + try FileManager.default.createDirectory(at: workDir, withIntermediateDirectories: true) + + let classpath = try await JavaDependencyResolver.resolve(config: config, workDir: workDir) + #expect(classpath.contains("hello-world")) + } + + @Test + func resolveNoDependencies_throws() async throws { + let config = Configuration() + let workDir = FileManager.default.temporaryDirectory + + await #expect(throws: JavaDependencyResolverError.self) { + _ = try await JavaDependencyResolver.resolve(config: config, workDir: workDir) + } + } +} diff --git a/Tests/SwiftJavaToolLibTests/SimpleJavaProject/build.gradle b/Tests/SwiftJavaToolLibTests/SimpleJavaProject/build.gradle new file mode 100644 index 00000000..ada22b35 --- /dev/null +++ b/Tests/SwiftJavaToolLibTests/SimpleJavaProject/build.gradle @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +plugins { + id 'java-library' + id 'maven-publish' +} + +group = 'com.example' +version = '1.0.0' + +repositories { + mavenCentral() +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +publishing { + publications { + maven(MavenPublication) { + from components.java + + groupId = 'com.example' + artifactId = 'hello-world' + version = '1.0.0' + } + } + repositories { + maven { + name = 'local' + url = project.hasProperty('repoDir') ? uri(project.property('repoDir')) : layout.buildDirectory.dir('repo') + } + } +} diff --git a/Tests/SwiftJavaToolLibTests/SimpleJavaProject/settings.gradle b/Tests/SwiftJavaToolLibTests/SimpleJavaProject/settings.gradle new file mode 100644 index 00000000..85851bd7 --- /dev/null +++ b/Tests/SwiftJavaToolLibTests/SimpleJavaProject/settings.gradle @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + } +} + +rootProject.name = 'simple-java-project' diff --git a/Tests/SwiftJavaToolLibTests/SimpleJavaProject/src/main/java/com/example/HelloWorld.java b/Tests/SwiftJavaToolLibTests/SimpleJavaProject/src/main/java/com/example/HelloWorld.java new file mode 100644 index 00000000..f46bfa4c --- /dev/null +++ b/Tests/SwiftJavaToolLibTests/SimpleJavaProject/src/main/java/com/example/HelloWorld.java @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example; + +/** + * A simple HelloWorld class used for testing swift-java dependency resolution. + */ +public class HelloWorld { + + private final String greeting; + + public HelloWorld() { + this.greeting = "Hello, World!"; + } + + public HelloWorld(String greeting) { + this.greeting = greeting; + } + + public String getGreeting() { + return greeting; + } + + @Override + public String toString() { + return "HelloWorld{greeting='" + greeting + "'}"; + } +} diff --git a/Tests/SwiftJavaToolLibTests/Testing+Tags.swift b/Tests/SwiftJavaToolLibTests/Testing+Tags.swift new file mode 100644 index 00000000..0b4c3d7f --- /dev/null +++ b/Tests/SwiftJavaToolLibTests/Testing+Tags.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +extension Tag { + @Tag static var slow: Self + @Tag static var gradle: Self + @Tag static var integration: Self +}