Skip to content

Commit 00928b2

Browse files
committed
Attempt to use build-plugin to bootstrap dependency resolver dependency
1 parent 1f23265 commit 00928b2

File tree

7 files changed

+852
-4
lines changed

7 files changed

+852
-4
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
import PackagePlugin
17+
18+
fileprivate let SwiftJavaConfigFileName = "swift-java.config"
19+
20+
@main
21+
struct Java2SwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
22+
23+
var pluginName: String = "swift-java-bootstrap"
24+
var verbose: Bool = getEnvironmentBool("SWIFT_JAVA_VERBOSE")
25+
26+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
27+
log("Create build commands for target '\(target.name)'")
28+
guard let sourceModule = target.sourceModule else { return [] }
29+
30+
let executable = try context.tool(named: "SwiftJavaBootstrapJavaTool").url
31+
var commands: [Command] = []
32+
33+
// Note: Target doesn't have a directoryURL counterpart to directory,
34+
// so we cannot eliminate this deprecation warning.
35+
let sourceDir = target.directory.string
36+
37+
// The name of the configuration file JavaKit.config from the target for
38+
// which we are generating Swift wrappers for Java classes.
39+
let configFile = URL(filePath: sourceDir)
40+
.appending(path: SwiftJavaConfigFileName)
41+
let config = try readConfiguration(sourceDir: sourceDir)
42+
43+
log("Config on path: \(configFile.path(percentEncoded: false))")
44+
log("Config was: \(config)")
45+
var javaDependencies = config.dependencies ?? []
46+
47+
/// Find the manifest files from other Java2Swift executions in any targets
48+
/// this target depends on.
49+
var dependentConfigFiles: [(String, URL)] = []
50+
func searchForConfigFiles(in target: any Target) {
51+
// log("Search for config files in target: \(target.name)")
52+
let dependencyURL = URL(filePath: target.directory.string)
53+
54+
// Look for a config file within this target.
55+
let dependencyConfigURL = dependencyURL
56+
.appending(path: SwiftJavaConfigFileName)
57+
let dependencyConfigString = dependencyConfigURL
58+
.path(percentEncoded: false)
59+
60+
if FileManager.default.fileExists(atPath: dependencyConfigString) {
61+
dependentConfigFiles.append((target.name, dependencyConfigURL))
62+
}
63+
}
64+
65+
// Process direct dependencies of this target.
66+
for dependency in target.dependencies {
67+
switch dependency {
68+
case .target(let target):
69+
searchForConfigFiles(in: target)
70+
71+
case .product(let product):
72+
for target in product.targets {
73+
searchForConfigFiles(in: target)
74+
}
75+
76+
@unknown default:
77+
break
78+
}
79+
}
80+
81+
// Process indirect target dependencies.
82+
for dependency in target.recursiveTargetDependencies {
83+
searchForConfigFiles(in: dependency)
84+
}
85+
86+
var arguments: [String] = []
87+
arguments += argumentsModuleName(sourceModule: sourceModule)
88+
arguments += argumentsOutputDirectory(context: context)
89+
90+
arguments += dependentConfigFiles.flatMap { moduleAndConfigFile in
91+
let (moduleName, configFile) = moduleAndConfigFile
92+
return [
93+
"--depends-on",
94+
"\(moduleName)=\(configFile.path(percentEncoded: false))"
95+
]
96+
}
97+
arguments.append(configFile.path(percentEncoded: false))
98+
99+
let classes = config.classes ?? [:]
100+
print("Classes to wrap: \(classes.map(\.key))")
101+
102+
/// Determine the set of Swift files that will be emitted by the Java2Swift tool.
103+
// TODO: this is not precise and won't work with more advanced Java files, e.g. lambdas etc.
104+
let outputDirectoryGenerated = self.outputDirectory(context: context, generated: true)
105+
let outputSwiftFiles = classes.map { (javaClassName, swiftName) in
106+
let swiftNestedName = swiftName.replacingOccurrences(of: ".", with: "+")
107+
return outputDirectoryGenerated.appending(path: "\(swiftNestedName).swift")
108+
}
109+
110+
arguments += [
111+
"--cache-directory",
112+
context.pluginWorkDirectoryURL.path(percentEncoded: false)
113+
]
114+
115+
// Find the Java .class files generated from prior plugins.
116+
let compiledClassFiles = sourceModule.pluginGeneratedResources.filter { url in
117+
url.pathExtension == "class"
118+
}
119+
120+
if let firstClassFile = compiledClassFiles.first {
121+
// Keep stripping off parts of the path until we hit the "Java" part.
122+
// That's where the class path starts.
123+
var classpath = firstClassFile
124+
while classpath.lastPathComponent != "Java" {
125+
classpath.deleteLastPathComponent()
126+
}
127+
arguments += ["--classpath", classpath.path()]
128+
}
129+
130+
var fetchDependenciesOutputFiles: [URL] = []
131+
if let dependencies = config.dependencies, !dependencies.isEmpty {
132+
let displayName = "Fetch (Java) dependencies for Swift target \(sourceModule.name)"
133+
log("Prepared: \(displayName)")
134+
135+
let arguments = [
136+
"--fetch", configFile.path(percentEncoded: false),
137+
"--module-name", sourceModule.name,
138+
"--output-directory", outputDirectory(context: context, generated: false).path(percentEncoded: false)
139+
]
140+
141+
log("Command: \(executable) \(arguments.joined(separator: " "))")
142+
143+
fetchDependenciesOutputFiles += [
144+
outputFilePath(context: context, generated: false, filename: "\(sourceModule.name).swift-java.classpath")
145+
]
146+
147+
commands += [
148+
.buildCommand(
149+
displayName: displayName,
150+
executable: executable,
151+
arguments: arguments,
152+
inputFiles: [configFile],
153+
outputFiles: fetchDependenciesOutputFiles
154+
)
155+
]
156+
} else {
157+
log("No dependencies to fetch for target \(sourceModule.name)")
158+
}
159+
160+
return commands
161+
}
162+
}
163+
164+
extension Java2SwiftBuildToolPlugin {
165+
func argumentsModuleName(sourceModule: Target) -> [String] {
166+
return [
167+
"--module-name", sourceModule.name
168+
]
169+
}
170+
171+
func argumentsOutputDirectory(context: PluginContext, generated: Bool = true) -> [String] {
172+
return [
173+
"--output-directory",
174+
outputDirectory(context: context, generated: generated).path(percentEncoded: false)
175+
]
176+
}
177+
178+
func outputDirectory(context: PluginContext, generated: Bool = true) -> URL {
179+
let dir = context.pluginWorkDirectoryURL
180+
if generated {
181+
return dir.appending(path: "generated")
182+
} else {
183+
return dir
184+
}
185+
}
186+
187+
func outputFilePath(context: PluginContext, generated: Bool, filename: String) -> URL {
188+
outputDirectory(context: context, generated: generated).appending(path: filename)
189+
}
190+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../PluginsShared

Samples/JavaDependencySampleApp/ci-validate.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
set -e
44
set -x
55

6+
# TODO: this is a workaround for build plugins getting stuck running the bootstrap plugin
7+
cd ../../
68
swift build --product SwiftJavaBootstrapJavaTool
7-
.build/arm64-apple-macosx/debug/SwiftJavaBootstrapJavaTool --fetch /Users/ktoso/code/swift-java/Sources/JavaKitDependencyResolver/swift-java.config --module-name JavaKitDependencyResolver --output-directory && .build/plugins/outputs/swift-java/JavaKitDependencyResolver/destination/SwiftJavaBootstrapJavaPlugin
9+
.build/arm64-apple-macosx/debug/SwiftJavaBootstrapJavaTool --fetch Sources/JavaKitDependencyResolver/swift-java.config --module-name JavaKitDependencyResolver --output-directory .build/plugins/outputs/swift-java/JavaKitDependencyResolver/destination/SwiftJavaBootstrapJavaPlugin
810

911
cd -
1012
swift run --disable-sandbox

Sources/JavaKitConfigurationShared/Configuration.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public func readConfiguration(configPath: URL, file: String = #fileID, line: UIn
116116
let configData = try Data(contentsOf: configPath)
117117
return try JSONDecoder().decode(Configuration.self, from: configData)
118118
} catch {
119-
throw ConfigurationError(message: "Failed to parse SwiftJava configuration at '\(configPath)'!", error: error,
119+
throw ConfigurationError(message: "Failed to parse SwiftJava configuration at '\(configPath.absoluteURL)'!", error: error,
120120
file: file, line: line)
121121
}
122122
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
import Synchronization
17+
import JavaKitConfigurationShared
18+
import Dispatch
19+
import _Subprocess
20+
21+
@available(macOS 15.0, *)
22+
@main
23+
final class SwiftJavaBootstrapJavaTool {
24+
25+
let SwiftJavaClasspathPrefix = "SWIFT_JAVA_CLASSPATH:"
26+
27+
// We seem to have weird "hang" issues with Gradle launched from Process(), workaround it by existing once we get the classpath
28+
let printRuntimeClasspathTaskName = "printRuntimeClasspath"
29+
30+
let out = Synchronization.Mutex<Data>(Data())
31+
let err = Synchronization.Mutex<Data>(Data())
32+
33+
static func main() async throws {
34+
try await SwiftJavaBootstrapJavaTool().run()
35+
}
36+
37+
func run() async throws {
38+
print("RUN SwiftJavaBootstrapJavaTool: \(CommandLine.arguments)")
39+
40+
var args = CommandLine.arguments
41+
_ = args.removeFirst() // executable
42+
43+
assert(args.removeFirst() == "--fetch")
44+
let configPath = args.removeFirst()
45+
46+
assert(args.removeFirst() == "--module-name")
47+
let moduleName = args.removeFirst()
48+
49+
assert(args.removeFirst() == "--output-directory")
50+
let outputDirectoryPath = args.removeFirst()
51+
52+
let config = try readConfiguration(configPath: URL(fileURLWithPath: configPath))
53+
54+
// We only support a single dependency right now.
55+
let localGradleProjectDependencyName = (config.dependencies ?? []).filter {
56+
$0.artifactID.hasPrefix(":")
57+
}.map {
58+
$0.artifactID
59+
}.first!
60+
61+
let process = try await Subprocess.run(
62+
.at("./gradlew"),
63+
arguments: [
64+
"--no-daemon",
65+
"--rerun-tasks",
66+
// "--debug",
67+
// "\(localGradleProjectDependencyName):jar",
68+
"\(localGradleProjectDependencyName):\(printRuntimeClasspathTaskName)",
69+
],
70+
workingDirectory: "/Users/ktoso/code/swift-java"
71+
)
72+
73+
let outString = String(
74+
data: process.standardOutput,
75+
encoding: .utf8
76+
)
77+
let errString = String(
78+
data: process.standardError,
79+
encoding: .utf8
80+
)
81+
82+
print("OUT ==== \(outString?.count) ::: \(outString ?? "")")
83+
print("ERR ==== \(errString?.count) ::: \(errString ?? "")")
84+
85+
let classpathOutput: String
86+
if let found = outString?.split(separator: "\n").first(where: { $0.hasPrefix(self.SwiftJavaClasspathPrefix) }) {
87+
classpathOutput = String(found)
88+
} else if let found = errString?.split(separator: "\n").first(where: { $0.hasPrefix(self.SwiftJavaClasspathPrefix) }) {
89+
classpathOutput = String(found)
90+
} else {
91+
let suggestDisablingSandbox = "It may be that the Sandbox has prevented dependency fetching, please re-run with '--disable-sandbox'."
92+
fatalError("Gradle output had no SWIFT_JAVA_CLASSPATH! \(suggestDisablingSandbox). \n" +
93+
"Output was:<<<\(outString ?? "<empty>")>>>; Err was:<<<\(errString ?? "<empty>")>>>")
94+
}
95+
96+
let classpathString = String(classpathOutput.dropFirst(self.SwiftJavaClasspathPrefix.count))
97+
98+
let classpathOutputURL =
99+
URL(fileURLWithPath: outputDirectoryPath)
100+
.appendingPathComponent("\(moduleName).swift-java.classpath", isDirectory: false)
101+
102+
try! classpathString.write(to: classpathOutputURL, atomically: true, encoding: .utf8)
103+
104+
print("[swift-java-bootstrap] Done, written classpath to: \(classpathOutputURL)")
105+
}
106+
107+
func writeBuildGradle(directory: URL) {
108+
// """
109+
// plugins { id 'java-library' }
110+
// repositories { mavenCentral() }
111+
//
112+
// dependencies {
113+
// implementation("dev.gradleplugins:gradle-api:8.10.1")
114+
// }
115+
//
116+
// task \(printRuntimeClasspathTaskName) {
117+
// def runtimeClasspath = sourceSets.main.runtimeClasspath
118+
// inputs.files(runtimeClasspath)
119+
// doLast {
120+
// println("CLASSPATH:${runtimeClasspath.asPath}")
121+
// }
122+
// }
123+
// """.write(to: URL(fileURLWithPath: tempDir.appendingPathComponent("build.gradle")).path(percentEncoded: false), atomically: true, encoding: .utf8)
124+
//
125+
// """
126+
// rootProject.name = "swift-java-resolve-temp-project"
127+
// """.write(to: URL(fileURLWithPath: tempDir.appendingPathComponent("settings.gradle.kts")).path(percentEncoded: false), atomically: true, encoding: .utf8)
128+
}
129+
130+
}

Sources/_Subprocess/Subprocess+IO.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@ import Foundation
1717

1818
import Dispatch
1919
import SystemPackage
20-
import Synchronization
2120

2221
// Naive Mutex so we don't have to update the macOS dependency
2322
final class _Mutex<Value>: Sendable {
24-
let lock = NSLock()
23+
let lock = Lock()
2524

2625
var value: Value
2726

0 commit comments

Comments
 (0)