Skip to content

Commit c434bcc

Browse files
committed
implemented resolving
1 parent c769b1a commit c434bcc

23 files changed

+1015
-290
lines changed

JavaKit/build.gradle

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
plugins {
16+
id("build-logic.java-application-conventions")
17+
}
18+
19+
group = "org.swift.javakit"
20+
version = "1.0-SNAPSHOT"
21+
22+
repositories {
23+
mavenCentral()
24+
}
25+
26+
java {
27+
toolchain {
28+
languageVersion.set(JavaLanguageVersion.of(22))
29+
}
30+
}
31+
32+
dependencies {
33+
implementation("dev.gradleplugins:gradle-api:8.10.1")
34+
35+
testImplementation(platform("org.junit:junit-bom:5.10.0"))
36+
testImplementation("org.junit.jupiter:junit-jupiter")
37+
}
38+
39+
tasks.test {
40+
useJUnitPlatform()
41+
testLogging {
42+
events("passed", "skipped", "failed")
43+
}
44+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
package org.swift.javakit.annotations;
16+
17+
import java.lang.annotation.Retention;
18+
import java.lang.annotation.RetentionPolicy;
19+
20+
/**
21+
* Since some public methods may not appear as used in Java source code, but are used by Swift,
22+
* we can use this source annotation to mark such entry points to not accidentally remove them with
23+
* "safe delete" refactorings in Java IDEs which would be unaware of the usages from Swift.
24+
*/
25+
@SuppressWarnings("unused") // used from Swift
26+
@Retention(RetentionPolicy.SOURCE)
27+
public @interface UsedFromSwift {
28+
}
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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+
package org.swift.javakit.dependencies;
16+
17+
import org.gradle.tooling.GradleConnector;
18+
import org.swift.javakit.annotations.UsedFromSwift;
19+
20+
import java.io.*;
21+
import java.nio.file.Files;
22+
import java.nio.file.Path;
23+
import java.nio.file.StandardOpenOption;
24+
import java.util.Arrays;
25+
import java.util.concurrent.TimeUnit;
26+
import java.util.stream.Stream;
27+
28+
/**
29+
* Fetches dependencies using the Gradle resolver and returns the resulting classpath which includes
30+
* the fetched dependency and all of its dependencies.
31+
*/
32+
public class DependencyResolver {
33+
34+
private static final String COMMAND_OUTPUT_LINE_PREFIX_CLASSPATH = "CLASSPATH:";
35+
private static final String CLASSPATH_CACHE_FILENAME = "JavaKitDependencyResolver.classpath.swift-java";
36+
37+
public static String GRADLE_API_DEPENDENCY = "dev.gradleplugins:gradle-api:8.10.1";
38+
public static String[] BASE_DEPENDENCIES = {
39+
GRADLE_API_DEPENDENCY
40+
};
41+
42+
/**
43+
* May throw runtime exceptions including {@link org.gradle.api.internal.artifacts.ivyservice.TypedResolveException}
44+
* if unable to resolve a dependency.
45+
*/
46+
@UsedFromSwift
47+
@SuppressWarnings("unused")
48+
public static String resolveDependenciesToClasspath(String projectBaseDirectoryString, String[] dependencies) throws IOException {
49+
try {
50+
simpleLog("Fetch dependencies: " + Arrays.toString(dependencies));
51+
simpleLog("projectBaseDirectoryString = " + projectBaseDirectoryString);
52+
var projectBasePath = new File(projectBaseDirectoryString).toPath();
53+
54+
File projectDir = Files.createTempDirectory("java-swift-dependencies").toFile();
55+
projectDir.mkdirs();
56+
57+
if (hasDependencyResolverDependenciesLoaded()) {
58+
// === Resolve dependencies using Gradle API in-process
59+
simpleLog("Gradle API runtime dependency is available, resolve dependencies...");
60+
return resolveDependenciesUsingAPI(projectDir, dependencies);
61+
}
62+
63+
// === Bootstrap the resolver dependencies and cache them
64+
simpleLog("Gradle API not available on classpath, bootstrap %s dependencies: %s"
65+
.formatted(DependencyResolver.class.getSimpleName(), Arrays.toString(BASE_DEPENDENCIES)));
66+
String dependencyResolverDependenciesClasspath = bootstrapDependencyResolverClasspath();
67+
writeDependencyResolverClasspath(projectBasePath, dependencyResolverDependenciesClasspath);
68+
69+
// --- Resolve dependencies using sub-process process
70+
// TODO: it would be nice to just add the above classpath to the system classloader and here call the API
71+
// immediately, but that's challenging and not a stable API we can rely on (hacks exist to add paths
72+
// to system classloader but are not reliable).
73+
printBuildFiles(projectDir, dependencies);
74+
return resolveDependenciesWithSubprocess(projectDir);
75+
} catch (Exception e) {
76+
e.printStackTrace();
77+
throw e;
78+
}
79+
}
80+
81+
82+
/**
83+
* Use an external {@code gradle} invocation in order to download dependencies such that we can use `gradle-api`
84+
* next time we want to resolve dependencies. This uses an external process and is sligtly worse than using the API
85+
* directly.
86+
*
87+
* @return classpath obtained for the dependencies
88+
* @throws IOException if file IO failed during mock project creation
89+
* @throws SwiftJavaBootstrapException if the resolve failed for some other reason
90+
*/
91+
private static String bootstrapDependencyResolverClasspath() throws IOException, SwiftJavaBootstrapException {
92+
var dependencies = BASE_DEPENDENCIES;
93+
simpleLog("Bootstrap gradle-api for DependencyResolver: " + Arrays.toString(dependencies));
94+
95+
File bootstrapDir = Files.createTempDirectory("swift-java-dependency-resolver").toFile();
96+
bootstrapDir.mkdirs();
97+
simpleLog("Bootstrap dependencies using project at: %s".formatted(bootstrapDir));
98+
99+
printBuildFiles(bootstrapDir, dependencies);
100+
101+
var bootstrapClasspath = resolveDependenciesWithSubprocess(bootstrapDir);
102+
simpleLog("Prepared dependency resolver bootstrap classpath: " + bootstrapClasspath.split(":").length + " entries");
103+
104+
return bootstrapClasspath;
105+
106+
}
107+
108+
private static String resolveDependenciesWithSubprocess(File gradleProjectDir) throws IOException {
109+
if (!gradleProjectDir.isDirectory()) {
110+
throw new IllegalArgumentException("Gradle project directory is not a directory: " + gradleProjectDir);
111+
}
112+
113+
File stdoutFile = File.createTempFile("swift-java-bootstrap", ".stdout", gradleProjectDir);
114+
stdoutFile.deleteOnExit();
115+
File stderrFile = File.createTempFile("swift-java-bootstrap", ".stderr", gradleProjectDir);
116+
stderrFile.deleteOnExit();
117+
118+
try {
119+
ProcessBuilder gradleBuilder = new ProcessBuilder("gradle", ":printRuntimeClasspath");
120+
gradleBuilder.directory(gradleProjectDir);
121+
gradleBuilder.redirectOutput(stdoutFile);
122+
gradleBuilder.redirectError(stderrFile);
123+
Process gradleProcess = gradleBuilder.start();
124+
gradleProcess.waitFor(10, TimeUnit.MINUTES); // TODO: must be configurable
125+
126+
if (gradleProcess.exitValue() != 0) {
127+
throw new SwiftJavaBootstrapException("Failed to resolve bootstrap dependencies, exit code: " + gradleProcess.exitValue());
128+
}
129+
130+
Stream<String> lines = Files.readAllLines(stdoutFile.toPath()).stream();
131+
var bootstrapClasspath = getClasspathFromGradleCommandOutput(lines);
132+
return bootstrapClasspath;
133+
} catch (Exception ex) {
134+
simpleLog("stdoutFile = " + stdoutFile);
135+
simpleLog("stderrFile = " + stderrFile);
136+
137+
ex.printStackTrace();
138+
throw new SwiftJavaBootstrapException("Failed to bootstrap dependencies necessary for " +
139+
DependencyResolver.class.getCanonicalName() + "!", ex);
140+
}
141+
}
142+
143+
private static void writeDependencyResolverClasspath(Path projectBasePath, String dependencyResolverDependenciesClasspath) throws IOException {
144+
File swiftBuildDirectory = new File(String.valueOf(projectBasePath), ".build");
145+
swiftBuildDirectory.mkdirs();
146+
147+
File dependencyResolverClasspathCacheFile = new File(swiftBuildDirectory, CLASSPATH_CACHE_FILENAME);
148+
dependencyResolverClasspathCacheFile.createNewFile();
149+
simpleLog("Cache %s dependencies classpath at: '%s'. Subsequent dependency resolutions will use gradle-api."
150+
.formatted(DependencyResolver.class.getSimpleName(), dependencyResolverClasspathCacheFile.toPath()));
151+
152+
Files.writeString(
153+
dependencyResolverClasspathCacheFile.toPath(),
154+
dependencyResolverDependenciesClasspath,
155+
StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
156+
}
157+
158+
/**
159+
* Detect if we have the necessary dependencies loaded.
160+
*/
161+
private static boolean hasDependencyResolverDependenciesLoaded() {
162+
return hasDependencyResolverDependenciesLoaded(DependencyResolver.class.getClassLoader());
163+
}
164+
165+
/**
166+
* Resolve dependencies in the passed project directory and return the resulting classpath.
167+
*
168+
* @return classpath which was resolved for the dependencies
169+
*/
170+
private static String resolveDependenciesUsingAPI(File projectDir, String[] dependencies) throws FileNotFoundException {
171+
printBuildFiles(projectDir, dependencies);
172+
173+
var connection = GradleConnector.newConnector()
174+
.forProjectDirectory(projectDir)
175+
.connect();
176+
177+
try (connection) {
178+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
179+
PrintStream printStream = new PrintStream(outputStream);
180+
181+
connection.newBuild().forTasks(":printRuntimeClasspath")
182+
.setStandardError(new NoopOutputStream())
183+
.setStandardOutput(printStream)
184+
.run();
185+
186+
var all = outputStream.toString();
187+
var classpath = Arrays.stream(all.split("\n"))
188+
.filter(s -> s.startsWith(COMMAND_OUTPUT_LINE_PREFIX_CLASSPATH))
189+
.map(s -> s.substring(COMMAND_OUTPUT_LINE_PREFIX_CLASSPATH.length()))
190+
.findFirst().orElseThrow(() -> new RuntimeException("Could not find classpath output from ':printRuntimeClasspath' task."));
191+
return classpath;
192+
}
193+
}
194+
195+
private static String getClasspathFromGradleCommandOutput(Stream<String> lines) {
196+
return lines.filter(s -> s.startsWith(COMMAND_OUTPUT_LINE_PREFIX_CLASSPATH))
197+
.map(s -> s.substring(COMMAND_OUTPUT_LINE_PREFIX_CLASSPATH.length()))
198+
.findFirst().orElseThrow(() -> new RuntimeException("Could not find classpath output from gradle command output task."));
199+
}
200+
201+
202+
private static boolean hasDependencyResolverDependenciesLoaded(ClassLoader classLoader) {
203+
try {
204+
classLoader.loadClass("org.gradle.tooling.GradleConnector");
205+
return true;
206+
} catch (ClassNotFoundException e) {
207+
return false;
208+
}
209+
}
210+
211+
private static void printBuildFiles(File projectDir, String[] dependencies) throws FileNotFoundException {
212+
File buildFile = new File(projectDir, "build.gradle");
213+
try (PrintWriter writer = new PrintWriter(buildFile)) {
214+
writer.println("plugins { id 'java-library' }");
215+
writer.println("repositories { mavenCentral() }");
216+
217+
writer.println("dependencies {");
218+
for (String dependency : dependencies) {
219+
writer.println("implementation(\"" + dependency + "\")");
220+
}
221+
writer.println("}");
222+
223+
writer.println("""
224+
task printRuntimeClasspath {
225+
def runtimeClasspath = sourceSets.main.runtimeClasspath
226+
inputs.files(runtimeClasspath)
227+
doLast {
228+
println("CLASSPATH:${runtimeClasspath.asPath}")
229+
}
230+
}
231+
""");
232+
}
233+
234+
File settingsFile = new File(projectDir, "settings.gradle.kts");
235+
try (PrintWriter writer = new PrintWriter(settingsFile)) {
236+
writer.println("""
237+
rootProject.name = "swift-java-resolve-temp-project"
238+
""");
239+
}
240+
}
241+
242+
private static void simpleLog(String message) {
243+
System.err.println("[info][swift-java/" + DependencyResolver.class.getSimpleName() + "] " + message);
244+
}
245+
246+
private static class NoopOutputStream extends OutputStream {
247+
@Override
248+
public void write(int b) throws IOException {
249+
// ignore
250+
}
251+
}
252+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
package org.swift.javakit.dependencies;
16+
17+
public class SwiftJavaBootstrapException extends RuntimeException {
18+
public SwiftJavaBootstrapException(String message) {
19+
super(message);
20+
}
21+
22+
public SwiftJavaBootstrapException(String message, Exception ex) {
23+
super(message, ex);
24+
}
25+
}

0 commit comments

Comments
 (0)