From 9b624db1dcd7c59514ba7ae35aaba954183fab2a Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Tue, 26 Aug 2025 18:17:10 +0200 Subject: [PATCH 01/20] Add repositories configuration option in swift-java.config --- .../Configuration.swift | 37 +++++++++++++++++++ .../Commands/ResolveCommand.swift | 31 ++++++++++++---- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index 2d9b4311..f73b878f 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -80,6 +80,8 @@ public struct Configuration: Codable { // Java dependencies we need to fetch for this target. public var dependencies: [JavaDependencyDescriptor]? + // Java repositories for this target when fetching dependencies. + public var repositories: [JavaRepositoryDescriptor]? public init() { } @@ -133,6 +135,41 @@ public struct JavaDependencyDescriptor: Hashable, Codable { } } +/// Descriptor for [repositories](https://docs.gradle.org/current/userguide/supported_repository_types.html#sec:maven-repo) +public struct JavaRepositoryDescriptor: Hashable, Codable { + public enum RepositoryType: String, Codable { + case mavenLocal, mavenCentral + case maven // TODO: ivy .. https://docs.gradle.org/current/userguide/supported_repository_types.html#sec:maven-repo + } + + public var type: RepositoryType + public var url: String? + public var artifactUrls: [String]? + + public init(type: RepositoryType, url: String? = nil, artifactUrls: [String]? = nil) { + self.type = type + self.url = url + self.artifactUrls = artifactUrls + } + + public var descriptionGradleStyle: String? { + switch type { + case .mavenLocal, .mavenCentral: + return "\(type.rawValue)()" + case .maven: + guard let url else { + return nil + } + return """ + maven { + url "\(url)" + \((artifactUrls ?? []).map({ "artifactUrls(\($0))" }).joined(separator: "\n")) + } + """ + } + } +} + 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 58c64690..75b83863 100644 --- a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift +++ b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift @@ -75,8 +75,19 @@ extension SwiftJava.ResolveCommand { return } + var repositoriesToResolve: [JavaRepositoryDescriptor] = [] + + if let repositories = config.repositories { + repositoriesToResolve += repositories + } + + if !repositoriesToResolve.contains(where: { $0.type == .mavenCentral }) { + // swift-java dependencies are originally located in mavenCentral + repositoriesToResolve.append(JavaRepositoryDescriptor(type: .mavenCentral)) + } + let dependenciesClasspath = - try await resolveDependencies(swiftModule: swiftModule, dependencies: dependenciesToResolve) + try await resolveDependencies(swiftModule: swiftModule, dependencies: dependenciesToResolve, repositories: repositoriesToResolve) // FIXME: disentangle the output directory from SwiftJava and then make it a required option in this Command guard let outputDirectory = self.commonOptions.outputDirectory else { @@ -99,12 +110,13 @@ extension SwiftJava.ResolveCommand { /// /// - Throws: func resolveDependencies( - swiftModule: String, dependencies: [JavaDependencyDescriptor] + 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) + 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: "") @@ -119,10 +131,11 @@ extension SwiftJava.ResolveCommand { /// Resolves maven-style dependencies from swift-java.config under temporary project directory. - /// + /// /// - Parameter dependencies: maven-style dependencies to resolve + /// - Parameter repositories: maven-style repositories to resolve /// - Returns: Colon-separated classpath - func resolveDependencies(dependencies: [JavaDependencyDescriptor]) async -> String { + func resolveDependencies(dependencies: [JavaDependencyDescriptor], repositories: [JavaRepositoryDescriptor]) async -> String { let workDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) .appendingPathComponent(".build") let resolverDir = try! createTemporaryDirectory(in: workDir) @@ -135,7 +148,7 @@ extension SwiftJava.ResolveCommand { try! copyGradlew(to: resolverDir) - try! printGradleProject(directory: resolverDir, dependencies: dependencies) + try! printGradleProject(directory: resolverDir, dependencies: dependencies, repositories: repositories) if #available(macOS 15, *) { let process = try! await _Subprocess.run( @@ -173,14 +186,16 @@ extension SwiftJava.ResolveCommand { } /// Creates Gradle project files (build.gradle, settings.gradle.kts) in temporary directory. - func printGradleProject(directory: URL, dependencies: [JavaDependencyDescriptor]) throws { + func printGradleProject(directory: URL, dependencies: [JavaDependencyDescriptor], repositories: [JavaRepositoryDescriptor]) throws { let buildGradle = directory .appendingPathComponent("build.gradle", isDirectory: false) let buildGradleText = """ plugins { id 'java-library' } - repositories { mavenCentral() } + repositories { + \(repositories.compactMap(\.descriptionGradleStyle).joined(separator: "\n")) + } dependencies { \(dependencies.map({ dep in "implementation(\"\(dep.descriptionGradleStyle)\")" }).joined(separator: ",\n")) From 7bf0de743df43904e7a7f2ac078e889d0a26b34f Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Tue, 26 Aug 2025 18:33:18 +0200 Subject: [PATCH 02/20] Add a repositories configuration example and fix artifactUrls output --- ...swift-java-with-custom-repositories.config | 29 +++++++++++++++++++ .../Configuration.swift | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/swift-java-with-custom-repositories.config diff --git a/Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/swift-java-with-custom-repositories.config b/Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/swift-java-with-custom-repositories.config new file mode 100644 index 00000000..a9020711 --- /dev/null +++ b/Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/swift-java-with-custom-repositories.config @@ -0,0 +1,29 @@ +{ + "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" + ], + "repositories": [ + { + "type": "maven", + "url": "https://jitpack.io", + "artifactUrls": [] + }, + { + "type": "maven", + "url": "file:~/.m2/repository" + }, + { + "type": "mavenLocal" + }, + { + "type": "mavenCentral" + } + ] +} diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index f73b878f..458c6aea 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -163,7 +163,7 @@ public struct JavaRepositoryDescriptor: Hashable, Codable { return """ maven { url "\(url)" - \((artifactUrls ?? []).map({ "artifactUrls(\($0))" }).joined(separator: "\n")) + \((artifactUrls ?? []).map({ "artifactUrls(\"\($0)\")" }).joined(separator: "\n")) } """ } From 6a126ec95b37b9f7801bc365122220d2e1efff34 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Tue, 26 Aug 2025 19:49:50 +0200 Subject: [PATCH 03/20] Update naming convention --- .../SwiftJavaTool/Commands/ResolveCommand.swift | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift index 75b83863..c46a5f2c 100644 --- a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift +++ b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift @@ -75,19 +75,19 @@ extension SwiftJava.ResolveCommand { return } - var repositoriesToResolve: [JavaRepositoryDescriptor] = [] + var configuredRepositories: [JavaRepositoryDescriptor] = [] if let repositories = config.repositories { - repositoriesToResolve += repositories + configuredRepositories += repositories } - if !repositoriesToResolve.contains(where: { $0.type == .mavenCentral }) { + if !configuredRepositories.contains(where: { $0.type == .mavenCentral }) { // swift-java dependencies are originally located in mavenCentral - repositoriesToResolve.append(JavaRepositoryDescriptor(type: .mavenCentral)) + configuredRepositories.append(JavaRepositoryDescriptor(type: .mavenCentral)) } let dependenciesClasspath = - try await resolveDependencies(swiftModule: swiftModule, dependencies: dependenciesToResolve, repositories: repositoriesToResolve) + 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 = self.commonOptions.outputDirectory else { @@ -102,11 +102,12 @@ extension SwiftJava.ResolveCommand { /// 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: func resolveDependencies( @@ -133,7 +134,7 @@ extension SwiftJava.ResolveCommand { /// Resolves maven-style dependencies from swift-java.config under temporary project directory. /// /// - Parameter dependencies: maven-style dependencies to resolve - /// - Parameter repositories: maven-style repositories to resolve + /// - Parameter repositories: repositories used to resolve dependencies /// - Returns: Colon-separated classpath func resolveDependencies(dependencies: [JavaDependencyDescriptor], repositories: [JavaRepositoryDescriptor]) async -> String { let workDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) From ef69b18ee9718691485e97446f8c5dcf7fcbd1c8 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Thu, 28 Aug 2025 12:23:22 +0200 Subject: [PATCH 04/20] Rename descriptionGradleStyle --- Sources/SwiftJavaConfigurationShared/Configuration.swift | 2 +- Sources/SwiftJavaTool/Commands/ResolveCommand.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index 458c6aea..45888977 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -152,7 +152,7 @@ public struct JavaRepositoryDescriptor: Hashable, Codable { self.artifactUrls = artifactUrls } - public var descriptionGradleStyle: String? { + public func renderGradleRepository() -> String? { switch type { case .mavenLocal, .mavenCentral: return "\(type.rawValue)()" diff --git a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift index c46a5f2c..9fa092be 100644 --- a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift +++ b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift @@ -195,7 +195,7 @@ extension SwiftJava.ResolveCommand { """ plugins { id 'java-library' } repositories { - \(repositories.compactMap(\.descriptionGradleStyle).joined(separator: "\n")) + \(repositories.compactMap({ $0.renderGradleRepository() }).joined(separator: "\n")) } dependencies { From 5a6616be818e0acf4a6e04e166af97b5af76127a Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Thu, 28 Aug 2025 15:22:22 +0200 Subject: [PATCH 05/20] Add JavaJson Example --- Samples/JavaDependencySampleApp/Package.swift | 22 +++++++++++- ...swift-java-with-custom-repositories.config | 29 --------------- .../Sources/JavaDependencySample/main.swift | 35 +++++++++++++++++++ .../Sources/JavaJson/dummy.swift | 13 +++++++ .../Sources/JavaJson/swift-java.config | 15 ++++++++ 5 files changed, 84 insertions(+), 30 deletions(-) delete mode 100644 Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/swift-java-with-custom-repositories.config create mode 100644 Samples/JavaDependencySampleApp/Sources/JavaJson/dummy.swift create mode 100644 Samples/JavaDependencySampleApp/Sources/JavaJson/swift-java.config diff --git a/Samples/JavaDependencySampleApp/Package.swift b/Samples/JavaDependencySampleApp/Package.swift index c5ae97c7..9e48d2f5 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: "CJNI", package: "swift-java"), .product(name: "JavaUtilFunction", package: "swift-java"), - "JavaCommonsCSV" + "JavaCommonsCSV", + "JavaJson", ], exclude: ["swift-java.config"], swiftSettings: [ @@ -99,6 +100,25 @@ let package = Package( ] ), + .target( + name: "JavaJson", + 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/JavaCommonsCSV/swift-java-with-custom-repositories.config b/Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/swift-java-with-custom-repositories.config deleted file mode 100644 index a9020711..00000000 --- a/Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/swift-java-with-custom-repositories.config +++ /dev/null @@ -1,29 +0,0 @@ -{ - "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" - ], - "repositories": [ - { - "type": "maven", - "url": "https://jitpack.io", - "artifactUrls": [] - }, - { - "type": "maven", - "url": "file:~/.m2/repository" - }, - { - "type": "mavenLocal" - }, - { - "type": "mavenCentral" - } - ] -} diff --git a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift index 13ea6eed..1c4a06a0 100644 --- a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift +++ b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift @@ -17,10 +17,16 @@ import JavaUtilFunction import JavaIO import SwiftJavaConfigurationShared import Foundation +#if canImport(System) +import System +#endif // Import the commons-csv library wrapper: import JavaCommonsCSV +// Import the json library wrapper: +import JavaJson + print("") print("") print("-----------------------------------------------------------------------") @@ -52,4 +58,33 @@ for record in try CSVFormatClass.RFC4180.parse(reader)!.getRecords()! { } } +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) + +#if canImport(System) +extension FilePath { + static var currentWorkingDirectory: Self { + let path = getcwd(nil, 0)! + defer { free(path) } + return .init(String(cString: path)) + } +} +print("Reading swift-java.config inside JavaJson folder...") + +let configPath = FilePath.currentWorkingDirectory.appending("Sources/JavaJson/swift-java.config").string + +let config = try JavaClass().of.url("file://" + configPath)! + +precondition(config.hasOwnProperty("repositories")) + +print(config.toString()) + +#endif + print("Done.") diff --git a/Samples/JavaDependencySampleApp/Sources/JavaJson/dummy.swift b/Samples/JavaDependencySampleApp/Sources/JavaJson/dummy.swift new file mode 100644 index 00000000..76f848f9 --- /dev/null +++ b/Samples/JavaDependencySampleApp/Sources/JavaJson/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/JavaJson/swift-java.config b/Samples/JavaDependencySampleApp/Sources/JavaJson/swift-java.config new file mode 100644 index 00000000..1e9fe5da --- /dev/null +++ b/Samples/JavaDependencySampleApp/Sources/JavaJson/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" + } + ] +} From 9416c5688fa7d2f7963e77f9962457cf3708a85f Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Thu, 28 Aug 2025 22:27:14 +0200 Subject: [PATCH 06/20] Change JavaRepositoryDescriptor to enum --- .../Configuration.swift | 83 ++++++++++++++----- .../Commands/ResolveCommand.swift | 4 +- 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index 45888977..5dfd089f 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -80,7 +80,11 @@ public struct Configuration: Codable { // Java dependencies we need to fetch for this target. public var dependencies: [JavaDependencyDescriptor]? - // Java repositories for this target when fetching dependencies. + /// 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() { @@ -136,36 +140,73 @@ public struct JavaDependencyDescriptor: Hashable, Codable { } /// Descriptor for [repositories](https://docs.gradle.org/current/userguide/supported_repository_types.html#sec:maven-repo) -public struct JavaRepositoryDescriptor: Hashable, Codable { - public enum RepositoryType: String, Codable { - case mavenLocal, mavenCentral - case maven // TODO: ivy .. https://docs.gradle.org/current/userguide/supported_repository_types.html#sec:maven-repo - } +public enum JavaRepositoryDescriptor: Hashable, Codable, Equatable { - public var type: RepositoryType - public var url: String? - public var artifactUrls: [String]? + /// 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 + case maven(url: String, artifactUrls: [String]? = nil) + case mavenLocal(includeGroups: [String]? = nil) + case other(_ type: String) - public init(type: RepositoryType, url: String? = nil, artifactUrls: [String]? = nil) { - self.type = type - self.url = url - self.artifactUrls = artifactUrls - } + enum CodingKeys: String, CodingKey { case type, url, artifactUrls, credentials, includeGroups } - public func renderGradleRepository() -> String? { + 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 .mavenLocal, .mavenCentral: - return "\(type.rawValue)()" - case .maven: - guard let url else { - return nil + 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 "\(url)" + 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)()" } } } diff --git a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift index 9fa092be..d49fd9db 100644 --- a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift +++ b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift @@ -81,9 +81,9 @@ extension SwiftJava.ResolveCommand { configuredRepositories += repositories } - if !configuredRepositories.contains(where: { $0.type == .mavenCentral }) { + if !configuredRepositories.contains(where: { $0 == .other("mavenCentral") }) { // swift-java dependencies are originally located in mavenCentral - configuredRepositories.append(JavaRepositoryDescriptor(type: .mavenCentral)) + configuredRepositories.append(.other("mavenCentral")) } let dependenciesClasspath = From 149358a9ee817a16ec320a27dee0c9b50d43fb53 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Thu, 28 Aug 2025 22:29:50 +0200 Subject: [PATCH 07/20] Add JavaRepositoryTests to test dependencies resolving with custom repositories --- Package.swift | 1 + .../SwiftJavaTests/JavaRepositoryTests.swift | 291 ++++++++++++++++++ 2 files changed, 292 insertions(+) create mode 100644 Tests/SwiftJavaTests/JavaRepositoryTests.swift diff --git a/Package.swift b/Package.swift index a30b9510..e880cbf8 100644 --- a/Package.swift +++ b/Package.swift @@ -454,6 +454,7 @@ let package = Package( name: "SwiftJavaTests", dependencies: [ "SwiftJava", + "SwiftJavaTool", "JavaNet" ], swiftSettings: [ diff --git a/Tests/SwiftJavaTests/JavaRepositoryTests.swift b/Tests/SwiftJavaTests/JavaRepositoryTests.swift new file mode 100644 index 00000000..a9c0ab25 --- /dev/null +++ b/Tests/SwiftJavaTests/JavaRepositoryTests.swift @@ -0,0 +1,291 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftJavaTool // test in terminal, if xcode can't find the module +import Testing + +@Suite(.serialized) +class JavaRepositoryTests { + static let localRepo: String = { + let directory = FileManager.default.temporaryDirectory.appendingPathComponent("SwiftJavaTest-Local-Repo", isDirectory: true) + try! FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + return directory.path + }() + + static let localJarRepo: String = { + let directory = FileManager.default.temporaryDirectory.appendingPathComponent("SwiftJavaTest-Local-Repo-Jar-Only", isDirectory: true) + try! FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + return directory.path + }() + + static let localPomRepo: String = { + let directory = FileManager.default.temporaryDirectory.appendingPathComponent("SwiftJavaTest-Local-Repo-Pom-Only", isDirectory: true) + try! FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + return directory.path + }() + + 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) + } + + @Test + func nonResolvableDependency() async throws { + try await #expect(processExitsWith: .failure, "commonCSVWithUnknownDependencies") { + try await resolve(configuration: .commonCSVWithUnknownDependencies) + } + try await #expect(processExitsWith: .failure, "jitpackJsonUsingCentralRepository") { + try await resolve(configuration: .jitpackJsonUsingCentralRepository) + } + try await #expect(processExitsWith: .failure, "jitpackJsonInRepoIncludeIOOnly") { + try await resolve(configuration: .jitpackJsonInRepoIncludeIOOnly) + } + try await #expect(processExitsWith: .failure, "andriodCoreInCentral") { + try await resolve(configuration: .andriodCoreInCentral) + } + } + + @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","credentials":{"username":"user123","password":"secret"}},{"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 + var command = try SwiftJava.ResolveCommand.parse([ + "--output-directory", + ".build/\(configuration.swiftModule!)/destination/SwiftJavaPlugin/", + + "--swift-module", + configuration.swiftModule! + ]) + try await config.downloadIfNeeded() + try await command.runSwiftJavaCommand(config: &config) +} + +extension SwiftJavaConfigurationShared.Configuration { + static var resolvableConfigurations: [Configuration] = [ + .commonCSV, .jitpackJson, + .jitpackJsonInRepo, + andriodCoreInGoogle + ] + + 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 + }() + + 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 + }() + + static let jitpackJsonInRepo: Configuration = { + var configuration = Configuration() + configuration.swiftModule = "JavaJson" + configuration.dependencies = [ + JavaDependencyDescriptor(groupID: "org.andrejs", artifactID: "json", version: "1.2") + ] + // using the following property to download to local repo + configuration.packageToDownload = #""org.andrejs:json:1.2""# + configuration.remoteRepo = "https://jitpack.io" + + let repo = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".m2/repository") + configuration.repositories = [.maven(url: repo.path)] + return configuration + }() + + static let androidLifecycleInRepoWithCustomArtifacts: Configuration = { + var configuration = Configuration() + configuration.swiftModule = "JavaAndroidLifecycle" + configuration.dependencies = [ + JavaDependencyDescriptor(groupID: "android.arch.lifecycle", artifactID: "common", version: "1.1.1") + ] + // using the following property to download to local repo + configuration.packageToDownload = #""android.arch.lifecycle:common:1.1.1""# + configuration.remoteRepo = "https://maven.google.com" + configuration.splitPackage = true + + configuration.repositories = [ + .maven(url: JavaRepositoryTests.localJarRepo, artifactUrls: [ + JavaRepositoryTests.localPomRepo + ]) + ] + return configuration + }() + + static let andriodCoreInGoogle: 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 + + 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 + }() + + static let jitpackJsonInRepoIncludeIOOnly: Configuration = { + var configuration = Configuration() + configuration.swiftModule = "JavaJson" + configuration.dependencies = [ + JavaDependencyDescriptor(groupID: "org.andrejs", artifactID: "json", version: "1.2") + ] + // using the following property to download to local repo + configuration.packageToDownload = #""org.andrejs:json:1.2""# + configuration.remoteRepo = "https://jitpack.io" + // use local repo, since includeGroups only applied to mavenLocal + configuration.preferredLocalRepo = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".m2/repository").path + + configuration.repositories = [.mavenLocal(includeGroups: ["commons-io"])] + return configuration + }() + + static let jitpackJsonUsingCentralRepository: Configuration = { + var configuration = Configuration() + configuration.swiftModule = "JavaJson" + configuration.dependencies = [ + JavaDependencyDescriptor(groupID: "org.andrejs", artifactID: "json", version: "1.2") + ] + return configuration + }() + + static let andriodCoreInCentral: Configuration = { + var configuration = Configuration() + configuration.swiftModule = "JavaAndroidCommon" + configuration.dependencies = [ + JavaDependencyDescriptor(groupID: "android.arch.core", artifactID: "common", version: "1.1.1") + ] + return configuration + }() +} + +// MARK: - Download to local repo + +private extension SwiftJavaConfigurationShared.Configuration { + /// in json format, which means string needs to be quoted + var packageToDownload: String? { + get { javaPackage } + set { javaPackage = newValue } + } + + var remoteRepo: String? { + get { outputJavaDirectory } + set { outputJavaDirectory = newValue } + } + + /// whether to download jar and pom files separately + var splitPackage: Bool? { + get { writeEmptyFiles } + set { writeEmptyFiles = newValue } + } + + var preferredLocalRepo: String? { + get { classpath } + set { classpath = newValue } + } + + func downloadIfNeeded() async throws { + guard + let data = packageToDownload?.data(using: .utf8), + let descriptor = try? JSONDecoder().decode(JavaDependencyDescriptor.self, from: data), + let repo = remoteRepo + else { + return + } + let splitPackage = splitPackage ?? false + + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/env") + process.arguments = [ + "mvn", "dependency:get", + "-DremoteRepositories=\(repo)", + "-DgroupId=\(descriptor.groupID)", + "-DartifactId=\(descriptor.artifactID)", + "-Dversion=\(descriptor.version)", + "-q" + ] + + if splitPackage { + print("Downloading: \(descriptor) from \(repo) to \(JavaRepositoryTests.localJarRepo) and \(JavaRepositoryTests.localPomRepo)".yellow) + process.arguments?.append(contentsOf: [ + "-Dpackaging=jar", + "-Dmaven.repo.local=\(JavaRepositoryTests.localJarRepo)", + "&&", + "mvn", "dependency:get", + "-DremoteRepositories=\(repo)", + "-DgroupId=\(descriptor.groupID)", + "-DartifactId=\(descriptor.artifactID)", + "-Dversion=\(descriptor.version)", + "-Dpackaging=pom", + "-Dmaven.repo.local=\(JavaRepositoryTests.localPomRepo)", + "-q" + ]) + } else { + let repoPath = classpath ?? JavaRepositoryTests.localRepo + print("Downloading: \(descriptor) from \(repo) to \(repoPath)".yellow) + process.arguments?.append("-Dmaven.repo.local=\(repoPath)") + } + + try process.run() + process.waitUntilExit() + + if process.terminationStatus == 0 { + print("Download complete: \(descriptor)".green) + } else { + throw NSError( + domain: "DownloadError", + code: Int(process.terminationStatus), + userInfo: [NSLocalizedDescriptionKey: "Unzip failed with status \(process.terminationStatus)"] + ) + } + } +} From 52243adde4d4832b0b0516041857befe354ae3a7 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Thu, 28 Aug 2025 23:16:25 +0200 Subject: [PATCH 08/20] Add documentation for swift-java resolve --- .../SwiftJavaCommandLineTool.md | 84 ++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md index 32a82eb4..86ee3503 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`. 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 From 764d586c89efa8d14b1855882ca9c963ba678041 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Fri, 29 Aug 2025 00:01:27 +0200 Subject: [PATCH 09/20] Add another non-resolvable config to verify artifactUrls --- .../SwiftJavaTests/JavaRepositoryTests.swift | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Tests/SwiftJavaTests/JavaRepositoryTests.swift b/Tests/SwiftJavaTests/JavaRepositoryTests.swift index a9c0ab25..2b80926d 100644 --- a/Tests/SwiftJavaTests/JavaRepositoryTests.swift +++ b/Tests/SwiftJavaTests/JavaRepositoryTests.swift @@ -62,6 +62,9 @@ class JavaRepositoryTests { try await #expect(processExitsWith: .failure, "andriodCoreInCentral") { try await resolve(configuration: .andriodCoreInCentral) } + try await #expect(processExitsWith: .failure, "androidLifecycleInRepo") { + try await resolve(configuration: .androidLifecycleInRepo) + } } @Test @@ -206,6 +209,25 @@ extension SwiftJavaConfigurationShared.Configuration { ] return configuration }() + + static let androidLifecycleInRepo: Configuration = { + var configuration = Configuration() + configuration.swiftModule = "JavaAndroidLifecycle" + configuration.dependencies = [ + JavaDependencyDescriptor(groupID: "android.arch.lifecycle", artifactID: "common", version: "1.1.1") + ] + // using the following property to download to local repo + configuration.packageToDownload = #""android.arch.lifecycle:common:1.1.1""# + configuration.remoteRepo = "https://maven.google.com" + configuration.splitPackage = true + + configuration.repositories = [ + .maven(url: JavaRepositoryTests.localJarRepo/*, artifactUrls: [ + JavaRepositoryTests.localPomRepo + ]*/) + ] + return configuration + }() } // MARK: - Download to local repo From 7f6cd04c94713e803d0dae7609fb499a741f53ca Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Fri, 29 Aug 2025 08:32:47 +0200 Subject: [PATCH 10/20] Move JavaRepositoryTests to SwiftJavaToolTests --- Package.swift | 14 +++++++++++++- .../JavaRepositoryTests.swift | 0 2 files changed, 13 insertions(+), 1 deletion(-) rename Tests/{SwiftJavaTests => SwiftJavaToolTests}/JavaRepositoryTests.swift (100%) diff --git a/Package.swift b/Package.swift index e880cbf8..4ed545af 100644 --- a/Package.swift +++ b/Package.swift @@ -453,7 +453,19 @@ let package = Package( .testTarget( name: "SwiftJavaTests", dependencies: [ - "SwiftJava", + "SwiftJava", + "JavaNet" + ], + swiftSettings: [ + .swiftLanguageMode(.v5), + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) + ] + ), + + .testTarget( + name: "SwiftJavaToolTests", + dependencies: [ + "SwiftJava", "SwiftJavaTool", "JavaNet" ], diff --git a/Tests/SwiftJavaTests/JavaRepositoryTests.swift b/Tests/SwiftJavaToolTests/JavaRepositoryTests.swift similarity index 100% rename from Tests/SwiftJavaTests/JavaRepositoryTests.swift rename to Tests/SwiftJavaToolTests/JavaRepositoryTests.swift From 8bb6cfa6d0560c0ecb2cf6c4f678d08c3cb7c4cf Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Fri, 29 Aug 2025 09:23:33 +0200 Subject: [PATCH 11/20] Rename JavaJson to OrgAndrejsJson --- Samples/JavaDependencySampleApp/Package.swift | 4 +- .../OrgAndrejsJsonTests.swift | 58 +++++++++++++++++++ .../Sources/JavaDependencySample/main.swift | 35 +---------- .../{JavaJson => OrgAndrejsJson}/dummy.swift | 0 .../swift-java.config | 0 5 files changed, 61 insertions(+), 36 deletions(-) create mode 100644 Samples/JavaDependencySampleApp/Sources/JavaDependencySample/OrgAndrejsJsonTests.swift rename Samples/JavaDependencySampleApp/Sources/{JavaJson => OrgAndrejsJson}/dummy.swift (100%) rename Samples/JavaDependencySampleApp/Sources/{JavaJson => OrgAndrejsJson}/swift-java.config (100%) diff --git a/Samples/JavaDependencySampleApp/Package.swift b/Samples/JavaDependencySampleApp/Package.swift index 9e48d2f5..167b62eb 100644 --- a/Samples/JavaDependencySampleApp/Package.swift +++ b/Samples/JavaDependencySampleApp/Package.swift @@ -68,7 +68,7 @@ let package = Package( .product(name: "CJNI", package: "swift-java"), .product(name: "JavaUtilFunction", package: "swift-java"), "JavaCommonsCSV", - "JavaJson", + "OrgAndrejsJson", ], exclude: ["swift-java.config"], swiftSettings: [ @@ -101,7 +101,7 @@ let package = Package( ), .target( - name: "JavaJson", + name: "OrgAndrejsJson", dependencies: [ .product(name: "SwiftJava", package: "swift-java"), .product(name: "JavaUtilFunction", package: "swift-java"), diff --git a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/OrgAndrejsJsonTests.swift b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/OrgAndrejsJsonTests.swift new file mode 100644 index 00000000..cf900e1e --- /dev/null +++ b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/OrgAndrejsJsonTests.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// 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 +#if canImport(System) +import System +#endif + +// 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) + + #if canImport(System) + print("Reading swift-java.config inside OrgAndrejsJson folder...") + + let configPath = FilePath.currentWorkingDirectory.appending("Sources/OrgAndrejsJson/swift-java.config").string + + let config = try JavaClass().of.url("file://" + configPath)! + + precondition(config.hasOwnProperty("repositories")) + + print(config.toString()) + + #endif + } +} + +#if canImport(System) +extension FilePath { + static var currentWorkingDirectory: Self { + let path = getcwd(nil, 0)! + defer { free(path) } + return .init(String(cString: path)) + } +} +#endif diff --git a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift index 1c4a06a0..28b7426b 100644 --- a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift +++ b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift @@ -17,16 +17,10 @@ import JavaUtilFunction import JavaIO import SwiftJavaConfigurationShared import Foundation -#if canImport(System) -import System -#endif // Import the commons-csv library wrapper: import JavaCommonsCSV -// Import the json library wrapper: -import JavaJson - print("") print("") print("-----------------------------------------------------------------------") @@ -58,33 +52,6 @@ for record in try CSVFormatClass.RFC4180.parse(reader)!.getRecords()! { } } -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) - -#if canImport(System) -extension FilePath { - static var currentWorkingDirectory: Self { - let path = getcwd(nil, 0)! - defer { free(path) } - return .init(String(cString: path)) - } -} -print("Reading swift-java.config inside JavaJson folder...") - -let configPath = FilePath.currentWorkingDirectory.appending("Sources/JavaJson/swift-java.config").string - -let config = try JavaClass().of.url("file://" + configPath)! - -precondition(config.hasOwnProperty("repositories")) - -print(config.toString()) - -#endif +try await OrgAndrejsJsonTests.run() print("Done.") diff --git a/Samples/JavaDependencySampleApp/Sources/JavaJson/dummy.swift b/Samples/JavaDependencySampleApp/Sources/OrgAndrejsJson/dummy.swift similarity index 100% rename from Samples/JavaDependencySampleApp/Sources/JavaJson/dummy.swift rename to Samples/JavaDependencySampleApp/Sources/OrgAndrejsJson/dummy.swift diff --git a/Samples/JavaDependencySampleApp/Sources/JavaJson/swift-java.config b/Samples/JavaDependencySampleApp/Sources/OrgAndrejsJson/swift-java.config similarity index 100% rename from Samples/JavaDependencySampleApp/Sources/JavaJson/swift-java.config rename to Samples/JavaDependencySampleApp/Sources/OrgAndrejsJson/swift-java.config From c57c01775c74196e5080c52dbb3037f4adc456f9 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Fri, 29 Aug 2025 09:28:07 +0200 Subject: [PATCH 12/20] Add referenced issue in the document --- .../Documentation.docc/SwiftJavaCommandLineTool.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md index 86ee3503..cd493be7 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md @@ -257,7 +257,7 @@ If you do not specify any `repositories`, dependencies are resolved from Maven C } ``` -> Note: Authentication for private repositories is not currently handled directly by `swift-java`. 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. +> 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`. From ab8f3ab5a67c200ac50499ad77e32f2e6fc47ea8 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Fri, 29 Aug 2025 09:35:50 +0200 Subject: [PATCH 13/20] [Test] Change minified json to pretty printed --- .../JavaRepositoryTests.swift | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Tests/SwiftJavaToolTests/JavaRepositoryTests.swift b/Tests/SwiftJavaToolTests/JavaRepositoryTests.swift index 2b80926d..bf92d992 100644 --- a/Tests/SwiftJavaToolTests/JavaRepositoryTests.swift +++ b/Tests/SwiftJavaToolTests/JavaRepositoryTests.swift @@ -69,7 +69,25 @@ class JavaRepositoryTests { @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","credentials":{"username":"user123","password":"secret"}},{"type":"mavenLocal","includeGroups":["com.example.myproject"]},{"type":"maven","url":"build/repo"},{"type":"mavenCentral"},{"type":"mavenLocal"},{"type":"google"}]"#.data(using: .utf8)! + 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") From f6f9800fe767ae4c73f103a79b4c1ed1011d174b Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:02:47 +0200 Subject: [PATCH 14/20] Remove System dependency from OrgAndrejsJsonTests --- .../JavaDependencySample/OrgAndrejsJsonTests.swift | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/OrgAndrejsJsonTests.swift b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/OrgAndrejsJsonTests.swift index cf900e1e..e5c15c07 100644 --- a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/OrgAndrejsJsonTests.swift +++ b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/OrgAndrejsJsonTests.swift @@ -14,9 +14,6 @@ import Foundation import SwiftJava -#if canImport(System) -import System -#endif // Import the json library wrapper: import OrgAndrejsJson @@ -32,27 +29,22 @@ enum OrgAndrejsJsonTests { print(json.get("port").toString()) precondition(json.get("port").as(JavaInteger.self)!.intValue() == 80) - #if canImport(System) print("Reading swift-java.config inside OrgAndrejsJson folder...") - let configPath = FilePath.currentWorkingDirectory.appending("Sources/OrgAndrejsJson/swift-java.config").string + 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()) - - #endif } } -#if canImport(System) -extension FilePath { +private extension String { static var currentWorkingDirectory: Self { let path = getcwd(nil, 0)! defer { free(path) } - return .init(String(cString: path)) + return String(cString: path) } } -#endif From de3dab39bab03d06d2628ec4b0ee17b2ebb190e9 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Fri, 29 Aug 2025 17:58:19 +0200 Subject: [PATCH 15/20] Add more referenced documents for JavaRepositoryDescriptor --- Sources/SwiftJavaConfigurationShared/Configuration.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index 5dfd089f..db72b824 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -143,6 +143,10 @@ public struct JavaDependencyDescriptor: Hashable, Codable { 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) From f6ad15f286728688022e639147d9d0c1443a4bf3 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Fri, 29 Aug 2025 17:59:18 +0200 Subject: [PATCH 16/20] [Test] Add a SimpleJavaProject to JavaRepositoryTests --- Package.swift | 5 +- .../JavaRepositoryTests.swift | 271 +++++++----------- .../SimpleJavaProject/build.gradle | 50 ++++ .../SimpleJavaProject/settings.gradle | 1 + .../src/main/java/com/example/HelloWorld.java | 7 + 5 files changed, 169 insertions(+), 165 deletions(-) create mode 100644 Tests/SwiftJavaToolTests/SimpleJavaProject/build.gradle create mode 100644 Tests/SwiftJavaToolTests/SimpleJavaProject/settings.gradle create mode 100644 Tests/SwiftJavaToolTests/SimpleJavaProject/src/main/java/com/example/HelloWorld.java diff --git a/Package.swift b/Package.swift index 4ed545af..7b3f750c 100644 --- a/Package.swift +++ b/Package.swift @@ -465,9 +465,10 @@ let package = Package( .testTarget( name: "SwiftJavaToolTests", dependencies: [ - "SwiftJava", "SwiftJavaTool", - "JavaNet" + ], + exclude: [ + "SimpleJavaProject", ], swiftSettings: [ .swiftLanguageMode(.v5), diff --git a/Tests/SwiftJavaToolTests/JavaRepositoryTests.swift b/Tests/SwiftJavaToolTests/JavaRepositoryTests.swift index bf92d992..4207c4b9 100644 --- a/Tests/SwiftJavaToolTests/JavaRepositoryTests.swift +++ b/Tests/SwiftJavaToolTests/JavaRepositoryTests.swift @@ -14,28 +14,16 @@ import Foundation @testable import SwiftJavaConfigurationShared -@testable import SwiftJavaTool // test in terminal, if xcode can't find the module +@testable import SwiftJavaTool // test in terminal with sandbox disabled, if xcode can't find the module import Testing @Suite(.serialized) class JavaRepositoryTests { - static let localRepo: String = { - let directory = FileManager.default.temporaryDirectory.appendingPathComponent("SwiftJavaTest-Local-Repo", isDirectory: true) - try! FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - return directory.path - }() + static let localRepo: String = String.localRepoRootDirectory.appending("/All") - static let localJarRepo: String = { - let directory = FileManager.default.temporaryDirectory.appendingPathComponent("SwiftJavaTest-Local-Repo-Jar-Only", isDirectory: true) - try! FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - return directory.path - }() + static let localJarRepo: String = String.localRepoRootDirectory.appending("/JarOnly") - static let localPomRepo: String = { - let directory = FileManager.default.temporaryDirectory.appendingPathComponent("SwiftJavaTest-Local-Repo-Pom-Only", isDirectory: true) - try! FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - return directory.path - }() + static let localPomRepo: String = String.localRepoRootDirectory.appending("/PomOnly") deinit { for item in [Self.localRepo, Self.localJarRepo, Self.localPomRepo] { @@ -48,46 +36,45 @@ class JavaRepositoryTests { try await resolve(configuration: configuration) } + #if compiler(>=6.2) @Test func nonResolvableDependency() async throws { try await #expect(processExitsWith: .failure, "commonCSVWithUnknownDependencies") { try await resolve(configuration: .commonCSVWithUnknownDependencies) } - try await #expect(processExitsWith: .failure, "jitpackJsonUsingCentralRepository") { - try await resolve(configuration: .jitpackJsonUsingCentralRepository) + try await #expect(processExitsWith: .failure, "helloWorldInLocalRepoIncludeIOOnly") { + try await resolve(configuration: .helloWorldInLocalRepoIncludeIOOnly) } - try await #expect(processExitsWith: .failure, "jitpackJsonInRepoIncludeIOOnly") { - try await resolve(configuration: .jitpackJsonInRepoIncludeIOOnly) + try await #expect(processExitsWith: .failure, "androidCoreInCentral") { + try await resolve(configuration: .androidCoreInCentral) } - try await #expect(processExitsWith: .failure, "andriodCoreInCentral") { - try await resolve(configuration: .andriodCoreInCentral) - } - try await #expect(processExitsWith: .failure, "androidLifecycleInRepo") { - try await resolve(configuration: .androidLifecycleInRepo) + try 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)! + [ + { "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") @@ -105,80 +92,83 @@ private func resolve(configuration: SwiftJavaConfigurationShared.Configuration) var config = configuration var command = try SwiftJava.ResolveCommand.parse([ "--output-directory", - ".build/\(configuration.swiftModule!)/destination/SwiftJavaPlugin/", + ".build/\(configuration.swiftModule!)/destination/SwiftJavaPlugin", "--swift-module", - configuration.swiftModule! + configuration.swiftModule!, ]) - try await config.downloadIfNeeded() + try config.publishSampleJavaProjectIfNeeded() try await command.runSwiftJavaCommand(config: &config) } extension SwiftJavaConfigurationShared.Configuration { static var resolvableConfigurations: [Configuration] = [ .commonCSV, .jitpackJson, - .jitpackJsonInRepo, - andriodCoreInGoogle + .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") + 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") + JavaDependencyDescriptor(groupID: "org.andrejs", artifactID: "json", version: "1.2"), ] configuration.repositories = [.maven(url: "https://jitpack.io")] return configuration }() - static let jitpackJsonInRepo: Configuration = { + /// Tests with local library HelloWorld published to temporary local maven repo + static let helloWorldInTempRepo: Configuration = { var configuration = Configuration() - configuration.swiftModule = "JavaJson" + configuration.swiftModule = "HelloWorld" configuration.dependencies = [ - JavaDependencyDescriptor(groupID: "org.andrejs", artifactID: "json", version: "1.2") + JavaDependencyDescriptor(groupID: "com.example", artifactID: "HelloWorld", version: "1.0.0"), ] - // using the following property to download to local repo - configuration.packageToDownload = #""org.andrejs:json:1.2""# - configuration.remoteRepo = "https://jitpack.io" + configuration.packageToPublish = "SimpleJavaProject" - let repo = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".m2/repository") - configuration.repositories = [.maven(url: repo.path)] + configuration.repositories = [.maven(url: JavaRepositoryTests.localRepo)] return configuration }() + + /// Tests with local library HelloWorld published to user's local maven repo + static let helloWorldInLocalRepo: Configuration = { + var configuration = Configuration.helloWorldInTempRepo - static let androidLifecycleInRepoWithCustomArtifacts: Configuration = { - var configuration = Configuration() - configuration.swiftModule = "JavaAndroidLifecycle" - configuration.dependencies = [ - JavaDependencyDescriptor(groupID: "android.arch.lifecycle", artifactID: "common", version: "1.1.1") - ] - // using the following property to download to local repo - configuration.packageToDownload = #""android.arch.lifecycle:common:1.1.1""# - configuration.remoteRepo = "https://maven.google.com" - configuration.splitPackage = true + 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: JavaRepositoryTests.localJarRepo, artifactUrls: [ - JavaRepositoryTests.localPomRepo - ]) + .maven(url: JavaRepositoryTests.localPomRepo, artifactUrls: [ + JavaRepositoryTests.localJarRepo, + ]), ] return configuration }() - static let andriodCoreInGoogle: 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") + JavaDependencyDescriptor(groupID: "android.arch.core", artifactID: "common", version: "1.1.1"), ] configuration.repositories = [.other("google")] // google() return configuration @@ -186,140 +176,74 @@ extension SwiftJavaConfigurationShared.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") + JavaDependencyDescriptor(groupID: "org.apache.commons.unknown", artifactID: "commons-csv", version: "1.12.0"), ] return configuration }() - static let jitpackJsonInRepoIncludeIOOnly: Configuration = { - var configuration = Configuration() - configuration.swiftModule = "JavaJson" - configuration.dependencies = [ - JavaDependencyDescriptor(groupID: "org.andrejs", artifactID: "json", version: "1.2") - ] - // using the following property to download to local repo - configuration.packageToDownload = #""org.andrejs:json:1.2""# - configuration.remoteRepo = "https://jitpack.io" - // use local repo, since includeGroups only applied to mavenLocal - configuration.preferredLocalRepo = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".m2/repository").path - + /// 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 }() - static let jitpackJsonUsingCentralRepository: Configuration = { - var configuration = Configuration() - configuration.swiftModule = "JavaJson" - configuration.dependencies = [ - JavaDependencyDescriptor(groupID: "org.andrejs", artifactID: "json", version: "1.2") - ] - return configuration - }() - - static let andriodCoreInCentral: 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") + JavaDependencyDescriptor(groupID: "android.arch.core", artifactID: "common", version: "1.1.1"), ] return configuration }() - static let androidLifecycleInRepo: Configuration = { - var configuration = Configuration() - configuration.swiftModule = "JavaAndroidLifecycle" - configuration.dependencies = [ - JavaDependencyDescriptor(groupID: "android.arch.lifecycle", artifactID: "common", version: "1.1.1") - ] - // using the following property to download to local repo - configuration.packageToDownload = #""android.arch.lifecycle:common:1.1.1""# - configuration.remoteRepo = "https://maven.google.com" - configuration.splitPackage = true + /// 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: JavaRepositoryTests.localJarRepo/*, artifactUrls: [ - JavaRepositoryTests.localPomRepo - ]*/) + .maven(url: JavaRepositoryTests.localJarRepo /* , artifactUrls: [ + JavaRepositoryTests.localPomRepo + ] */ ), ] return configuration }() } -// MARK: - Download to local repo +// MARK: - Publish sample java project to local repo private extension SwiftJavaConfigurationShared.Configuration { - /// in json format, which means string needs to be quoted - var packageToDownload: String? { + var packageToPublish: String? { get { javaPackage } set { javaPackage = newValue } } - var remoteRepo: String? { - get { outputJavaDirectory } - set { outputJavaDirectory = newValue } - } - - /// whether to download jar and pom files separately - var splitPackage: Bool? { - get { writeEmptyFiles } - set { writeEmptyFiles = newValue } - } - - var preferredLocalRepo: String? { - get { classpath } - set { classpath = newValue } - } - - func downloadIfNeeded() async throws { + func publishSampleJavaProjectIfNeeded() throws { guard - let data = packageToDownload?.data(using: .utf8), - let descriptor = try? JSONDecoder().decode(JavaDependencyDescriptor.self, from: data), - let repo = remoteRepo + let packageName = packageToPublish else { return } - let splitPackage = splitPackage ?? false let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/env") + process.executableURL = URL(fileURLWithPath: .gradlewPath) process.arguments = [ - "mvn", "dependency:get", - "-DremoteRepositories=\(repo)", - "-DgroupId=\(descriptor.groupID)", - "-DartifactId=\(descriptor.artifactID)", - "-Dversion=\(descriptor.version)", - "-q" + "-p", "\(String.packageDirectory)/Tests/SwiftJavaToolTests/\(packageName)", + "publishAllArtifacts", + "publishToMavenLocal", // also publish to maven local to test includeGroups" + "-q", ] - if splitPackage { - print("Downloading: \(descriptor) from \(repo) to \(JavaRepositoryTests.localJarRepo) and \(JavaRepositoryTests.localPomRepo)".yellow) - process.arguments?.append(contentsOf: [ - "-Dpackaging=jar", - "-Dmaven.repo.local=\(JavaRepositoryTests.localJarRepo)", - "&&", - "mvn", "dependency:get", - "-DremoteRepositories=\(repo)", - "-DgroupId=\(descriptor.groupID)", - "-DartifactId=\(descriptor.artifactID)", - "-Dversion=\(descriptor.version)", - "-Dpackaging=pom", - "-Dmaven.repo.local=\(JavaRepositoryTests.localPomRepo)", - "-q" - ]) - } else { - let repoPath = classpath ?? JavaRepositoryTests.localRepo - print("Downloading: \(descriptor) from \(repo) to \(repoPath)".yellow) - process.arguments?.append("-Dmaven.repo.local=\(repoPath)") - } - try process.run() process.waitUntilExit() if process.terminationStatus == 0 { - print("Download complete: \(descriptor)".green) + print("Published \(packageName) to: \(String.localRepoRootDirectory)".green) } else { throw NSError( domain: "DownloadError", @@ -329,3 +253,24 @@ private extension SwiftJavaConfigurationShared.Configuration { } } } + +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) + // TODO: This needs to be tested in Xcode as well, for now Xcode can't run tests, due to this issue: https://github.com/swiftlang/swift-java/issues/281 + precondition(dir.hasSuffix("swift-java"), "Please run the tests from the swift-java directory") + return dir + } + + static var localRepoRootDirectory: Self { + packageDirectory + "/.build/SwiftJavaToolTests/LocalRepo" + } + + static var gradlewPath: Self { + packageDirectory + "/gradlew" + } +} diff --git a/Tests/SwiftJavaToolTests/SimpleJavaProject/build.gradle b/Tests/SwiftJavaToolTests/SimpleJavaProject/build.gradle new file mode 100644 index 00000000..a83a5041 --- /dev/null +++ b/Tests/SwiftJavaToolTests/SimpleJavaProject/build.gradle @@ -0,0 +1,50 @@ +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/SwiftJavaToolTests/SimpleJavaProject/settings.gradle b/Tests/SwiftJavaToolTests/SimpleJavaProject/settings.gradle new file mode 100644 index 00000000..df0ec21a --- /dev/null +++ b/Tests/SwiftJavaToolTests/SimpleJavaProject/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'HelloWorld' diff --git a/Tests/SwiftJavaToolTests/SimpleJavaProject/src/main/java/com/example/HelloWorld.java b/Tests/SwiftJavaToolTests/SimpleJavaProject/src/main/java/com/example/HelloWorld.java new file mode 100644 index 00000000..5a2c1c00 --- /dev/null +++ b/Tests/SwiftJavaToolTests/SimpleJavaProject/src/main/java/com/example/HelloWorld.java @@ -0,0 +1,7 @@ +package com.example; + +public class HelloWorld { + public static String sayHello() { + return "Hello, world!"; + } +} From a61c51869dacb2169e05c6045262fe1efbc9b644 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Fri, 29 Aug 2025 18:38:33 +0200 Subject: [PATCH 17/20] [Test] Update error messages in JavaRepositoryTests.swift --- Tests/SwiftJavaToolTests/JavaRepositoryTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/SwiftJavaToolTests/JavaRepositoryTests.swift b/Tests/SwiftJavaToolTests/JavaRepositoryTests.swift index 4207c4b9..b77f302e 100644 --- a/Tests/SwiftJavaToolTests/JavaRepositoryTests.swift +++ b/Tests/SwiftJavaToolTests/JavaRepositoryTests.swift @@ -246,9 +246,9 @@ private extension SwiftJavaConfigurationShared.Configuration { print("Published \(packageName) to: \(String.localRepoRootDirectory)".green) } else { throw NSError( - domain: "DownloadError", + domain: "PublishError", code: Int(process.terminationStatus), - userInfo: [NSLocalizedDescriptionKey: "Unzip failed with status \(process.terminationStatus)"] + userInfo: [NSLocalizedDescriptionKey: "Publish failed with status \(process.terminationStatus)"] ) } } From 967ccafcf1b6f88b14fd6e1288bc76693cf66369 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Sun, 31 Aug 2025 10:42:57 +0200 Subject: [PATCH 18/20] [Test] Add missing license headers --- .../SimpleJavaProject/build.gradle | 14 ++++++++++++++ .../SimpleJavaProject/settings.gradle | 14 ++++++++++++++ .../src/main/java/com/example/HelloWorld.java | 14 ++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/Tests/SwiftJavaToolTests/SimpleJavaProject/build.gradle b/Tests/SwiftJavaToolTests/SimpleJavaProject/build.gradle index a83a5041..e2679a76 100644 --- a/Tests/SwiftJavaToolTests/SimpleJavaProject/build.gradle +++ b/Tests/SwiftJavaToolTests/SimpleJavaProject/build.gradle @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// 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' diff --git a/Tests/SwiftJavaToolTests/SimpleJavaProject/settings.gradle b/Tests/SwiftJavaToolTests/SimpleJavaProject/settings.gradle index df0ec21a..288d5312 100644 --- a/Tests/SwiftJavaToolTests/SimpleJavaProject/settings.gradle +++ b/Tests/SwiftJavaToolTests/SimpleJavaProject/settings.gradle @@ -1 +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/SwiftJavaToolTests/SimpleJavaProject/src/main/java/com/example/HelloWorld.java b/Tests/SwiftJavaToolTests/SimpleJavaProject/src/main/java/com/example/HelloWorld.java index 5a2c1c00..82ffa33d 100644 --- a/Tests/SwiftJavaToolTests/SimpleJavaProject/src/main/java/com/example/HelloWorld.java +++ b/Tests/SwiftJavaToolTests/SimpleJavaProject/src/main/java/com/example/HelloWorld.java @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// 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 { From 8fe1360b2fd3bbc5bf1397b6bc0499c1a224293f Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Mon, 1 Sep 2025 10:35:45 +0200 Subject: [PATCH 19/20] Add JavaResolver in SwiftJavaToolLib to resolve for ResolveCommand --- .../Commands/ResolveCommand.swift | 270 +-------------- .../SwiftJavaBaseAsyncParsableCommand.swift | 24 +- Sources/SwiftJavaToolLib/JavaResolver.swift | 320 ++++++++++++++++++ 3 files changed, 326 insertions(+), 288 deletions(-) create mode 100644 Sources/SwiftJavaToolLib/JavaResolver.swift diff --git a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift index d49fd9db..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,195 +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 - } - - 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 = 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. - /// - repositories: repositories used to resolve dependencies - /// - /// - Throws: - 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 - 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. - 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 - /// - 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 ) } @@ -255,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))" + } +} From be8729097b4573e4a58f937590ff27219e7458da Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Mon, 1 Sep 2025 10:36:49 +0200 Subject: [PATCH 20/20] [Test] Move SwiftJavaToolTests/JavaRepositoryTests --- Package.swift | 17 +----- .../JavaResolverTests.swift} | 61 ++++++++++--------- .../SimpleJavaProject/build.gradle | 0 .../SimpleJavaProject/settings.gradle | 0 .../src/main/java/com/example/HelloWorld.java | 0 5 files changed, 35 insertions(+), 43 deletions(-) rename Tests/{SwiftJavaToolTests/JavaRepositoryTests.swift => SwiftJavaToolLibTests/JavaResolverTests.swift} (83%) rename Tests/{SwiftJavaToolTests => SwiftJavaToolLibTests}/SimpleJavaProject/build.gradle (100%) rename Tests/{SwiftJavaToolTests => SwiftJavaToolLibTests}/SimpleJavaProject/settings.gradle (100%) rename Tests/{SwiftJavaToolTests => SwiftJavaToolLibTests}/SimpleJavaProject/src/main/java/com/example/HelloWorld.java (100%) diff --git a/Package.swift b/Package.swift index 5f439d96..646b1f80 100644 --- a/Package.swift +++ b/Package.swift @@ -462,20 +462,6 @@ let package = Package( ] ), - .testTarget( - name: "SwiftJavaToolTests", - dependencies: [ - "SwiftJavaTool", - ], - exclude: [ - "SimpleJavaProject", - ], - swiftSettings: [ - .swiftLanguageMode(.v5), - .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) - ] - ), - .testTarget( name: "JavaTypesTests", dependencies: [ @@ -502,6 +488,9 @@ let package = Package( dependencies: [ "SwiftJavaToolLib" ], + exclude: [ + "SimpleJavaProject", + ], swiftSettings: [ .swiftLanguageMode(.v5), .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) diff --git a/Tests/SwiftJavaToolTests/JavaRepositoryTests.swift b/Tests/SwiftJavaToolLibTests/JavaResolverTests.swift similarity index 83% rename from Tests/SwiftJavaToolTests/JavaRepositoryTests.swift rename to Tests/SwiftJavaToolLibTests/JavaResolverTests.swift index b77f302e..0e2824a7 100644 --- a/Tests/SwiftJavaToolTests/JavaRepositoryTests.swift +++ b/Tests/SwiftJavaToolLibTests/JavaResolverTests.swift @@ -14,11 +14,11 @@ import Foundation @testable import SwiftJavaConfigurationShared -@testable import SwiftJavaTool // test in terminal with sandbox disabled, if xcode can't find the module +@testable import SwiftJavaToolLib import Testing @Suite(.serialized) -class JavaRepositoryTests { +class JavaResolverTests { static let localRepo: String = String.localRepoRootDirectory.appending("/All") static let localJarRepo: String = String.localRepoRootDirectory.appending("/JarOnly") @@ -39,16 +39,16 @@ class JavaRepositoryTests { #if compiler(>=6.2) @Test func nonResolvableDependency() async throws { - try await #expect(processExitsWith: .failure, "commonCSVWithUnknownDependencies") { + await #expect(processExitsWith: .failure, "commonCSVWithUnknownDependencies") { try await resolve(configuration: .commonCSVWithUnknownDependencies) } - try await #expect(processExitsWith: .failure, "helloWorldInLocalRepoIncludeIOOnly") { + await #expect(processExitsWith: .failure, "helloWorldInLocalRepoIncludeIOOnly") { try await resolve(configuration: .helloWorldInLocalRepoIncludeIOOnly) } - try await #expect(processExitsWith: .failure, "androidCoreInCentral") { + await #expect(processExitsWith: .failure, "androidCoreInCentral") { try await resolve(configuration: .androidCoreInCentral) } - try await #expect(processExitsWith: .failure, "helloWorldInRepoWithoutArtifact") { + await #expect(processExitsWith: .failure, "helloWorldInRepoWithoutArtifact") { try await resolve(configuration: .helloWorldInRepoWithoutArtifact) } } @@ -90,18 +90,16 @@ class JavaRepositoryTests { // Wired issue with #require, marking the function as static seems to resolve it private func resolve(configuration: SwiftJavaConfigurationShared.Configuration) async throws { var config = configuration - var command = try SwiftJava.ResolveCommand.parse([ - "--output-directory", - ".build/\(configuration.swiftModule!)/destination/SwiftJavaPlugin", - - "--swift-module", - configuration.swiftModule!, - ]) try config.publishSampleJavaProjectIfNeeded() - try await command.runSwiftJavaCommand(config: &config) + try await JavaResolver.runResolveCommand( + config: &config, + input: nil, + swiftModule: configuration.swiftModule!, + outputDirectory: ".build/\(configuration.swiftModule!)/destination/SwiftJavaPlugin" + ) } -extension SwiftJavaConfigurationShared.Configuration { +extension SwiftJavaConfigurationShared.Configuration: @unchecked Sendable { static var resolvableConfigurations: [Configuration] = [ .commonCSV, .jitpackJson, .helloWorldInTempRepo, @@ -140,7 +138,7 @@ extension SwiftJavaConfigurationShared.Configuration { ] configuration.packageToPublish = "SimpleJavaProject" - configuration.repositories = [.maven(url: JavaRepositoryTests.localRepo)] + configuration.repositories = [.maven(url: JavaResolverTests.localRepo)] return configuration }() @@ -156,8 +154,8 @@ extension SwiftJavaConfigurationShared.Configuration { static let helloWorldInRepoWithCustomArtifacts: Configuration = { var configuration = Configuration.helloWorldInTempRepo configuration.repositories = [ - .maven(url: JavaRepositoryTests.localPomRepo, artifactUrls: [ - JavaRepositoryTests.localJarRepo, + .maven(url: JavaResolverTests.localPomRepo, artifactUrls: [ + JavaResolverTests.localJarRepo, ]), ] return configuration @@ -207,8 +205,8 @@ extension SwiftJavaConfigurationShared.Configuration { var configuration = Configuration.helloWorldInTempRepo configuration.repositories = [ - .maven(url: JavaRepositoryTests.localJarRepo /* , artifactUrls: [ - JavaRepositoryTests.localPomRepo + .maven(url: JavaResolverTests.localJarRepo /* , artifactUrls: [ + JavaResolverTests.localPomRepo ] */ ), ] return configuration @@ -230,10 +228,17 @@ private extension SwiftJavaConfigurationShared.Configuration { 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) + process.executableURL = URL(fileURLWithPath: gradlewPath) + let packagePath = URL(fileURLWithPath: #file).deletingLastPathComponent().appendingPathComponent(packageName).path process.arguments = [ - "-p", "\(String.packageDirectory)/Tests/SwiftJavaToolTests/\(packageName)", + "-p", "\(packagePath)", "publishAllArtifacts", "publishToMavenLocal", // also publish to maven local to test includeGroups" "-q", @@ -261,16 +266,14 @@ private extension String { defer { free(path) } let dir = String(cString: path) - // TODO: This needs to be tested in Xcode as well, for now Xcode can't run tests, due to this issue: https://github.com/swiftlang/swift-java/issues/281 - precondition(dir.hasSuffix("swift-java"), "Please run the tests from the swift-java directory") - return dir + 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" } - - static var gradlewPath: Self { - packageDirectory + "/gradlew" - } } diff --git a/Tests/SwiftJavaToolTests/SimpleJavaProject/build.gradle b/Tests/SwiftJavaToolLibTests/SimpleJavaProject/build.gradle similarity index 100% rename from Tests/SwiftJavaToolTests/SimpleJavaProject/build.gradle rename to Tests/SwiftJavaToolLibTests/SimpleJavaProject/build.gradle diff --git a/Tests/SwiftJavaToolTests/SimpleJavaProject/settings.gradle b/Tests/SwiftJavaToolLibTests/SimpleJavaProject/settings.gradle similarity index 100% rename from Tests/SwiftJavaToolTests/SimpleJavaProject/settings.gradle rename to Tests/SwiftJavaToolLibTests/SimpleJavaProject/settings.gradle diff --git a/Tests/SwiftJavaToolTests/SimpleJavaProject/src/main/java/com/example/HelloWorld.java b/Tests/SwiftJavaToolLibTests/SimpleJavaProject/src/main/java/com/example/HelloWorld.java similarity index 100% rename from Tests/SwiftJavaToolTests/SimpleJavaProject/src/main/java/com/example/HelloWorld.java rename to Tests/SwiftJavaToolLibTests/SimpleJavaProject/src/main/java/com/example/HelloWorld.java