diff --git a/Package.swift b/Package.swift index 1c0e9481..646b1f80 100644 --- a/Package.swift +++ b/Package.swift @@ -453,7 +453,7 @@ let package = Package( .testTarget( name: "SwiftJavaTests", dependencies: [ - "SwiftJava", + "SwiftJava", "JavaNet" ], swiftSettings: [ @@ -488,6 +488,9 @@ let package = Package( dependencies: [ "SwiftJavaToolLib" ], + exclude: [ + "SimpleJavaProject", + ], swiftSettings: [ .swiftLanguageMode(.v5), .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) diff --git a/Samples/JavaDependencySampleApp/Package.swift b/Samples/JavaDependencySampleApp/Package.swift index 573a60cc..48456a46 100644 --- a/Samples/JavaDependencySampleApp/Package.swift +++ b/Samples/JavaDependencySampleApp/Package.swift @@ -67,7 +67,8 @@ let package = Package( .product(name: "SwiftJava", package: "swift-java"), .product(name: "CSwiftJavaJNI", package: "swift-java"), .product(name: "JavaUtilFunction", package: "swift-java"), - "JavaCommonsCSV" + "JavaCommonsCSV", + "OrgAndrejsJson", ], exclude: ["swift-java.config"], swiftSettings: [ @@ -99,6 +100,25 @@ let package = Package( ] ), + .target( + name: "OrgAndrejsJson", + dependencies: [ + .product(name: "SwiftJava", package: "swift-java"), + .product(name: "JavaUtilFunction", package: "swift-java"), + .product(name: "JavaUtil", package: "swift-java"), + .product(name: "JavaIO", package: "swift-java"), + .product(name: "JavaNet", package: "swift-java"), + ], + exclude: ["swift-java.config"], + swiftSettings: [ + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]), + .swiftLanguageMode(.v5), + ], + plugins: [ + .plugin(name: "SwiftJavaPlugin", package: "swift-java"), + ] + ), + .target(name: "JavaExample"), ] diff --git a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/OrgAndrejsJsonTests.swift b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/OrgAndrejsJsonTests.swift new file mode 100644 index 00000000..e5c15c07 --- /dev/null +++ b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/OrgAndrejsJsonTests.swift @@ -0,0 +1,50 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 SwiftJava + +// Import the json library wrapper: +import OrgAndrejsJson + +enum OrgAndrejsJsonTests { + static func run() async throws { + print("Now testing Json library...") + + let json = Json(#"{"host": "localhost", "port": 80}"#) + + precondition(json.hasOwnProperty("port")) + + print(json.get("port").toString()) + precondition(json.get("port").as(JavaInteger.self)!.intValue() == 80) + + print("Reading swift-java.config inside OrgAndrejsJson folder...") + + let configPath = String.currentWorkingDirectory.appending("/Sources/OrgAndrejsJson/swift-java.config") + + let config = try JavaClass().of.url("file://" + configPath)! + + precondition(config.hasOwnProperty("repositories")) + + print(config.toString()) + } +} + +private extension String { + static var currentWorkingDirectory: Self { + let path = getcwd(nil, 0)! + defer { free(path) } + return String(cString: path) + } +} diff --git a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift index 13ea6eed..28b7426b 100644 --- a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift +++ b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift @@ -52,4 +52,6 @@ for record in try CSVFormatClass.RFC4180.parse(reader)!.getRecords()! { } } +try await OrgAndrejsJsonTests.run() + print("Done.") diff --git a/Samples/JavaDependencySampleApp/Sources/OrgAndrejsJson/dummy.swift b/Samples/JavaDependencySampleApp/Sources/OrgAndrejsJson/dummy.swift new file mode 100644 index 00000000..76f848f9 --- /dev/null +++ b/Samples/JavaDependencySampleApp/Sources/OrgAndrejsJson/dummy.swift @@ -0,0 +1,13 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// diff --git a/Samples/JavaDependencySampleApp/Sources/OrgAndrejsJson/swift-java.config b/Samples/JavaDependencySampleApp/Sources/OrgAndrejsJson/swift-java.config new file mode 100644 index 00000000..1e9fe5da --- /dev/null +++ b/Samples/JavaDependencySampleApp/Sources/OrgAndrejsJson/swift-java.config @@ -0,0 +1,15 @@ +{ + "classes": { + "org.andrejs.json.Json": "Json", + "org.andrejs.json.JsonFactory": "JsonFactory" + }, + "dependencies": [ + "org.andrejs:json:1.2" + ], + "repositories": [ + { + "type": "maven", + "url": "https://jitpack.io" + } + ] +} diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index 2d9b4311..db72b824 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -80,6 +80,12 @@ public struct Configuration: Codable { // Java dependencies we need to fetch for this target. public var dependencies: [JavaDependencyDescriptor]? + /// Maven repositories for this target when fetching dependencies. + /// + /// `mavenCentral()` will always be used. + /// + /// Reference: [Repository Types](https://docs.gradle.org/current/userguide/supported_repository_types.html) + public var repositories: [JavaRepositoryDescriptor]? public init() { } @@ -133,6 +139,82 @@ public struct JavaDependencyDescriptor: Hashable, Codable { } } +/// Descriptor for [repositories](https://docs.gradle.org/current/userguide/supported_repository_types.html#sec:maven-repo) +public enum JavaRepositoryDescriptor: Hashable, Codable, Equatable { + + /// Haven't found a proper way to test credentials, packages that need to download from private repo can be downloaded by maven and then use local repo instead + /// + /// References: + /// - [Maven repositories](https://docs.gradle.org/current/userguide/supported_repository_types.html#sec:maven-repo) + /// - [Artifacts](https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.repositories.MavenArtifactRepository.html#:~:text=urls)-,Adds%20some%20additional%20URLs%20to%20use%20to%20find%20artifact%20files.%20Note%20that%20these%20URLs%20are%20not%20used%20to%20find%20POM%20files.,-The) + case maven(url: String, artifactUrls: [String]? = nil) + case mavenLocal(includeGroups: [String]? = nil) + case other(_ type: String) + + enum CodingKeys: String, CodingKey { case type, url, artifactUrls, credentials, includeGroups } + + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + let type = try c.decode(String.self, forKey: .type) + switch type { + case "maven": + self = try .maven( + url: c.decode(String.self, forKey: .url), + artifactUrls: try? c.decode([String].self, forKey: .artifactUrls), + ) + case "mavenLocal": + self = .mavenLocal(includeGroups: try? c.decode([String].self, forKey: .includeGroups)) + default: + self = .other(type) + } + } + + public func encode(to encoder: Encoder) throws { + var c = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .maven(url, artifactUrls/*, creds*/): + try c.encode("maven", forKey: .type) + try c.encode(url, forKey: .url) + if let artifactUrls = artifactUrls { + try c.encode(artifactUrls, forKey: .artifactUrls) + } + case let .mavenLocal(includeGroups): + try c.encode("mavenLocal", forKey: .type) + if let gs = includeGroups { + try c.encode(gs, forKey: .includeGroups) + } + case let .other(type): + try c.encode("\(type)", forKey: .type) + } + } + + public func renderGradleRepository() -> String? { + switch self { + case let .maven(url, artifactUrls): + return """ + maven { + url = uri("\(url)") + \((artifactUrls ?? []).map({ "artifactUrls(\"\($0)\")" }).joined(separator: "\n")) + } + """ + case let .mavenLocal(groups): + if let gs = groups { + return """ + mavenLocal { + content { + \(gs.map({ "includeGroup(\"\($0)\")" }).joined(separator: "\n")) + } + } + """ + } else { + return "mavenLocal()" + } + case let .other(type): + return "\(type)()" + } + } +} + 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/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md index 32a82eb4..cd493be7 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md @@ -199,9 +199,89 @@ struct HelloSwiftMain: ParsableCommand { ### Download Java dependencies in Swift builds: swift-java resolve -> TIP: See the `Samples/DependencySampleApp` for a fully functional showcase of this mode. +> TIP: See the `Samples/JavaDependencySampleApp` for a fully functional showcase of this mode. -TODO: documentation on this feature + +The `swift-java resolve` command automates the process of downloading and resolving Java dependencies for your Swift project. This is configured through your `swift-java.config` file, where you can declare both the dependencies you need and the repositories from which to fetch them. + +To get started, add a `dependencies` array to your configuration file, listing the Maven coordinates for each required library (e.g., `group:artifact:version`). You may also include a `repositories` array to specify custom Maven repositories. For example: + +```json +{ + "classes": { + "org.apache.commons.io.FilenameUtils": "FilenameUtils", + "org.apache.commons.io.IOCase": "IOCase", + "org.apache.commons.csv.CSVFormat": "CSVFormat", + "org.apache.commons.csv.CSVParser": "CSVParser", + "org.apache.commons.csv.CSVRecord": "CSVRecord" + }, + "dependencies": [ + "org.apache.commons:commons-csv:1.12.0" + ] +} +``` + +To resolve and download these dependencies, run: + +```bash +# See Samples/JavaDependencySampleApp/ci-validate.sh for a complete example +swift-java resolve \ + swift-java.config \ + --swift-module JavaCommonsCSV \ + --output-directory .build/plugins/JavaCommonsCSV/destination/SwiftJavaPlugin/ +``` + +The tool will fetch all specified dependencies from the repositories listed in your config (or Maven Central by default), and generate a `swift-java.classpath` file. This file is then used for building and running your Swift-Java interop code. + +If you do not specify any `repositories`, dependencies are resolved from Maven Central. To use a custom or private repository, add it to the `repositories` array, for example: + +```json +{ + "repositories": [ + { "type": "maven", "url": "https://repo.mycompany.com/maven2" }, + { + "type": "maven", + "url": "https://repo2.mycompany.com/maven2", + "artifactUrls": [ + "https://repo.mycompany.com/jars", + "https://repo.mycompany.com/jars2" + ] + }, + { "type": "maven", "url": "https://secure.repo.com/maven2" }, + { "type": "mavenLocal", "includeGroups": ["com.example.myproject"] }, + { "type": "maven", "url": "build/repo" }, // Relative to build folder of the temporary project, better to use absolute path here, no need to add `file:` prefix + { "type": "mavenCentral" }, + { "type": "mavenLocal" }, + { "type": "google" } + ] +} +``` + +> Note: [Authentication for private repositories is not currently handled directly by `swift-java`](https://github.com/swiftlang/swift-java/issues/382). If you need to access packages from a private repository that requires credentials, you can use Maven to download the required artifacts and then reference them via your local Maven repository in your configuration. + +For practical usage, refer to `Samples/JavaDependencySampleApp` and the tests in `Tests/SwiftJavaTests/JavaRepositoryTests.swift`. + +This workflow streamlines Java dependency management for Swift projects, letting you use Java libraries without manually downloading JAR files. + +#### About the `classes` section + +The `classes` section in your `swift-java.config` file specifies which Java classes should be made available in Swift, and what their corresponding Swift type names should be. Each entry maps a fully-qualified Java class name to a Swift type name. For example: + +```json +{ + "classes": { + "org.apache.commons.io.FilenameUtils" : "FilenameUtils", + "org.apache.commons.io.IOCase" : "IOCase", + "org.apache.commons.csv.CSVFormat" : "CSVFormat", + "org.apache.commons.csv.CSVParser" : "CSVParser", + "org.apache.commons.csv.CSVRecord" : "CSVRecord" + } +} +``` + +When you run `swift-java wrap-java` (or build your project with the plugin), Swift source files are generated for each mapped class. For instance, the above config will result in `CSVFormat.swift`, `CSVParser.swift`, `CSVRecord.swift`, `FilenameUtils.swift` and `IOCase.swift` files, each containing a Swift class that wraps the corresponding Java class and exposes its constructors, methods, and fields for use in Swift. + +This mapping allows you to use Java APIs directly from Swift, with type-safe wrappers and automatic bridging of method calls and data types. ### Expose Swift code to Java: swift-java jextract diff --git a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift index 58c64690..5f050426 100644 --- a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift +++ b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift @@ -15,18 +15,7 @@ import ArgumentParser import Foundation import SwiftJavaToolLib -import SwiftJava -import Foundation -import JavaUtilJar -import SwiftJavaToolLib import SwiftJavaConfigurationShared -import SwiftJavaShared -import _Subprocess -#if canImport(System) -import System -#else -@preconcurrency import SystemPackage -#endif typealias Configuration = SwiftJavaConfigurationShared.Configuration @@ -58,179 +47,13 @@ extension SwiftJava { } extension SwiftJava.ResolveCommand { - var SwiftJavaClasspathPrefix: String { "SWIFT_JAVA_CLASSPATH:" } - var printRuntimeClasspathTaskName: String { "printRuntimeClasspath" } mutating func runSwiftJavaCommand(config: inout Configuration) async throws { - var dependenciesToResolve: [JavaDependencyDescriptor] = [] - if let input, let inputDependencies = parseDependencyDescriptor(input) { - dependenciesToResolve.append(inputDependencies) - } - if let dependencies = config.dependencies { - dependenciesToResolve += dependencies - } - - if dependenciesToResolve.isEmpty { - print("[warn][swift-java] Attempted to 'resolve' dependencies but no dependencies specified in swift-java.config or command input!") - return - } - - let dependenciesClasspath = - try await resolveDependencies(swiftModule: swiftModule, dependencies: dependenciesToResolve) - - // FIXME: disentangle the output directory from SwiftJava and then make it a required option in this Command - guard let outputDirectory = self.commonOptions.outputDirectory else { - fatalError("error: Must specify --output-directory in 'resolve' mode! This option will become explicitly required") - } - - try writeSwiftJavaClasspathFile( + try await JavaResolver.runResolveCommand( + config: &config, + input: input, swiftModule: swiftModule, - outputDirectory: outputDirectory, - resolvedClasspath: dependenciesClasspath) - } - - - /// Resolves Java dependencies from swift-java.config and returns classpath information. - /// - /// - Parameters: - /// - swiftModule: module name from --swift-module. e.g.: --swift-module MySwiftModule - /// - dependencies: parsed maven-style dependency descriptors (groupId:artifactId:version) - /// from Sources/MySwiftModule/swift-java.config "dependencies" array. - /// - /// - Throws: - func resolveDependencies( - swiftModule: String, dependencies: [JavaDependencyDescriptor] - ) async throws -> ResolvedDependencyClasspath { - let deps = dependencies.map { $0.descriptionGradleStyle } - print("[debug][swift-java] Resolve and fetch dependencies for: \(deps)") - - let dependenciesClasspath = await resolveDependencies(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) - - for entry in classpathEntries { - print("[info][swift-java] Classpath entry: \(entry)") - } - - return ResolvedDependencyClasspath(for: dependencies, classpath: dependenciesClasspath) - } - - - /// Resolves maven-style dependencies from swift-java.config under temporary project directory. - /// - /// - Parameter dependencies: maven-style dependencies to resolve - /// - Returns: Colon-separated classpath - func resolveDependencies(dependencies: [JavaDependencyDescriptor]) async -> String { - let workDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) - .appendingPathComponent(".build") - let resolverDir = try! createTemporaryDirectory(in: workDir) - defer { - try? FileManager.default.removeItem(at: resolverDir) - } - - // 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" + - "Output was:<<<\(outString)>>>; 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. - /// - /// - Parameters: - /// - swiftModule: Swift module name for classpath filename (--swift-module value) - /// - outputDirectory: Directory path for classpath file (--output-directory value) - /// - resolvedClasspath: Complete dependency classpath information - /// - mutating func writeSwiftJavaClasspathFile( - swiftModule: String, - outputDirectory: String, - resolvedClasspath: ResolvedDependencyClasspath) throws { - // Convert the artifact name to a module name - // e.g. reactive-streams -> ReactiveStreams - - // The file contents are just plain - let contents = resolvedClasspath.classpath - - let filename = "\(swiftModule).swift-java.classpath" - print("[debug][swift-java] Write resolved dependencies to: \(outputDirectory)/\(filename)") - - // Write the file - try writeContents( - contents, - outputDirectory: URL(fileURLWithPath: outputDirectory), - to: filename, - description: "swift-java.classpath file for module \(swiftModule)" + outputDirectory: commonOptions.outputDirectory ) } @@ -239,74 +62,5 @@ extension SwiftJava.ResolveCommand { let camelCased = components.map { $0.capitalized }.joined() 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 { - /// The dependency identifiers this is the classpath for. - let rootDependencies: [JavaDependencyDescriptor] - - /// Plain string representation of a Java classpath - let classpath: String - - var classpathEntries: [String] { - classpath.split(separator: ":").map(String.init) - } - - init(for rootDependencies: [JavaDependencyDescriptor], classpath: String) { - self.rootDependencies = rootDependencies - self.classpath = classpath - } - - var description: String { - "JavaClasspath(for: \(rootDependencies), classpath: \(classpath))" - } } diff --git a/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift index 25b162e1..7159c54c 100644 --- a/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift +++ b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift @@ -69,27 +69,7 @@ extension SwiftJavaBaseAsyncParsableCommand { outputDirectory: Foundation.URL?, to filename: String, description: String) throws { - guard let outputDir = outputDirectory else { - print("// \(filename) - \(description)") - print(contents) - return - } - - // If we haven't tried to create the output directory yet, do so now before - // we write any files to it. - // if !createdOutputDirectory { - try FileManager.default.createDirectory( - at: outputDir, - withIntermediateDirectories: true - ) - // createdOutputDirectory = true - //} - - // Write the file: - let file = outputDir.appendingPathComponent(filename) - print("[trace][swift-java] Writing \(description) to '\(file.path)'... ", terminator: "") - try contents.write(to: file, atomically: true, encoding: .utf8) - print("done.".green) + try JavaResolver.writeContents(contents, outputDirectory: outputDirectory, to: filename, description: description) } } @@ -167,4 +147,4 @@ extension SwiftJavaBaseAsyncParsableCommand { config.logLevel = command.logLevel return config } -} \ No newline at end of file +} diff --git a/Sources/SwiftJavaToolLib/JavaResolver.swift b/Sources/SwiftJavaToolLib/JavaResolver.swift new file mode 100644 index 00000000..0d1f489b --- /dev/null +++ b/Sources/SwiftJavaToolLib/JavaResolver.swift @@ -0,0 +1,320 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 _Subprocess +import Foundation +import SwiftJavaConfigurationShared +import SwiftJavaShared +#if canImport(System) +import System +#else +@preconcurrency import SystemPackage +#endif + +/// Utility that downloads and resolves Java dependencies for your Swift project. +package enum JavaResolver { + private static var SwiftJavaClasspathPrefix: String { "SWIFT_JAVA_CLASSPATH:" } + private static var printRuntimeClasspathTaskName: String { "printRuntimeClasspath" } + + package static func runResolveCommand(config: inout SwiftJavaConfigurationShared.Configuration, input: String?, swiftModule: String, outputDirectory: String?) async throws { + var dependenciesToResolve: [JavaDependencyDescriptor] = [] + if let input, let inputDependencies = parseDependencyDescriptor(input) { + dependenciesToResolve.append(inputDependencies) + } + if let dependencies = config.dependencies { + dependenciesToResolve += dependencies + } + + if dependenciesToResolve.isEmpty { + print("[warn][swift-java] Attempted to 'resolve' dependencies but no dependencies specified in swift-java.config or command input!") + return + } + + var configuredRepositories: [JavaRepositoryDescriptor] = [] + + if let repositories = config.repositories { + configuredRepositories += repositories + } + + if !configuredRepositories.contains(where: { $0 == .other("mavenCentral") }) { + // swift-java dependencies are originally located in mavenCentral + configuredRepositories.append(.other("mavenCentral")) + } + + let dependenciesClasspath = + try await resolveDependencies(swiftModule: swiftModule, dependencies: dependenciesToResolve, repositories: configuredRepositories) + + // FIXME: disentangle the output directory from SwiftJava and then make it a required option in this Command + guard let outputDirectory else { + fatalError("error: Must specify --output-directory in 'resolve' mode! This option will become explicitly required") + } + + try writeSwiftJavaClasspathFile( + swiftModule: swiftModule, + outputDirectory: outputDirectory, + resolvedClasspath: dependenciesClasspath + ) + } + + /// Resolves Java dependencies from swift-java.config and returns classpath information. + /// + /// - Parameters: + /// - swiftModule: module name from --swift-module. e.g.: --swift-module MySwiftModule + /// - dependencies: parsed maven-style dependency descriptors (groupId:artifactId:version) + /// from Sources/MySwiftModule/swift-java.config "dependencies" array. + /// - repositories: repositories used to resolve dependencies + /// + /// - Throws: + private static func resolveDependencies( + swiftModule: String, dependencies: [JavaDependencyDescriptor], + repositories: [JavaRepositoryDescriptor] + ) async throws -> ResolvedDependencyClasspath { + let deps = dependencies.map { $0.descriptionGradleStyle } + print("[debug][swift-java] Resolve and fetch dependencies for: \(deps)") + + let dependenciesClasspath = await resolveDependencies(dependencies: dependencies, repositories: repositories) + 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) + + for entry in classpathEntries { + print("[info][swift-java] Classpath entry: \(entry)") + } + + return ResolvedDependencyClasspath(for: dependencies, classpath: dependenciesClasspath) + } + + /// Resolves maven-style dependencies from swift-java.config under temporary project directory. + /// + /// - Parameter dependencies: maven-style dependencies to resolve + /// - Parameter repositories: repositories used to resolve dependencies + /// - Returns: Colon-separated classpath + private static func resolveDependencies(dependencies: [JavaDependencyDescriptor], repositories: [JavaRepositoryDescriptor]) async -> String { + let workDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + .appendingPathComponent(".build") + let resolverDir = try! createTemporaryDirectory(in: workDir) + defer { + try? FileManager.default.removeItem(at: resolverDir) + } + + // 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, repositories: repositories) + + 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" + + "Output was:<<<\(outString)>>>; 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. + private static func printGradleProject(directory: URL, dependencies: [JavaDependencyDescriptor], repositories: [JavaRepositoryDescriptor]) throws { + let buildGradle = directory + .appendingPathComponent("build.gradle", isDirectory: false) + + let buildGradleText = + """ + plugins { id 'java-library' } + repositories { + \(repositories.compactMap { $0.renderGradleRepository() }.joined(separator: "\n")) + } + + 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. + /// + /// - Parameters: + /// - swiftModule: Swift module name for classpath filename (--swift-module value) + /// - outputDirectory: Directory path for classpath file (--output-directory value) + /// - resolvedClasspath: Complete dependency classpath information + /// + private static func writeSwiftJavaClasspathFile( + swiftModule: String, + outputDirectory: String, + resolvedClasspath: ResolvedDependencyClasspath + ) throws { + // Convert the artifact name to a module name + // e.g. reactive-streams -> ReactiveStreams + + // The file contents are just plain + let contents = resolvedClasspath.classpath + + let filename = "\(swiftModule).swift-java.classpath" + print("[debug][swift-java] Write resolved dependencies to: \(outputDirectory)/\(filename)") + + // Write the file + try writeContents( + contents, + outputDirectory: URL(fileURLWithPath: outputDirectory), + to: filename, + description: "swift-java.classpath file for module \(swiftModule)" + ) + } + + // copy gradlew & gradle.bat from root, throws error if there is no gradle setup. + 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: 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 + } + } + + private 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 + } +} + +package extension JavaResolver { + static func writeContents( + _ contents: String, + outputDirectory: Foundation.URL?, + to filename: String, + description: String + ) throws { + guard let outputDir = outputDirectory else { + print("// \(filename) - \(description)") + print(contents) + return + } + + // If we haven't tried to create the output directory yet, do so now before + // we write any files to it. + // if !createdOutputDirectory { + try FileManager.default.createDirectory( + at: outputDir, + withIntermediateDirectories: true + ) + // createdOutputDirectory = true + // } + + // Write the file: + let file = outputDir.appendingPathComponent(filename) + print("[trace][swift-java] Writing \(description) to '\(file.path)'... ", terminator: "") + try contents.write(to: file, atomically: true, encoding: .utf8) + print("done.".green) + } +} + +struct ResolvedDependencyClasspath: CustomStringConvertible { + /// The dependency identifiers this is the classpath for. + let rootDependencies: [JavaDependencyDescriptor] + + /// Plain string representation of a Java classpath + let classpath: String + + var classpathEntries: [String] { + classpath.split(separator: ":").map(String.init) + } + + init(for rootDependencies: [JavaDependencyDescriptor], classpath: String) { + self.rootDependencies = rootDependencies + self.classpath = classpath + } + + var description: String { + "JavaClasspath(for: \(rootDependencies), classpath: \(classpath))" + } +} diff --git a/Tests/SwiftJavaToolLibTests/JavaResolverTests.swift b/Tests/SwiftJavaToolLibTests/JavaResolverTests.swift new file mode 100644 index 00000000..0e2824a7 --- /dev/null +++ b/Tests/SwiftJavaToolLibTests/JavaResolverTests.swift @@ -0,0 +1,279 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +@testable import SwiftJavaConfigurationShared +@testable import SwiftJavaToolLib +import Testing + +@Suite(.serialized) +class JavaResolverTests { + static let localRepo: String = String.localRepoRootDirectory.appending("/All") + + static let localJarRepo: String = String.localRepoRootDirectory.appending("/JarOnly") + + static let localPomRepo: String = String.localRepoRootDirectory.appending("/PomOnly") + + deinit { + for item in [Self.localRepo, Self.localJarRepo, Self.localPomRepo] { + try? FileManager.default.removeItem(atPath: item) + } + } + + @Test(arguments: Configuration.resolvableConfigurations) + func resolvableDependency(configuration: SwiftJavaConfigurationShared.Configuration) async throws { + try await resolve(configuration: configuration) + } + + #if compiler(>=6.2) + @Test + func nonResolvableDependency() async throws { + await #expect(processExitsWith: .failure, "commonCSVWithUnknownDependencies") { + try await resolve(configuration: .commonCSVWithUnknownDependencies) + } + await #expect(processExitsWith: .failure, "helloWorldInLocalRepoIncludeIOOnly") { + try await resolve(configuration: .helloWorldInLocalRepoIncludeIOOnly) + } + await #expect(processExitsWith: .failure, "androidCoreInCentral") { + try await resolve(configuration: .androidCoreInCentral) + } + await #expect(processExitsWith: .failure, "helloWorldInRepoWithoutArtifact") { + try await resolve(configuration: .helloWorldInRepoWithoutArtifact) + } + } + #endif + + @Test + func respositoryDecoding() throws { + let data = """ + [ + { "type": "maven", "url": "https://repo.mycompany.com/maven2" }, + { + "type": "maven", + "url": "https://repo2.mycompany.com/maven2", + "artifactUrls": [ + "https://repo.mycompany.com/jars", + "https://repo.mycompany.com/jars2" + ] + }, + { "type": "maven", "url": "https://secure.repo.com/maven2" }, + { "type": "mavenLocal", "includeGroups": ["com.example.myproject"] }, + { "type": "maven", "url": "build/repo" }, + { "type": "mavenCentral" }, + { "type": "mavenLocal" }, + { "type": "google" } + ] + """.data(using: .utf8)! + let repositories = try JSONDecoder().decode([JavaRepositoryDescriptor].self, from: data) + #expect(!repositories.isEmpty, "Expected to decode at least one repository") + #expect(repositories.contains(.maven(url: "https://repo.mycompany.com/maven2")), "Expected to contain the default repository") + #expect(repositories.contains(.maven(url: "build/repo")), "Expected to contain a repository from a build repo") + #expect(repositories.contains(.maven(url: "https://repo2.mycompany.com/maven2", artifactUrls: ["https://repo.mycompany.com/jars", "https://repo.mycompany.com/jars2"])), "Expected to contain a repository with artifact URLs") + #expect(repositories.contains(.mavenLocal(includeGroups: ["com.example.myproject"])), "Expected to contain mavenLocal with includeGroups") + #expect(repositories.contains(.mavenLocal()), "Expected to contain mavenLocal") + #expect(repositories.contains(.other("mavenCentral")), "Expected to contain mavenCentral") + #expect(repositories.contains(.other("google")), "Expected to contain google") + } +} + +// Wired issue with #require, marking the function as static seems to resolve it +private func resolve(configuration: SwiftJavaConfigurationShared.Configuration) async throws { + var config = configuration + try config.publishSampleJavaProjectIfNeeded() + try await JavaResolver.runResolveCommand( + config: &config, + input: nil, + swiftModule: configuration.swiftModule!, + outputDirectory: ".build/\(configuration.swiftModule!)/destination/SwiftJavaPlugin" + ) +} + +extension SwiftJavaConfigurationShared.Configuration: @unchecked Sendable { + static var resolvableConfigurations: [Configuration] = [ + .commonCSV, .jitpackJson, + .helloWorldInTempRepo, + .helloWorldInLocalRepo, + .helloWorldInRepoWithCustomArtifacts, + .androidCoreInGoogle, + ] + + /// Tests with Apache Commons CSV in mavenCentral + static let commonCSV: Configuration = { + var configuration = Configuration() + configuration.swiftModule = "JavaCommonCSV" + configuration.dependencies = [ + JavaDependencyDescriptor(groupID: "org.apache.commons", artifactID: "commons-csv", version: "1.12.0"), + ] + return configuration + }() + + /// Tests with JSON library from Jitpack + static let jitpackJson: Configuration = { + var configuration = Configuration() + configuration.swiftModule = "JavaJson" + configuration.dependencies = [ + JavaDependencyDescriptor(groupID: "org.andrejs", artifactID: "json", version: "1.2"), + ] + configuration.repositories = [.maven(url: "https://jitpack.io")] + return configuration + }() + + /// Tests with local library HelloWorld published to temporary local maven repo + static let helloWorldInTempRepo: Configuration = { + var configuration = Configuration() + configuration.swiftModule = "HelloWorld" + configuration.dependencies = [ + JavaDependencyDescriptor(groupID: "com.example", artifactID: "HelloWorld", version: "1.0.0"), + ] + configuration.packageToPublish = "SimpleJavaProject" + + configuration.repositories = [.maven(url: JavaResolverTests.localRepo)] + return configuration + }() + + /// Tests with local library HelloWorld published to user's local maven repo + static let helloWorldInLocalRepo: Configuration = { + var configuration = Configuration.helloWorldInTempRepo + + configuration.repositories = [.mavenLocal(includeGroups: ["com.example"])] + return configuration + }() + + /// Tests with local library HelloWorld published to temporary local maven repo, with custom artifact URLs + static let helloWorldInRepoWithCustomArtifacts: Configuration = { + var configuration = Configuration.helloWorldInTempRepo + configuration.repositories = [ + .maven(url: JavaResolverTests.localPomRepo, artifactUrls: [ + JavaResolverTests.localJarRepo, + ]), + ] + return configuration + }() + + /// Tests with Android Core library in Google's Maven repository + static let androidCoreInGoogle: Configuration = { + var configuration = Configuration() + configuration.swiftModule = "JavaAndroidCommon" + configuration.dependencies = [ + JavaDependencyDescriptor(groupID: "android.arch.core", artifactID: "common", version: "1.1.1"), + ] + configuration.repositories = [.other("google")] // google() + return configuration + }() + + // MARK: - Non resolvable dependencies + + /// Tests with Apache Commons CSV in mavenCentral, but with an unknown dependency, it should fail + static let commonCSVWithUnknownDependencies: Configuration = { + var configuration = Configuration.commonCSV + configuration.dependencies = [ + JavaDependencyDescriptor(groupID: "org.apache.commons.unknown", artifactID: "commons-csv", version: "1.12.0"), + ] + return configuration + }() + + /// Tests with local library HelloWorld published to user's local maven repo, but trying to include a group that doesn't match, it should fail + static let helloWorldInLocalRepoIncludeIOOnly: Configuration = { + var configuration = Configuration.helloWorldInLocalRepo + configuration.repositories = [.mavenLocal(includeGroups: ["commons-io"])] + return configuration + }() + + /// Tests with Android Core library in mavenCentral, it should fail because it's only in Google's repo + static let androidCoreInCentral: Configuration = { + var configuration = Configuration() + configuration.swiftModule = "JavaAndroidCommon" + configuration.dependencies = [ + JavaDependencyDescriptor(groupID: "android.arch.core", artifactID: "common", version: "1.1.1"), + ] + return configuration + }() + + /// Tests with local library HelloWorld published to temporary local maven repo, but without artifactUrls, it should fail + static let helloWorldInRepoWithoutArtifact: Configuration = { + var configuration = Configuration.helloWorldInTempRepo + + configuration.repositories = [ + .maven(url: JavaResolverTests.localJarRepo /* , artifactUrls: [ + JavaResolverTests.localPomRepo + ] */ ), + ] + return configuration + }() +} + +// MARK: - Publish sample java project to local repo + +private extension SwiftJavaConfigurationShared.Configuration { + var packageToPublish: String? { + get { javaPackage } + set { javaPackage = newValue } + } + + func publishSampleJavaProjectIfNeeded() throws { + guard + let packageName = packageToPublish + else { + return + } + + var gradlewPath = String.packageDirectory + "/gradlew" + if !FileManager.default.fileExists(atPath: gradlewPath) { + let currentWorkingDir = URL(filePath: .packageDirectory).appendingPathComponent(".build", isDirectory: true) + try JavaResolver.copyGradlew(to: currentWorkingDir) + gradlewPath = currentWorkingDir.appendingPathComponent("gradlew").path + } + let process = Process() + process.executableURL = URL(fileURLWithPath: gradlewPath) + let packagePath = URL(fileURLWithPath: #file).deletingLastPathComponent().appendingPathComponent(packageName).path + process.arguments = [ + "-p", "\(packagePath)", + "publishAllArtifacts", + "publishToMavenLocal", // also publish to maven local to test includeGroups" + "-q", + ] + + try process.run() + process.waitUntilExit() + + if process.terminationStatus == 0 { + print("Published \(packageName) to: \(String.localRepoRootDirectory)".green) + } else { + throw NSError( + domain: "PublishError", + code: Int(process.terminationStatus), + userInfo: [NSLocalizedDescriptionKey: "Publish failed with status \(process.terminationStatus)"] + ) + } + } +} + +private extension String { + static var packageDirectory: Self { + let path = getcwd(nil, 0)! + // current directory where `swift test` is run, usually swift-java + defer { free(path) } + + let dir = String(cString: path) + if dir.hasSuffix("swift-java") { // most likely running with `swift test` + return dir + } else { + return FileManager.default.temporaryDirectory.path + } + } + + static var localRepoRootDirectory: Self { + packageDirectory + "/.build/SwiftJavaToolTests/LocalRepo" + } +} diff --git a/Tests/SwiftJavaToolLibTests/SimpleJavaProject/build.gradle b/Tests/SwiftJavaToolLibTests/SimpleJavaProject/build.gradle new file mode 100644 index 00000000..e2679a76 --- /dev/null +++ b/Tests/SwiftJavaToolLibTests/SimpleJavaProject/build.gradle @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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' + id 'maven-publish' +} + +group = 'com.example' +version = '1.0.0' + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + } + } +} + +// Task to publish both JAR and POM to three separate locations +tasks.register('publishAllArtifacts') { + dependsOn jar, generatePomFileForMavenJavaPublication + doLast { + def gradleDir = new File(System.getProperty('user.dir'), ".build/SwiftJavaToolTests") // Usually .build in swift-java + def jarDest = new File(gradleDir, "LocalRepo/JarOnly/${project.group.replace('.', '/')}/${project.name}/${project.version}") + def pomDest = new File(gradleDir, "LocalRepo/PomOnly/${project.group.replace('.', '/')}/${project.name}/${project.version}") + def allDest = new File(gradleDir, "LocalRepo/All/${project.group.replace('.', '/')}/${project.name}/${project.version}") + def jarFile = tasks.jar.archiveFile.get().asFile + def pomFile = file("${buildDir}/publications/mavenJava/pom-default.xml") + def pomName = "${project.name}-${project.version}.pom" + + // Copy JAR to all destinations + [jarDest, allDest].each { dest -> + copy { + from jarFile + into dest + } + } + // Copy POM to all destinations + [pomDest, allDest].each { dest -> + copy { + from pomFile + into dest + rename { String fileName -> pomName } + } + } + } +} diff --git a/Tests/SwiftJavaToolLibTests/SimpleJavaProject/settings.gradle b/Tests/SwiftJavaToolLibTests/SimpleJavaProject/settings.gradle new file mode 100644 index 00000000..288d5312 --- /dev/null +++ b/Tests/SwiftJavaToolLibTests/SimpleJavaProject/settings.gradle @@ -0,0 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +rootProject.name = 'HelloWorld' 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..82ffa33d --- /dev/null +++ b/Tests/SwiftJavaToolLibTests/SimpleJavaProject/src/main/java/com/example/HelloWorld.java @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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; + +public class HelloWorld { + public static String sayHello() { + return "Hello, world!"; + } +}