From 92883494672a0323d4acda3fcd194d4bf5ccdeba 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/21] Add repositories configuration option in swift-java.config # Conflicts: # Sources/SwiftJavaTool/Commands/ResolveCommand.swift --- .../Configuration.swift | 37 +++++++++++++++++++ .../Commands/ResolveCommand.swift | 29 +++++++++++---- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index 93ee97bf0..b0a713493 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -99,6 +99,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() { } @@ -152,6 +154,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 7cc4c477d..fa47f8129 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,7 +110,8 @@ 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)") @@ -107,7 +119,7 @@ extension SwiftJava.ResolveCommand { let workDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) .appendingPathComponent(".build") - let dependenciesClasspath = await resolveDependencies(workDir: workDir, 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: "") @@ -122,8 +134,9 @@ 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(workDir: URL, dependencies: [JavaDependencyDescriptor]) async -> String { print("Create directory: \(workDir.absoluteString)") @@ -143,7 +156,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( @@ -183,14 +196,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 787def1b78ea2a6dc9b15c1197d1d0bd29c411fe 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/21] 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 000000000..a90207111 --- /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 b0a713493..e030a3a0b 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -182,7 +182,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 2eed33ab28914db7a3412b0a5d767f570be19dc5 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/21] 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 fa47f8129..210dd43a2 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( @@ -136,7 +137,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(workDir: URL, dependencies: [JavaDependencyDescriptor]) async -> String { print("Create directory: \(workDir.absoluteString)") From 7d270f30f8a40b410b202759ccb14495d1832760 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/21] 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 e030a3a0b..404e291c7 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -171,7 +171,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 210dd43a2..82aca198b 100644 --- a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift +++ b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift @@ -205,7 +205,7 @@ extension SwiftJava.ResolveCommand { """ plugins { id 'java-library' } repositories { - \(repositories.compactMap(\.descriptionGradleStyle).joined(separator: "\n")) + \(repositories.compactMap({ $0.renderGradleRepository() }).joined(separator: "\n")) } dependencies { From 30f1013984054b64bbab9944fa8fb101afccb1b5 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/21] 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 573a60cc2..a2389265f 100644 --- a/Samples/JavaDependencySampleApp/Package.swift +++ b/Samples/JavaDependencySampleApp/Package.swift @@ -67,7 +67,8 @@ let package = Package( .product(name: "SwiftJava", package: "swift-java"), .product(name: "CSwiftJavaJNI", package: "swift-java"), .product(name: "JavaUtilFunction", package: "swift-java"), - "JavaCommonsCSV" + "JavaCommonsCSV", + "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 a90207111..000000000 --- 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 13ea6eed3..1c4a06a0d 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 000000000..76f848f95 --- /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 000000000..1e9fe5da5 --- /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 edf1a918363e2e06086693e7c6a1b827304cece9 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/21] 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 404e291c7..4fb54285b 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -99,7 +99,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() { @@ -155,36 +159,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 82aca198b..730b94748 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 8d1b7808119e7dea12fdce3ddde58445779727ab 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/21] 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 b6a18f90e..788b211aa 100644 --- a/Package.swift +++ b/Package.swift @@ -484,6 +484,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 000000000..a9c0ab251 --- /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 f30d4db138010b7e9edebe7af15a7f144047dbcb 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/21] 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 823713836..676454e89 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 6f98a8dc92c8cf4a6aa0dcc88c04f5b83b504b7a 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/21] 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 a9c0ab251..2b80926da 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 4450b498a5fc8b7d6dce47c2dee1d4b651458c42 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/21] 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 788b211aa..3b7afa755 100644 --- a/Package.swift +++ b/Package.swift @@ -483,7 +483,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 81ee741967d325a2f0effeb0778e066b8774d7ab 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/21] 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 a2389265f..48456a461 100644 --- a/Samples/JavaDependencySampleApp/Package.swift +++ b/Samples/JavaDependencySampleApp/Package.swift @@ -68,7 +68,7 @@ let package = Package( .product(name: "CSwiftJavaJNI", 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 000000000..cf900e1ef --- /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 1c4a06a0d..28b7426b3 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 71b56c660f1acaa23ae45fdb44e6caa26dfa448b 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/21] 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 676454e89..4d75f3c08 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 6c73cb50d4910e1e82057211ccb3a6f7089053f6 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/21] [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 2b80926da..bf92d992b 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 e8ebe5efea97296c98ef15d0378272e3bbdbf182 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/21] 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 cf900e1ef..e5c15c07a 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 85ed39fd795733923969e03001466d3515b8b5e8 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/21] 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 4fb54285b..25833b0f1 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -162,6 +162,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 391b55bebe9a04e7733a4ef839d1f6f5bb78750f 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/21] [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 3b7afa755..aa943bf6d 100644 --- a/Package.swift +++ b/Package.swift @@ -495,9 +495,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 bf92d992b..4207c4b98 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 000000000..a83a50411 --- /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 000000000..df0ec21ad --- /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 000000000..5a2c1c002 --- /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 6fe8a7cf0dbd3956a39d9f9cf5f960d5afb30c1e 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/21] [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 4207c4b98..b77f302e9 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 afea9f8530b73bb9f336cf099644ee04ea7c60a5 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/21] [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 a83a50411..e2679a76f 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 df0ec21ad..288d53128 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 5a2c1c002..82ffa33df 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 5f85438b7a6cd64c8db3e266c9244809cffbb597 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/21] Add JavaResolver in SwiftJavaToolLib to resolve for ResolveCommand --- .../Commands/ResolveCommand.swift | 280 +-------------- .../SwiftJavaBaseAsyncParsableCommand.swift | 22 +- Sources/SwiftJavaToolLib/JavaResolver.swift | 331 ++++++++++++++++++ 3 files changed, 336 insertions(+), 297 deletions(-) create mode 100644 Sources/SwiftJavaToolLib/JavaResolver.swift diff --git a/Sources/SwiftJavaTool/Commands/ResolveCommand.swift b/Sources/SwiftJavaTool/Commands/ResolveCommand.swift index 730b94748..5f0504263 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,205 +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 workDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) - .appendingPathComponent(".build") - - 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(workDir: URL, dependencies: [JavaDependencyDescriptor]) async -> String { - print("Create directory: \(workDir.absoluteString)") - - let resolverDir: URL - do { - resolverDir = try createTemporaryDirectory(in: workDir) - } catch { - fatalError("Unable to create temp directory at: \(workDir.absoluteString)! \(error)") - } - 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" + - "Command was: \(CommandLine.arguments.joined(separator: " ").bold)\n" + - "Output was: <<<\(outString)>>>;\n" + - "Err was: <<<\(errString)>>>") - } - - return String(classpathOutput.dropFirst(SwiftJavaClasspathPrefix.count)) - } else { - // Subprocess is unavailable - fatalError("Subprocess is unavailable yet required to execute `gradlew` subprocess. Please update to macOS 15+") - } - } - - /// Creates Gradle project files (build.gradle, settings.gradle.kts) in temporary directory. - func printGradleProject(directory: URL, dependencies: [JavaDependencyDescriptor], 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 ) } @@ -265,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 92818e43a..a6cbd969c 100644 --- a/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift +++ b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift @@ -73,27 +73,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) } } diff --git a/Sources/SwiftJavaToolLib/JavaResolver.swift b/Sources/SwiftJavaToolLib/JavaResolver.swift new file mode 100644 index 000000000..d1e4ef840 --- /dev/null +++ b/Sources/SwiftJavaToolLib/JavaResolver.swift @@ -0,0 +1,331 @@ +//===----------------------------------------------------------------------===// +// +// 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 workDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + .appendingPathComponent(".build") + + let dependenciesClasspath = await resolveDependencies(workDir: workDir, 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(workDir: URL, dependencies: [JavaDependencyDescriptor], repositories: [JavaRepositoryDescriptor]) async -> String { + print("Create directory: \(workDir.absoluteString)") + + let resolverDir: URL + do { + resolverDir = try createTemporaryDirectory(in: workDir) + } catch { + fatalError("Unable to create temp directory at: \(workDir.absoluteString)! \(error)") + } + 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" + + "Command was: \(CommandLine.arguments.joined(separator: " ").bold)\n" + + "Output was: <<<\(outString)>>>;\n" + + "Err was: <<<\(errString)>>>") + } + + return String(classpathOutput.dropFirst(SwiftJavaClasspathPrefix.count)) + } else { + // Subprocess is unavailable + fatalError("Subprocess is unavailable yet required to execute `gradlew` subprocess. Please update to macOS 15+") + } + } + + /// Creates Gradle project files (build.gradle, settings.gradle.kts) in temporary directory. + 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 f34092071ddee90466af299aa6d0bdc3a4cc4126 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/21] [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 aa943bf6d..10938f03b 100644 --- a/Package.swift +++ b/Package.swift @@ -492,20 +492,6 @@ let package = Package( ] ), - .testTarget( - name: "SwiftJavaToolTests", - dependencies: [ - "SwiftJavaTool", - ], - exclude: [ - "SimpleJavaProject", - ], - swiftSettings: [ - .swiftLanguageMode(.v5), - .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) - ] - ), - .testTarget( name: "JavaTypesTests", dependencies: [ @@ -532,6 +518,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 b77f302e9..0e2824a7a 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 From b342e88292a467a1f81f7296d55559dd93830c97 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Sat, 13 Dec 2025 09:48:10 +0100 Subject: [PATCH 21/21] [Test] fix generic type inference --- .../Sources/JavaDependencySample/OrgAndrejsJsonTests.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/OrgAndrejsJsonTests.swift b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/OrgAndrejsJsonTests.swift index e5c15c07a..ffee30462 100644 --- a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/OrgAndrejsJsonTests.swift +++ b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/OrgAndrejsJsonTests.swift @@ -25,9 +25,10 @@ enum OrgAndrejsJsonTests { let json = Json(#"{"host": "localhost", "port": 80}"#) precondition(json.hasOwnProperty("port")) + let port: JavaInteger = json.get("port")! - print(json.get("port").toString()) - precondition(json.get("port").as(JavaInteger.self)!.intValue() == 80) + precondition(port.toString() == "80") + precondition(port.intValue() == 80) print("Reading swift-java.config inside OrgAndrejsJson folder...")