Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9b624db
Add repositories configuration option in swift-java.config
bo2themax Aug 26, 2025
7bf0de7
Add a repositories configuration example and fix artifactUrls output
bo2themax Aug 26, 2025
6a126ec
Update naming convention
bo2themax Aug 26, 2025
ef69b18
Rename descriptionGradleStyle
bo2themax Aug 28, 2025
5a6616b
Add JavaJson Example
bo2themax Aug 28, 2025
9416c56
Change JavaRepositoryDescriptor to enum
bo2themax Aug 28, 2025
149358a
Add JavaRepositoryTests to test dependencies resolving with custom re…
bo2themax Aug 28, 2025
52243ad
Add documentation for swift-java resolve
bo2themax Aug 28, 2025
764d586
Add another non-resolvable config to verify artifactUrls
bo2themax Aug 28, 2025
7f6cd04
Move JavaRepositoryTests to SwiftJavaToolTests
bo2themax Aug 29, 2025
8bb6cfa
Rename JavaJson to OrgAndrejsJson
bo2themax Aug 29, 2025
c57c017
Add referenced issue in the document
bo2themax Aug 29, 2025
ab8f3ab
[Test] Change minified json to pretty printed
bo2themax Aug 29, 2025
f6f9800
Remove System dependency from OrgAndrejsJsonTests
bo2themax Aug 29, 2025
de3dab3
Add more referenced documents for JavaRepositoryDescriptor
bo2themax Aug 29, 2025
f6ad15f
[Test] Add a SimpleJavaProject to JavaRepositoryTests
bo2themax Aug 29, 2025
8e0687b
Merge branch 'swiftlang:main' into main
bo2themax Aug 29, 2025
a61c518
[Test] Update error messages in JavaRepositoryTests.swift
bo2themax Aug 29, 2025
967ccaf
[Test] Add missing license headers
bo2themax Aug 31, 2025
8fe1360
Add JavaResolver in SwiftJavaToolLib to resolve for ResolveCommand
bo2themax Sep 1, 2025
be87290
[Test] Move SwiftJavaToolTests/JavaRepositoryTests
bo2themax Sep 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add JavaResolver in SwiftJavaToolLib to resolve for ResolveCommand
  • Loading branch information
bo2themax committed Sep 1, 2025
commit 8fe1360b2fd3bbc5bf1397b6bc0499c1a224293f
270 changes: 4 additions & 266 deletions Sources/SwiftJavaTool/Commands/ResolveCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -58,195 +47,13 @@ extension SwiftJava {
}

extension SwiftJava.ResolveCommand {
var SwiftJavaClasspathPrefix: String { "SWIFT_JAVA_CLASSPATH:" }
var printRuntimeClasspathTaskName: String { "printRuntimeClasspath" }

mutating func runSwiftJavaCommand(config: inout Configuration) async throws {
var dependenciesToResolve: [JavaDependencyDescriptor] = []
if let input, let inputDependencies = parseDependencyDescriptor(input) {
dependenciesToResolve.append(inputDependencies)
}
if let dependencies = config.dependencies {
dependenciesToResolve += dependencies
}

if dependenciesToResolve.isEmpty {
print("[warn][swift-java] Attempted to 'resolve' dependencies but no dependencies specified in swift-java.config or command input!")
return
}

var configuredRepositories: [JavaRepositoryDescriptor] = []

if let repositories = config.repositories {
configuredRepositories += repositories
}

if !configuredRepositories.contains(where: { $0 == .other("mavenCentral") }) {
// swift-java dependencies are originally located in mavenCentral
configuredRepositories.append(.other("mavenCentral"))
}

let dependenciesClasspath =
try await resolveDependencies(swiftModule: swiftModule, dependencies: dependenciesToResolve, repositories: configuredRepositories)

// FIXME: disentangle the output directory from SwiftJava and then make it a required option in this Command
guard let outputDirectory = self.commonOptions.outputDirectory else {
fatalError("error: Must specify --output-directory in 'resolve' mode! This option will become explicitly required")
}

try writeSwiftJavaClasspathFile(
try await JavaResolver.runResolveCommand(
config: &config,
input: input,
swiftModule: swiftModule,
outputDirectory: outputDirectory,
resolvedClasspath: dependenciesClasspath)
}


/// Resolves Java dependencies from swift-java.config and returns classpath information.
///
/// - Parameters:
/// - swiftModule: module name from --swift-module. e.g.: --swift-module MySwiftModule
/// - dependencies: parsed maven-style dependency descriptors (groupId:artifactId:version)
/// from Sources/MySwiftModule/swift-java.config "dependencies" array.
/// - repositories: repositories used to resolve dependencies
///
/// - Throws:
func resolveDependencies(
swiftModule: String, dependencies: [JavaDependencyDescriptor],
repositories: [JavaRepositoryDescriptor]
) async throws -> ResolvedDependencyClasspath {
let deps = dependencies.map { $0.descriptionGradleStyle }
print("[debug][swift-java] Resolve and fetch dependencies for: \(deps)")

let dependenciesClasspath = await resolveDependencies(dependencies: dependencies, repositories: repositories)
let classpathEntries = dependenciesClasspath.split(separator: ":")

print("[info][swift-java] Resolved classpath for \(deps.count) dependencies of '\(swiftModule)', classpath entries: \(classpathEntries.count), ", terminator: "")
print("done.".green)

for entry in classpathEntries {
print("[info][swift-java] Classpath entry: \(entry)")
}

return ResolvedDependencyClasspath(for: dependencies, classpath: dependenciesClasspath)
}


/// Resolves maven-style dependencies from swift-java.config under temporary project directory.
///
/// - Parameter dependencies: maven-style dependencies to resolve
/// - Parameter repositories: repositories used to resolve dependencies
/// - Returns: Colon-separated classpath
func resolveDependencies(dependencies: [JavaDependencyDescriptor], repositories: [JavaRepositoryDescriptor]) async -> String {
let workDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
.appendingPathComponent(".build")
let resolverDir = try! createTemporaryDirectory(in: workDir)
defer {
try? FileManager.default.removeItem(at: resolverDir)
}

// We try! because it's easier to track down errors like this than when we bubble up the errors,
// and don't get great diagnostics or backtraces due to how swiftpm plugin tools are executed.

try! copyGradlew(to: resolverDir)

try! printGradleProject(directory: resolverDir, dependencies: dependencies, repositories: repositories)

if #available(macOS 15, *) {
let process = try! await _Subprocess.run(
.path(FilePath(resolverDir.appendingPathComponent("gradlew").path)),
arguments: [
"--no-daemon",
"--rerun-tasks",
"\(printRuntimeClasspathTaskName)",
],
workingDirectory: Optional(FilePath(resolverDir.path)),
// TODO: we could move to stream processing the outputs
output: .string(limit: Int.max, encoding: UTF8.self), // Don't limit output, we know it will be reasonable size
error: .string(limit: Int.max, encoding: UTF8.self) // Don't limit output, we know it will be reasonable size
)

let outString = process.standardOutput ?? ""
let errString = process.standardError ?? ""

let classpathOutput: String
if let found = outString.split(separator: "\n").first(where: { $0.hasPrefix(self.SwiftJavaClasspathPrefix) }) {
classpathOutput = String(found)
} else if let found = errString.split(separator: "\n").first(where: { $0.hasPrefix(self.SwiftJavaClasspathPrefix) }) {
classpathOutput = String(found)
} else {
let suggestDisablingSandbox = "It may be that the Sandbox has prevented dependency fetching, please re-run with '--disable-sandbox'."
fatalError("Gradle output had no SWIFT_JAVA_CLASSPATH! \(suggestDisablingSandbox). \n" +
"Output was:<<<\(outString)>>>; Err was:<<<\(errString ?? "<empty>")>>>")
}

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
)
}

Expand All @@ -255,74 +62,5 @@ extension SwiftJava.ResolveCommand {
let camelCased = components.map { $0.capitalized }.joined()
return camelCased
}

// copy gradlew & gradle.bat from root, throws error if there is no gradle setup.
func copyGradlew(to resolverWorkDirectory: URL) throws {
var searchDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)

while searchDir.pathComponents.count > 1 {
let gradlewFile = searchDir.appendingPathComponent("gradlew")
let gradlewExists = FileManager.default.fileExists(atPath: gradlewFile.path)
guard gradlewExists else {
searchDir = searchDir.deletingLastPathComponent()
continue
}

let gradlewBatFile = searchDir.appendingPathComponent("gradlew.bat")
let gradlewBatExists = FileManager.default.fileExists(atPath: gradlewFile.path)

let gradleDir = searchDir.appendingPathComponent("gradle")
let gradleDirExists = FileManager.default.fileExists(atPath: gradleDir.path)
guard gradleDirExists else {
searchDir = searchDir.deletingLastPathComponent()
continue
}

// TODO: gradle.bat as well
try? FileManager.default.copyItem(
at: gradlewFile,
to: resolverWorkDirectory.appendingPathComponent("gradlew"))
if gradlewBatExists {
try? FileManager.default.copyItem(
at: gradlewBatFile,
to: resolverWorkDirectory.appendingPathComponent("gradlew.bat"))
}
try? FileManager.default.copyItem(
at: gradleDir,
to: resolverWorkDirectory.appendingPathComponent("gradle"))
return
}
}

func createTemporaryDirectory(in directory: URL) throws -> URL {
let uuid = UUID().uuidString
let resolverDirectoryURL = directory.appendingPathComponent("swift-java-dependencies-\(uuid)")

try FileManager.default.createDirectory(at: resolverDirectoryURL, withIntermediateDirectories: true, attributes: nil)

return resolverDirectoryURL
}

}

struct ResolvedDependencyClasspath: CustomStringConvertible {
/// The dependency identifiers this is the classpath for.
let rootDependencies: [JavaDependencyDescriptor]

/// Plain string representation of a Java classpath
let classpath: String

var classpathEntries: [String] {
classpath.split(separator: ":").map(String.init)
}

init(for rootDependencies: [JavaDependencyDescriptor], classpath: String) {
self.rootDependencies = rootDependencies
self.classpath = classpath
}

var description: String {
"JavaClasspath(for: \(rootDependencies), classpath: \(classpath))"
}
}

24 changes: 2 additions & 22 deletions Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,27 +69,7 @@ extension SwiftJavaBaseAsyncParsableCommand {
outputDirectory: Foundation.URL?,
to filename: String,
description: String) throws {
guard let outputDir = outputDirectory else {
print("// \(filename) - \(description)")
print(contents)
return
}

// If we haven't tried to create the output directory yet, do so now before
// we write any files to it.
// if !createdOutputDirectory {
try FileManager.default.createDirectory(
at: outputDir,
withIntermediateDirectories: true
)
// createdOutputDirectory = true
//}

// Write the file:
let file = outputDir.appendingPathComponent(filename)
print("[trace][swift-java] Writing \(description) to '\(file.path)'... ", terminator: "")
try contents.write(to: file, atomically: true, encoding: .utf8)
print("done.".green)
try JavaResolver.writeContents(contents, outputDirectory: outputDirectory, to: filename, description: description)
}
}

Expand Down Expand Up @@ -167,4 +147,4 @@ extension SwiftJavaBaseAsyncParsableCommand {
config.logLevel = command.logLevel
return config
}
}
}
Loading