From 79882d8800a0b8dc9e39c3c069685d509777d332 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Tue, 10 Dec 2024 10:59:59 +0900 Subject: [PATCH] Fetching dependencies declared in swift-java.config (#191) --- .github/scripts/validate_sample.sh | 32 ++ .github/scripts/validate_samples.sh | 37 -- .github/workflows/pull_request.yml | 14 +- .licenseignore | 3 +- JavaKit/build.gradle | 77 +++ JavaKit/gradlew | 1 + JavaKit/gradlew.bat | 1 + .../javakit/annotations/UsedFromSwift.java | 28 + .../dependencies/DependencyResolver.java | 303 +++++++++++ .../SwiftJavaBootstrapException.java | 25 + .../dependencies/SystemClassLoaderUnsafe.java | 46 ++ Package.swift | 44 +- .../Java2SwiftPlugin/Java2SwiftPlugin.swift | 135 +++-- Plugins/PluginsShared/Configuration.swift | 81 --- .../PluginsShared/JavaKitConfigurationShared | 1 + Plugins/PluginsShared/PluginUtils.swift | 5 + Samples/JavaDependencySampleApp/Package.swift | 101 ++++ .../Sources/JavaCommonsCSV/dummy.swift | 13 + .../Sources/JavaCommonsCSV/swift-java.config | 9 + .../Sources/JavaDependencySample/main.swift | 47 ++ .../JavaDependencySample/swift-java.config | 2 + .../JavaDependencySampleApp/ci-validate.sh | 10 + Samples/JavaKitSampleApp/ci-validate.sh | 3 + Samples/JavaProbablyPrime/ci-validate.sh | 3 + Samples/JavaSieve/.gitignore | 1 + Samples/JavaSieve/README.md | 3 +- .../JavaSieve/Sources/JavaSieve/main.swift | 6 +- .../Sources/JavaSieve/swift-java.config | 2 +- Samples/JavaSieve/ci-validate.sh | 2 +- Sources/JExtractSwift/Swift2Java.swift | 1 + .../JavaToSwift+EmitConfiguration.swift | 122 +++++ .../JavaToSwift+FetchDependencies.swift | 122 +++++ .../JavaToSwift+GenerateWrappers.swift | 145 +++++ Sources/Java2Swift/JavaToSwift.swift | 500 +++++++++--------- Sources/Java2Swift/String+Extensions.swift | 62 +++ Sources/Java2SwiftLib/Configuration.swift | 45 -- .../JavaTranslator+Configuration.swift | 7 +- .../JavaTranslator+Validation.swift | 2 +- Sources/Java2SwiftLib/JavaTranslator.swift | 1 + .../JavaKitVM/JavaVirtualMachine.swift | 68 ++- .../Configuration.swift | 191 +++++++ .../DependencyResolver.swift | 32 ++ .../swift-java.config | 9 + .../TerminalColors.swift | 0 settings.gradle | 1 + 45 files changed, 1872 insertions(+), 471 deletions(-) create mode 100755 .github/scripts/validate_sample.sh delete mode 100755 .github/scripts/validate_samples.sh create mode 100644 JavaKit/build.gradle create mode 120000 JavaKit/gradlew create mode 120000 JavaKit/gradlew.bat create mode 100644 JavaKit/src/main/java/org/swift/javakit/annotations/UsedFromSwift.java create mode 100644 JavaKit/src/main/java/org/swift/javakit/dependencies/DependencyResolver.java create mode 100644 JavaKit/src/main/java/org/swift/javakit/dependencies/SwiftJavaBootstrapException.java create mode 100644 JavaKit/src/main/java/org/swift/javakit/dependencies/SystemClassLoaderUnsafe.java delete mode 100644 Plugins/PluginsShared/Configuration.swift create mode 120000 Plugins/PluginsShared/JavaKitConfigurationShared create mode 100644 Samples/JavaDependencySampleApp/Package.swift create mode 100644 Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/dummy.swift create mode 100644 Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/swift-java.config create mode 100644 Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift create mode 100644 Samples/JavaDependencySampleApp/Sources/JavaDependencySample/swift-java.config create mode 100755 Samples/JavaDependencySampleApp/ci-validate.sh create mode 100644 Sources/Java2Swift/JavaToSwift+EmitConfiguration.swift create mode 100644 Sources/Java2Swift/JavaToSwift+FetchDependencies.swift create mode 100644 Sources/Java2Swift/JavaToSwift+GenerateWrappers.swift create mode 100644 Sources/Java2Swift/String+Extensions.swift delete mode 100644 Sources/Java2SwiftLib/Configuration.swift create mode 100644 Sources/JavaKitConfigurationShared/Configuration.swift create mode 100644 Sources/JavaKitDependencyResolver/DependencyResolver.swift create mode 100644 Sources/JavaKitDependencyResolver/swift-java.config rename Sources/{JExtractSwift => JavaKitShared}/TerminalColors.swift (100%) diff --git a/.github/scripts/validate_sample.sh b/.github/scripts/validate_sample.sh new file mode 100755 index 00000000..7e0ab3d2 --- /dev/null +++ b/.github/scripts/validate_sample.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# shellcheck disable=SC2034 +declare -r GREEN='\033[0;32m' +declare -r BOLD='\033[1m' +declare -r RESET='\033[0m' + +declare -r sampleDir="$1" +declare -r CI_VALIDATE_SCRIPT='ci-validate.sh' + +echo "" +echo "" +echo "========================================================================" +printf "Validate sample '${BOLD}%s${RESET}' using: " "$sampleDir" +cd "$sampleDir" || exit +if [[ $(find . -name ${CI_VALIDATE_SCRIPT} -maxdepth 1) ]]; then + echo -e "Custom ${BOLD}${CI_VALIDATE_SCRIPT}${RESET} script..." + ./${CI_VALIDATE_SCRIPT} || exit +elif [[ $(find . -name 'build.gradle*' -maxdepth 1) ]]; then + echo -e "${BOLD}Gradle${RESET} build..." + ./gradlew build || ./gradlew build --info # re-run to get better failure output +else + echo -e "${BOLD}SwiftPM${RESET} build..." + swift build || exit +fi + +echo -e "Validated sample '${BOLD}${sampleDir}${RESET}': ${BOLD}passed${RESET}." +cd - || exit + +echo +printf "Done validating sample: %s" "${sampleDir}" +echo -e "${GREEN}done${RESET}." diff --git a/.github/scripts/validate_samples.sh b/.github/scripts/validate_samples.sh deleted file mode 100755 index 26273575..00000000 --- a/.github/scripts/validate_samples.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -# shellcheck disable=SC2034 -declare -r GREEN='\033[0;32m' -declare -r BOLD='\033[1m' -declare -r RESET='\033[0m' - -# shellcheck disable=SC2155 -declare -r SAMPLE_PACKAGES=$(find Samples -name Package.swift -maxdepth 2) -declare -r CI_VALIDATE_SCRIPT='ci-validate.sh' - -for samplePackage in ${SAMPLE_PACKAGES} ; do - sampleDir=$(dirname "$samplePackage") - - echo "" - echo "" - echo "========================================================================" - printf "Validate sample '${BOLD}%s${RESET}' using: " "$sampleDir" - cd "$sampleDir" || exit - if [[ $(find . -name ${CI_VALIDATE_SCRIPT} -maxdepth 1) ]]; then - echo -e "Custom ${BOLD}${CI_VALIDATE_SCRIPT}${RESET} script..." - ./${CI_VALIDATE_SCRIPT} || exit - elif [[ $(find . -name 'build.gradle*' -maxdepth 1) ]]; then - echo -e "${BOLD}Gradle${RESET} build..." - ./gradlew build || ./gradlew build --info # re-run to get better failure output - else - echo -e "${BOLD}SwiftPM${RESET} build..." - swift build || exit - fi - - echo -e "Validated sample '${BOLD}${sampleDir}${RESET}': ${BOLD}passed${RESET}." - cd - || exit -done - -echo -printf "Done validating samples: " -echo -e "${GREEN}done${RESET}." diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 06db1ff2..d289296d 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -82,8 +82,18 @@ jobs: - uses: actions/checkout@v4 - name: Prepare CI Environment uses: ./.github/actions/prepare_env - - name: Verify Samples (All) - run: .github/scripts/validate_samples.sh + - name: "Verify Sample: JavaDependencySampleApp" + run: .github/scripts/validate_sample.sh Samples/JavaDependencySampleApp + - name: "Verify Sample: JavaKitSampleApp" + run: .github/scripts/validate_sample.sh Samples/JavaKitSampleApp + - name: "Verify Sample: JavaProbablyPrime" + run: .github/scripts/validate_sample.sh Samples/JavaProbablyPrime + - name: "Verify Sample: JavaSieve" + run: .github/scripts/validate_sample.sh Samples/JavaSieve + - name: "Verify Sample: SwiftAndJavaJarSampleLib" + run: .github/scripts/validate_sample.sh Samples/SwiftAndJavaJarSampleLib + - name: "Verify Sample: SwiftKitSampleApp" + run: .github/scripts/validate_sample.sh Samples/SwiftKitSampleApp # TODO: Benchmark compile crashes in CI, enable when nightly toolchains in better shape. # - name: Build (Swift) Benchmarks # run: "swift package --package-path Benchmarks/ benchmark list" diff --git a/.licenseignore b/.licenseignore index ce382547..5f93c9a6 100644 --- a/.licenseignore +++ b/.licenseignore @@ -42,4 +42,5 @@ gradlew.bat **/ci-validate.sh **/DO_NOT_EDIT.txt Plugins/**/_PluginsShared -Plugins/**/0_PLEASE_SYMLINK* \ No newline at end of file +Plugins/**/0_PLEASE_SYMLINK* +Plugins/PluginsShared/JavaKitConfigurationShared \ No newline at end of file diff --git a/JavaKit/build.gradle b/JavaKit/build.gradle new file mode 100644 index 00000000..8eed1c6e --- /dev/null +++ b/JavaKit/build.gradle @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// 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("build-logic.java-library-conventions") +} + +group = "org.swift.javakit" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(22)) + } +} + +dependencies { + implementation("dev.gradleplugins:gradle-api:8.10.1") + + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +tasks.test { + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + } +} + +// Copy the gradle wrapper we're using into the resulting jar's resources. +// We'll use it to bootstrap dependencies (and gradle!) if there is none yet. +tasks.processResources { + from('gradlew') { + into 'gradle/' + } + from('gradlew.bat') { + into 'gradle/' + } + from('../gradle/wrapper/gradle-wrapper.jar') { + into 'gradle/wrapper/' + } + from('../gradle/wrapper/gradle-wrapper.properties') { + into 'gradle/wrapper/' + } +} + +//task fatJar(type: Jar) { +// archiveBaseName = 'java-kit-fat-jar' +// duplicatesStrategy = DuplicatesStrategy.EXCLUDE +// from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } +// with jar +//} + +// Task necessary to bootstrap +task printRuntimeClasspath { + def runtimeClasspath = sourceSets.main.runtimeClasspath + inputs.files(runtimeClasspath) + doLast { + println("CLASSPATH:${runtimeClasspath.asPath}") + } +} diff --git a/JavaKit/gradlew b/JavaKit/gradlew new file mode 120000 index 00000000..502f5a2d --- /dev/null +++ b/JavaKit/gradlew @@ -0,0 +1 @@ +../gradlew \ No newline at end of file diff --git a/JavaKit/gradlew.bat b/JavaKit/gradlew.bat new file mode 120000 index 00000000..28401328 --- /dev/null +++ b/JavaKit/gradlew.bat @@ -0,0 +1 @@ +../gradlew.bat \ No newline at end of file diff --git a/JavaKit/src/main/java/org/swift/javakit/annotations/UsedFromSwift.java b/JavaKit/src/main/java/org/swift/javakit/annotations/UsedFromSwift.java new file mode 100644 index 00000000..ad9e7918 --- /dev/null +++ b/JavaKit/src/main/java/org/swift/javakit/annotations/UsedFromSwift.java @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// 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 org.swift.javakit.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Since some public methods may not appear as used in Java source code, but are used by Swift, + * we can use this source annotation to mark such entry points to not accidentally remove them with + * "safe delete" refactorings in Java IDEs which would be unaware of the usages from Swift. + */ +@SuppressWarnings("unused") // used from Swift +@Retention(RetentionPolicy.SOURCE) +public @interface UsedFromSwift { +} diff --git a/JavaKit/src/main/java/org/swift/javakit/dependencies/DependencyResolver.java b/JavaKit/src/main/java/org/swift/javakit/dependencies/DependencyResolver.java new file mode 100644 index 00000000..ff4085c9 --- /dev/null +++ b/JavaKit/src/main/java/org/swift/javakit/dependencies/DependencyResolver.java @@ -0,0 +1,303 @@ +//===----------------------------------------------------------------------===// +// +// 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 org.swift.javakit.dependencies; + +import org.gradle.tooling.GradleConnector; +import org.swift.javakit.annotations.UsedFromSwift; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Fetches dependencies using the Gradle resolver and returns the resulting classpath which includes + * the fetched dependency and all of its dependencies. + */ +@UsedFromSwift +@SuppressWarnings("unused") +public class DependencyResolver { + + private static final String COMMAND_OUTPUT_LINE_PREFIX_CLASSPATH = "CLASSPATH:"; + private static final String CLASSPATH_CACHE_FILENAME = "JavaKitDependencyResolver.swift-java.classpath"; + + public static String GRADLE_API_DEPENDENCY = "dev.gradleplugins:gradle-api:8.10.1"; + public static String[] BASE_DEPENDENCIES = { + GRADLE_API_DEPENDENCY + }; + + /** + * May throw runtime exceptions including {@link org.gradle.api.internal.artifacts.ivyservice.TypedResolveException} + * if unable to resolve a dependency. + */ + @UsedFromSwift + @SuppressWarnings("unused") + public static String resolveDependenciesToClasspath(String projectBaseDirectoryString, String[] dependencies) throws IOException { + try { + simpleLog("Fetch dependencies: " + Arrays.toString(dependencies)); + simpleLog("Classpath: " + System.getProperty("java.class.path")); + var projectBasePath = new File(projectBaseDirectoryString).toPath(); + + File projectDir = Files.createTempDirectory("java-swift-dependencies").toFile(); + projectDir.mkdirs(); + + if (hasDependencyResolverDependenciesLoaded()) { + // === Resolve dependencies using Gradle API in-process + simpleLog("Gradle API runtime dependency is available, resolve dependencies..."); + return resolveDependenciesUsingAPI(projectDir, dependencies); + } + + // === Bootstrap the resolver dependencies and cache them + simpleLog("Gradle API not available on classpath, bootstrap %s dependencies: %s" + .formatted(DependencyResolver.class.getSimpleName(), Arrays.toString(BASE_DEPENDENCIES))); + String dependencyResolverDependenciesClasspath = bootstrapDependencyResolverClasspath(); + writeDependencyResolverClasspath(projectBasePath, dependencyResolverDependenciesClasspath); + + // --- Resolve dependencies using sub-process process + // TODO: it would be nice to just add the above classpath to the system classloader and here call the API + // immediately, but that's challenging and not a stable API we can rely on (hacks exist to add paths + // to system classloader but are not reliable). + printBuildFiles(projectDir, dependencies); + return resolveDependenciesWithSubprocess(projectDir); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + + + /** + * Use an external {@code gradle} invocation in order to download dependencies such that we can use `gradle-api` + * next time we want to resolve dependencies. This uses an external process and is slightly worse than using the API + * directly. + * + * @return classpath obtained for the dependencies + * @throws IOException if file IO failed during mock project creation + * @throws SwiftJavaBootstrapException if the resolve failed for some other reason + */ + private static String bootstrapDependencyResolverClasspath() throws IOException, SwiftJavaBootstrapException { + var dependencies = BASE_DEPENDENCIES; + simpleLog("Bootstrap gradle-api for DependencyResolver: " + Arrays.toString(dependencies)); + + File bootstrapDir = Files.createTempDirectory("swift-java-dependency-resolver").toFile(); + bootstrapDir.mkdirs(); + simpleLog("Bootstrap dependencies using project at: %s".formatted(bootstrapDir)); + + printBuildFiles(bootstrapDir, dependencies); + + var bootstrapClasspath = resolveDependenciesWithSubprocess(bootstrapDir); + simpleLog("Prepared dependency resolver bootstrap classpath: " + bootstrapClasspath.split(":").length + " entries"); + + return bootstrapClasspath; + + } + + private static String resolveDependenciesWithSubprocess(File gradleProjectDir) throws IOException { + if (!gradleProjectDir.isDirectory()) { + throw new IllegalArgumentException("Gradle project directory is not a directory: " + gradleProjectDir); + } + + File stdoutFile = File.createTempFile("swift-java-bootstrap", ".stdout", gradleProjectDir); + stdoutFile.deleteOnExit(); + File stderrFile = File.createTempFile("swift-java-bootstrap", ".stderr", gradleProjectDir); + stderrFile.deleteOnExit(); + + try { + ProcessBuilder gradleBuilder = new ProcessBuilder("./gradlew", ":printRuntimeClasspath"); + gradleBuilder.directory(gradleProjectDir); + gradleBuilder.redirectOutput(stdoutFile); + gradleBuilder.redirectError(stderrFile); + Process gradleProcess = gradleBuilder.start(); + gradleProcess.waitFor(10, TimeUnit.MINUTES); // TODO: must be configurable + + if (gradleProcess.exitValue() != 0) { + throw new SwiftJavaBootstrapException("Failed to resolve bootstrap dependencies, exit code: " + gradleProcess.exitValue()); + } + + Stream lines = Files.readAllLines(stdoutFile.toPath()).stream(); + var bootstrapClasspath = getClasspathFromGradleCommandOutput(lines); + return bootstrapClasspath; + } catch (Exception ex) { + simpleLog("stdoutFile = " + stdoutFile); + simpleLog("stderrFile = " + stderrFile); + + ex.printStackTrace(); + throw new SwiftJavaBootstrapException("Failed to bootstrap dependencies necessary for " + + DependencyResolver.class.getCanonicalName() + "! " + + "Make sure to invoke SwiftPM with --disable-sandbox because " + + "swift-java needs network access to fetch java dependencies.", ex); + } + } + + private static void writeDependencyResolverClasspath(Path projectBasePath, String dependencyResolverDependenciesClasspath) throws IOException { + File swiftBuildDirectory = new File(String.valueOf(projectBasePath), ".build"); + swiftBuildDirectory.mkdirs(); + + File dependencyResolverClasspathCacheFile = new File(swiftBuildDirectory, CLASSPATH_CACHE_FILENAME); + dependencyResolverClasspathCacheFile.createNewFile(); + simpleLog("Cache %s dependencies classpath at: '%s'. Subsequent dependency resolutions will use gradle-api." + .formatted(DependencyResolver.class.getSimpleName(), dependencyResolverClasspathCacheFile.toPath())); + + Files.writeString( + dependencyResolverClasspathCacheFile.toPath(), + dependencyResolverDependenciesClasspath, + StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + } + + /** + * Detect if we have the necessary dependencies loaded. + */ + @UsedFromSwift + public static boolean hasDependencyResolverDependenciesLoaded() { + return hasDependencyResolverDependenciesLoaded(DependencyResolver.class.getClassLoader()); + } + + /** + * Resolve dependencies in the passed project directory and return the resulting classpath. + * + * @return classpath which was resolved for the dependencies + */ + private static String resolveDependenciesUsingAPI(File projectDir, String[] dependencies) throws IOException { + printBuildFiles(projectDir, dependencies); + + var connection = GradleConnector.newConnector() + .forProjectDirectory(projectDir) + .connect(); + + try (connection) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PrintStream printStream = new PrintStream(outputStream); + + connection.newBuild().forTasks(":printRuntimeClasspath") + .setStandardError(new NoopOutputStream()) + .setStandardOutput(printStream) + .run(); + + var all = outputStream.toString(); + var classpathString = Arrays.stream(all.split("\n")) + .filter(s -> s.startsWith(COMMAND_OUTPUT_LINE_PREFIX_CLASSPATH)) + .map(s -> s.substring(COMMAND_OUTPUT_LINE_PREFIX_CLASSPATH.length())) + .findFirst().orElseThrow(() -> new RuntimeException("Could not find classpath output from ':printRuntimeClasspath' task.")); + + // remove output directories of the project we used for the dependency resolution + var classpath = Arrays.stream(classpathString + .split(":")) + .filter(s -> !s.startsWith(projectDir.getAbsolutePath())) + .collect(Collectors.joining(":")); + + + return classpath; + } + } + + private static String getClasspathFromGradleCommandOutput(Stream lines) { + return lines.filter(s -> s.startsWith(COMMAND_OUTPUT_LINE_PREFIX_CLASSPATH)) + .map(s -> s.substring(COMMAND_OUTPUT_LINE_PREFIX_CLASSPATH.length())) + .findFirst().orElseThrow(() -> new RuntimeException("Could not find classpath output from gradle command output task.")); + } + + + private static boolean hasDependencyResolverDependenciesLoaded(ClassLoader classLoader) { + try { + classLoader.loadClass("org.gradle.tooling.GradleConnector"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + private static void printBuildFiles(File projectDir, String[] dependencies) throws IOException { + // === build.gradle + File buildFile = new File(projectDir, "build.gradle"); + try (PrintWriter writer = new PrintWriter(buildFile)) { + writer.println("plugins { id 'java-library' }"); + writer.println("repositories { mavenCentral() }"); + + writer.println("dependencies {"); + for (String dependency : dependencies) { + writer.println("implementation(\"" + dependency + "\")"); + } + writer.println("}"); + + writer.println(""" + task printRuntimeClasspath { + def runtimeClasspath = sourceSets.main.runtimeClasspath + inputs.files(runtimeClasspath) + doLast { + println("CLASSPATH:${runtimeClasspath.asPath}") + } + } + """); + } + + // === settings.gradle + File settingsFile = new File(projectDir, "settings.gradle.kts"); + try (PrintWriter writer = new PrintWriter(settingsFile)) { + writer.println(""" + rootProject.name = "swift-java-resolve-temp-project" + """); + } + + // === gradle wrapper files, so we can even download gradle when necessary to bootstrap + File gradlew = new File(projectDir, "gradlew"); + writeResourceToFile("/gradle/gradlew", gradlew); + gradlew.setExecutable(true); + + File gradlewBat = new File(projectDir, "gradlew.bat"); + writeResourceToFile("/gradle/gradlew.bat", gradlewBat); + gradlew.setExecutable(true); + + File gradleDir = new File(projectDir, "gradle"); + File gradleWrapperDir = new File(gradleDir, "wrapper"); + gradleWrapperDir.mkdirs(); + + File gradleWrapperJar = new File(gradleWrapperDir, "gradle-wrapper.jar"); + writeResourceToFile("/gradle/wrapper/gradle-wrapper.jar", gradleWrapperJar); + File gradleWrapperProps = new File(gradleWrapperDir, "gradle-wrapper.properties"); + writeResourceToFile("/gradle/wrapper/gradle-wrapper.properties", gradleWrapperProps); + } + + private static void writeResourceToFile(String resource, File target) throws IOException { + try (PrintWriter writer = new PrintWriter(target)) { + try (InputStream inputStream = DependencyResolver.class.getResourceAsStream(resource)) { + if (inputStream == null) { + throw new FileNotFoundException("Not found: gradlew wrapper in resources!"); + } + try (var os = new BufferedOutputStream(new FileOutputStream(target))) { + byte[] buffer = new byte[8192]; // Buffer size of 8 KB + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + os.write(buffer, 0, bytesRead); + } + } + } + } + } + + private static void simpleLog(String message) { + System.err.println("[info][swift-java/" + DependencyResolver.class.getSimpleName() + "] " + message); + } + + private static class NoopOutputStream extends OutputStream { + @Override + public void write(int b) throws IOException { + // ignore + } + } +} diff --git a/JavaKit/src/main/java/org/swift/javakit/dependencies/SwiftJavaBootstrapException.java b/JavaKit/src/main/java/org/swift/javakit/dependencies/SwiftJavaBootstrapException.java new file mode 100644 index 00000000..3c8d475a --- /dev/null +++ b/JavaKit/src/main/java/org/swift/javakit/dependencies/SwiftJavaBootstrapException.java @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// 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 org.swift.javakit.dependencies; + +public class SwiftJavaBootstrapException extends RuntimeException { + public SwiftJavaBootstrapException(String message) { + super(message); + } + + public SwiftJavaBootstrapException(String message, Exception ex) { + super(message, ex); + } +} diff --git a/JavaKit/src/main/java/org/swift/javakit/dependencies/SystemClassLoaderUnsafe.java b/JavaKit/src/main/java/org/swift/javakit/dependencies/SystemClassLoaderUnsafe.java new file mode 100644 index 00000000..d8d4f64a --- /dev/null +++ b/JavaKit/src/main/java/org/swift/javakit/dependencies/SystemClassLoaderUnsafe.java @@ -0,0 +1,46 @@ +//===----------------------------------------------------------------------===// +// +// 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 org.swift.javakit.dependencies; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; + +public final class SystemClassLoaderUnsafe { + + private SystemClassLoaderUnsafe() {} + + /** + * Use internal methods to add a path to the system classloader. + * If this ever starts throwing in new JDK versions, we may need to abandon this technique. + * + * @param path path to add to the current system classloader. + */ + public static void addPath(String path) { + try { + var url = new File(path).toURI().toURL(); + var urlClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader(); + var method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + method.setAccessible(true); + method.invoke(urlClassLoader, url); + } catch (Throwable ex) { + throw new RuntimeException("Unable to add path to system class loader! " + + "This is not supported API and may indeed start failing in the future. " + + "If/when this happens, we have to change the bootstrap logic to instead " + + "create a new JVM with the new bootstrap classpath, " + + "rather than add paths to the existing one.", ex); + } + } +} diff --git a/Package.swift b/Package.swift index 119aca6e..f6645115 100644 --- a/Package.swift +++ b/Package.swift @@ -40,7 +40,7 @@ let javaIncludePath = "\(javaHome)/include" #endif let package = Package( - name: "JavaKit", + name: "SwiftJava", platforms: [ .macOS(.v10_15) ], @@ -171,9 +171,29 @@ let package = Package( .swiftLanguageMode(.v5) ] ), + + .target( + name: "JavaKitDependencyResolver", + dependencies: [ + "JavaKit", + ], + exclude: [ + "swift-java.config", + ], + swiftSettings: [ + .swiftLanguageMode(.v5), + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]), + ] + ), + .target( name: "JavaKit", - dependencies: ["JavaRuntime", "JavaKitMacros", "JavaTypes"], + dependencies: [ + "JavaRuntime", + "JavaKitMacros", + "JavaTypes", + "JavaKitConfigurationShared", // for Configuration reading at runtime + ], exclude: ["swift-java.config"], swiftSettings: [ .swiftLanguageMode(.v5), @@ -183,10 +203,8 @@ let package = Package( .unsafeFlags( [ "-L\(javaHome)/lib/server", - "-Xlinker", - "-rpath", - "-Xlinker", - "\(javaHome)/lib/server", + "-Xlinker", "-rpath", + "-Xlinker", "\(javaHome)/lib/server", ], .when(platforms: [.linux, .macOS]) ), @@ -282,6 +300,14 @@ let package = Package( ] ), + .target( + name: "JavaKitConfigurationShared" + ), + + .target( + name: "JavaKitShared" + ), + .target( name: "Java2SwiftLib", dependencies: [ @@ -293,6 +319,9 @@ let package = Package( "JavaKitReflection", "JavaKitNetwork", "JavaTypes", + "JavaKitShared", + "JavaKitConfigurationShared", + "JavaKitDependencyResolver", ], swiftSettings: [ .swiftLanguageMode(.v5), @@ -312,6 +341,8 @@ let package = Package( "JavaKitJar", "JavaKitNetwork", "Java2SwiftLib", + "JavaKitDependencyResolver", + "JavaKitShared", ], swiftSettings: [ @@ -329,6 +360,7 @@ let package = Package( .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), .product(name: "ArgumentParser", package: "swift-argument-parser"), "JavaTypes", + "JavaKitShared", ], swiftSettings: [ .swiftLanguageMode(.v5), diff --git a/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift b/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift index 8c7045e0..d932ee97 100644 --- a/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift +++ b/Plugins/Java2SwiftPlugin/Java2SwiftPlugin.swift @@ -20,12 +20,16 @@ fileprivate let SwiftJavaConfigFileName = "swift-java.config" @main struct Java2SwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { - var pluginName: String = "swift-java-javac" + var pluginName: String = "swift-java" var verbose: Bool = getEnvironmentBool("SWIFT_JAVA_VERBOSE") func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { + log("Create build commands for target '\(target.name)'") guard let sourceModule = target.sourceModule else { return [] } + let executable = try context.tool(named: "Java2Swift").url + var commands: [Command] = [] + // Note: Target doesn't have a directoryURL counterpart to directory, // so we cannot eliminate this deprecation warning. let sourceDir = target.directory.string @@ -34,13 +38,17 @@ struct Java2SwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { // which we are generating Swift wrappers for Java classes. let configFile = URL(filePath: sourceDir) .appending(path: SwiftJavaConfigFileName) - let configData = try Data(contentsOf: configFile) - let config = try JSONDecoder().decode(Configuration.self, from: configData) + let config = try readConfiguration(sourceDir: sourceDir) + + log("Config on path: \(configFile.path(percentEncoded: false))") + log("Config was: \(config)") + var javaDependencies = config.dependencies ?? [] /// Find the manifest files from other Java2Swift executions in any targets /// this target depends on. var dependentConfigFiles: [(String, URL)] = [] func searchForConfigFiles(in target: any Target) { + // log("Search for config files in target: \(target.name)") let dependencyURL = URL(filePath: target.directory.string) // Look for a config file within this target. @@ -58,10 +66,13 @@ struct Java2SwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { for dependency in target.dependencies { switch dependency { case .target(let target): + // log("Dependency target: \(target.name)") searchForConfigFiles(in: target) case .product(let product): + // log("Dependency product: \(product.name)") for target in product.targets { + // log("Dependency product: \(product.name), target: \(target.name)") searchForConfigFiles(in: target) } @@ -72,16 +83,14 @@ struct Java2SwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { // Process indirect target dependencies. for dependency in target.recursiveTargetDependencies { + // log("Recursive dependency target: \(dependency.name)") searchForConfigFiles(in: dependency) } - let outputDirectory = context.pluginWorkDirectoryURL - .appending(path: "generated") - - var arguments: [String] = [ - "--module-name", sourceModule.name, - "--output-directory", outputDirectory.path(percentEncoded: false), - ] + var arguments: [String] = [] + arguments += argumentsModuleName(sourceModule: sourceModule) + arguments += argumentsOutputDirectory(context: context) + arguments += dependentConfigFiles.flatMap { moduleAndConfigFile in let (moduleName, configFile) = moduleAndConfigFile return [ @@ -91,17 +100,25 @@ struct Java2SwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { } arguments.append(configFile.path(percentEncoded: false)) - guard let classes = config.classes else { - log("Config at \(configFile) did not have 'classes' configured, skipping java2swift step.") - return [] - } - - /// Determine the set of Swift files that will be emitted by the Java2Swift - /// tool. +// guard let classes = config.classes else { +// log("Config at \(configFile) did not have 'classes' configured, skipping java2swift step.") +// return [] +// } + let classes = config.classes ?? [:] + print("Classes to wrap: \(classes.map(\.key))") + + /// Determine the set of Swift files that will be emitted by the Java2Swift tool. + // TODO: this is not precise and won't work with more advanced Java files, e.g. lambdas etc. + let outputDirectoryGenerated = self.outputDirectory(context: context, generated: true) let outputSwiftFiles = classes.map { (javaClassName, swiftName) in let swiftNestedName = swiftName.replacingOccurrences(of: ".", with: "+") - return outputDirectory.appending(path: "\(swiftNestedName).swift") + return outputDirectoryGenerated.appending(path: "\(swiftNestedName).swift") } + + arguments += [ + "--cache-directory", + context.pluginWorkDirectoryURL.path(percentEncoded: false) + ] // Find the Java .class files generated from prior plugins. let compiledClassFiles = sourceModule.pluginGeneratedResources.filter { url in @@ -115,7 +132,7 @@ struct Java2SwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { while classpath.lastPathComponent != "Java" { classpath.deleteLastPathComponent() } - arguments += [ "--classpath", classpath.path() ] + arguments += ["--classpath", classpath.path()] // For each of the class files, note that it can have Swift-native // implementations. We figure this out based on the path. @@ -131,23 +148,81 @@ struct Java2SwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { let className = classNameComponents .reversed() .joined(separator: ".") - arguments += [ "--swift-native-implementation", className] + arguments += ["--swift-native-implementation", className] } } - guard let classes = config.classes else { - log("Skipping java2swift step: Missing 'classes' key in swift-java.config at '\(configFile.path)'") - return [] + var fetchDependenciesOutputFiles: [URL] = [] + if let dependencies = config.dependencies, !dependencies.isEmpty { + let displayName = "Fetch (Java) dependencies for Swift target \(sourceModule.name)" + log("Prepared: \(displayName)") + + fetchDependenciesOutputFiles += [ + outputFilePath(context: context, generated: false, filename: "\(sourceModule.name).swift-java.classpath") + ] + + commands += [ + .buildCommand( + displayName: displayName, + executable: executable, + arguments: [ + "--fetch", configFile.path(percentEncoded: false), + "--module-name", sourceModule.name, + "--output-directory", outputDirectory(context: context, generated: false).path(percentEncoded: false) + ], + environment: [:], + inputFiles: [configFile], + outputFiles: fetchDependenciesOutputFiles + ) + ] + } else { + log("No dependencies to fetch for target \(sourceModule.name)") } + + if !outputSwiftFiles.isEmpty { + commands += [ + .buildCommand( + displayName: "Wrapping \(classes.count) Java classes in Swift target '\(sourceModule.name)'", + executable: executable, + arguments: arguments, + inputFiles: compiledClassFiles + fetchDependenciesOutputFiles + [ + configFile + ], + outputFiles: outputSwiftFiles + ) + ] + } else { + log("No Swift output files, skip wrapping") + } + + return commands + } +} +extension Java2SwiftBuildToolPlugin { + func argumentsModuleName(sourceModule: Target) -> [String] { return [ - .buildCommand( - displayName: "Wrapping \(classes.count) Java classes target \(sourceModule.name) in Swift", - executable: try context.tool(named: "Java2Swift").url, - arguments: arguments, - inputFiles: [ configFile ] + compiledClassFiles, - outputFiles: outputSwiftFiles - ) + "--module-name", sourceModule.name ] } + + func argumentsOutputDirectory(context: PluginContext, generated: Bool = true) -> [String] { + return [ + "--output-directory", + outputDirectory(context: context, generated: generated).path(percentEncoded: false) + ] + } + + func outputDirectory(context: PluginContext, generated: Bool = true) -> URL { + let dir = context.pluginWorkDirectoryURL + if generated { + return dir.appending(path: "generated") + } else { + return dir + } + } + + func outputFilePath(context: PluginContext, generated: Bool, filename: String) -> URL { + outputDirectory(context: context, generated: generated).appending(path: filename) + } } diff --git a/Plugins/PluginsShared/Configuration.swift b/Plugins/PluginsShared/Configuration.swift deleted file mode 100644 index 769eef70..00000000 --- a/Plugins/PluginsShared/Configuration.swift +++ /dev/null @@ -1,81 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 - -typealias JavaVersion = Int - -/// Configuration for the SwiftJava plugins, provided on a per-target basis. -struct Configuration: Codable { - // ==== swift 2 java --------------------------------------------------------- - - var javaPackage: String? - - // ==== java 2 swift --------------------------------------------------------- - - /// The Java class path that should be passed along to the Java2Swift tool. - var classPath: String? = nil - - /// The Java classes that should be translated to Swift. The keys are - /// canonical Java class names (e.g., java.util.Vector) and the values are - /// the corresponding Swift names (e.g., JavaVector). - var classes: [String: String]? = [:] - - // Compile for the specified Java SE release. - var sourceCompatibility: JavaVersion? - - // Generate class files suitable for the specified Java SE release. - var targetCompatibility: JavaVersion? -} - -func readConfiguration(sourceDir: String, file: String = #fileID, line: UInt = #line) throws -> Configuration { - let configFile = URL(filePath: sourceDir).appending(path: "swift-java.config") - do { - let configData = try Data(contentsOf: configFile) - return try JSONDecoder().decode(Configuration.self, from: configData) - } catch { - throw ConfigurationError(message: "Failed to parse SwiftJava configuration at '\(configFile)!'", error: error, - file: file, line: line) - } -} - -extension Configuration { - var compilerVersionArgs: [String] { - var compilerVersionArgs = [String]() - - if let sourceCompatibility { - compilerVersionArgs += ["--source", String(sourceCompatibility)] - } - if let targetCompatibility { - compilerVersionArgs += ["--target", String(targetCompatibility)] - } - - return compilerVersionArgs - } -} - -struct ConfigurationError: Error { - let message: String - let error: any Error - - let file: String - let line: UInt - - init(message: String, error: any Error, file: String = #fileID, line: UInt = #line) { - self.message = message - self.error = error - self.file = file - self.line = line - } -} diff --git a/Plugins/PluginsShared/JavaKitConfigurationShared b/Plugins/PluginsShared/JavaKitConfigurationShared new file mode 120000 index 00000000..d5c765df --- /dev/null +++ b/Plugins/PluginsShared/JavaKitConfigurationShared @@ -0,0 +1 @@ +../../Sources/JavaKitConfigurationShared \ No newline at end of file diff --git a/Plugins/PluginsShared/PluginUtils.swift b/Plugins/PluginsShared/PluginUtils.swift index bf4d0f0a..6932253a 100644 --- a/Plugins/PluginsShared/PluginUtils.swift +++ b/Plugins/PluginsShared/PluginUtils.swift @@ -71,4 +71,9 @@ extension PluginContext { self.pluginWorkDirectoryURL .appending(path: "Sources") } + + func cachedClasspathFile(moduleName: String) -> URL { + self.pluginWorkDirectoryURL + .appending(path: "\(moduleName)", directoryHint: .notDirectory) + } } diff --git a/Samples/JavaDependencySampleApp/Package.swift b/Samples/JavaDependencySampleApp/Package.swift new file mode 100644 index 00000000..3d210e36 --- /dev/null +++ b/Samples/JavaDependencySampleApp/Package.swift @@ -0,0 +1,101 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import CompilerPluginSupport +import PackageDescription + +import class Foundation.FileManager +import class Foundation.ProcessInfo + +// Note: the JAVA_HOME environment variable must be set to point to where +// Java is installed, e.g., +// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home. +func findJavaHome() -> String { + if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] { + return home + } + + // This is a workaround for envs (some IDEs) which have trouble with + // picking up env variables during the build process + let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home" + if let home = try? String(contentsOfFile: path, encoding: .utf8) { + if let lastChar = home.last, lastChar.isNewline { + return String(home.dropLast()) + } + + return home + } + + fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.") +} +let javaHome = findJavaHome() + +let javaIncludePath = "\(javaHome)/include" +#if os(Linux) + let javaPlatformIncludePath = "\(javaIncludePath)/linux" +#elseif os(macOS) + let javaPlatformIncludePath = "\(javaIncludePath)/darwin" +#else + // TODO: Handle windows as well + #error("Currently only macOS and Linux platforms are supported, this may change in the future.") +#endif + +let package = Package( + name: "JavaDependencySampleApp", + platforms: [ + .macOS(.v13), + .iOS(.v13), + .tvOS(.v13), + .watchOS(.v6), + .macCatalyst(.v13), + ], + + products: [ + .executable( + name: "JavaDependencySample", + targets: ["JavaDependencySample"] + ), + ], + + dependencies: [ + .package(name: "swift-java", path: "../../") + ], + + targets: [ + .executableTarget( + name: "JavaDependencySample", + dependencies: [ + .product(name: "JavaKit", package: "swift-java"), + .product(name: "JavaRuntime", package: "swift-java"), + .product(name: "JavaKitFunction", package: "swift-java"), + "JavaCommonsCSV" + ], + exclude: ["swift-java.config"], + swiftSettings: [ + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]), + .swiftLanguageMode(.v5), + ], + plugins: [ + .plugin(name: "Java2SwiftPlugin", package: "swift-java"), + ] + ), + + .target( + name: "JavaCommonsCSV", + dependencies: [ + .product(name: "JavaKit", package: "swift-java"), + .product(name: "JavaKitFunction", package: "swift-java"), + .product(name: "JavaKitCollection", package: "swift-java"), + ], + exclude: ["swift-java.config"], + swiftSettings: [ + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]), + .swiftLanguageMode(.v5), + ], + plugins: [ + .plugin(name: "Java2SwiftPlugin", package: "swift-java"), + ] + ), + + ] +) diff --git a/Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/dummy.swift b/Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/dummy.swift new file mode 100644 index 00000000..76f848f9 --- /dev/null +++ b/Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/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/JavaCommonsCSV/swift-java.config b/Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/swift-java.config new file mode 100644 index 00000000..3ab83f79 --- /dev/null +++ b/Samples/JavaDependencySampleApp/Sources/JavaCommonsCSV/swift-java.config @@ -0,0 +1,9 @@ +{ + "classes" : { + "org.apache.commons.io.FilenameUtils" : "FilenameUtils", + "org.apache.commons.io.IOCase" : "IOCase" + }, + "dependencies" : [ + "org.apache.commons:commons-csv:1.12.0" + ] +} diff --git a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift new file mode 100644 index 00000000..86770ee8 --- /dev/null +++ b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/main.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// 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 JavaKit +import JavaKitFunction +import JavaKitConfigurationShared +import Foundation + +// Import the commons-csv library wrapper: +import JavaCommonsCSV + +print("") +print("") +print("-----------------------------------------------------------------------") +print("Start Sample app...") + +// Make sure we have the classpath loaded +// TODO: this is more complex than that, need to account for dependencies of our module +let currentDir = FileManager.default.currentDirectoryPath +let swiftJavaClasspath = findSwiftJavaClasspaths() + +// 1) Start a JVM with appropriate classpath +let jvm = try JavaVirtualMachine.shared(classpath: swiftJavaClasspath) + +// 2) Get the FilenameUtils Java class so we can call the static methods on it +let FilenameUtilsClass = try JavaClass() + +// Some silly sample path we want to work with: +let path = "/example/path/executable.exe" +print("Path = \(path)") + +let ext = try! FilenameUtilsClass.getExtension(path) +print("org.apache.commons.io.FilenameUtils.getExtension = \(ext)") +precondition(ext == "exe") + +print("Done.") diff --git a/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/swift-java.config b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/swift-java.config new file mode 100644 index 00000000..7a73a41b --- /dev/null +++ b/Samples/JavaDependencySampleApp/Sources/JavaDependencySample/swift-java.config @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/Samples/JavaDependencySampleApp/ci-validate.sh b/Samples/JavaDependencySampleApp/ci-validate.sh new file mode 100755 index 00000000..86f83978 --- /dev/null +++ b/Samples/JavaDependencySampleApp/ci-validate.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +set -e +set -x + +cd ../../JavaKit +./gradlew build + +cd - +swift run --disable-sandbox diff --git a/Samples/JavaKitSampleApp/ci-validate.sh b/Samples/JavaKitSampleApp/ci-validate.sh index eff61551..f453a00b 100755 --- a/Samples/JavaKitSampleApp/ci-validate.sh +++ b/Samples/JavaKitSampleApp/ci-validate.sh @@ -1,5 +1,8 @@ #!/bin/sh +set -e +set -x + swift build "$JAVA_HOME/bin/java" \ -cp .build/plugins/outputs/javakitsampleapp/JavaKitExample/destination/JavaCompilerPlugin/Java \ diff --git a/Samples/JavaProbablyPrime/ci-validate.sh b/Samples/JavaProbablyPrime/ci-validate.sh index 948a390c..0bdd86d1 100755 --- a/Samples/JavaProbablyPrime/ci-validate.sh +++ b/Samples/JavaProbablyPrime/ci-validate.sh @@ -1,3 +1,6 @@ #!/bin/sh +set -e +set -x + swift run JavaProbablyPrime 1337 \ No newline at end of file diff --git a/Samples/JavaSieve/.gitignore b/Samples/JavaSieve/.gitignore index 0023a534..6e1b4ce3 100644 --- a/Samples/JavaSieve/.gitignore +++ b/Samples/JavaSieve/.gitignore @@ -6,3 +6,4 @@ DerivedData/ .swiftpm/configuration/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc +quadratic-sieve-Java \ No newline at end of file diff --git a/Samples/JavaSieve/README.md b/Samples/JavaSieve/README.md index b8dc3c9a..09fbde83 100644 --- a/Samples/JavaSieve/README.md +++ b/Samples/JavaSieve/README.md @@ -12,10 +12,9 @@ This example wraps an [open-source Java library](https://github.com/gazman-sdk/q git clone https://github.com/gazman-sdk/quadratic-sieve-Java cd quadratic-sieve-Java sh ./gradlew jar +cd .. ``` -Then, copy the resulting Jar file (`./build/libs/QuadraticSieve-1.0.jar`) into the `Samples/JavaSieve` directory. - Now we're ready to build and run the Swift program from `Samples/JavaSieve`: ``` diff --git a/Samples/JavaSieve/Sources/JavaSieve/main.swift b/Samples/JavaSieve/Sources/JavaSieve/main.swift index 6d262b9f..e2047713 100644 --- a/Samples/JavaSieve/Sources/JavaSieve/main.swift +++ b/Samples/JavaSieve/Sources/JavaSieve/main.swift @@ -15,7 +15,11 @@ import JavaKit import JavaMath -let jvm = try JavaVirtualMachine.shared(classpath: ["QuadraticSieve-1.0.jar"]) +let jvm = try JavaVirtualMachine.shared(classpath: [ + "quadratic-sieve-Java/build/libs/QuadraticSieve-1.0.jar", + ".", +]) + do { let sieveClass = try JavaClass(environment: jvm.environment()) for prime in sieveClass.findPrimes(100)! { diff --git a/Samples/JavaSieve/Sources/JavaSieve/swift-java.config b/Samples/JavaSieve/Sources/JavaSieve/swift-java.config index 7ef33db9..7e055d1c 100644 --- a/Samples/JavaSieve/Sources/JavaSieve/swift-java.config +++ b/Samples/JavaSieve/Sources/JavaSieve/swift-java.config @@ -1,5 +1,5 @@ { - "classpath" : "QuadraticSieve-1.0.jar", + "classpath" : ".:quadratic-sieve-Java/build/libs/QuadraticSieve-1.0.jar", "classes" : { "com.gazman.quadratic_sieve.QuadraticSieve" : "QuadraticSieve", "com.gazman.quadratic_sieve.core.BaseFact" : "BaseFact", diff --git a/Samples/JavaSieve/ci-validate.sh b/Samples/JavaSieve/ci-validate.sh index 3eebd3ca..730a8ba8 100755 --- a/Samples/JavaSieve/ci-validate.sh +++ b/Samples/JavaSieve/ci-validate.sh @@ -1,3 +1,3 @@ #!/bin/bash -echo "Not validated in CI..." +echo "This sample is not verified in CI because old Gradle in the project making it hard to use its ./gradlew" \ No newline at end of file diff --git a/Sources/JExtractSwift/Swift2Java.swift b/Sources/JExtractSwift/Swift2Java.swift index 2167c391..a46d1ab4 100644 --- a/Sources/JExtractSwift/Swift2Java.swift +++ b/Sources/JExtractSwift/Swift2Java.swift @@ -16,6 +16,7 @@ import ArgumentParser import Foundation import SwiftSyntax import SwiftSyntaxBuilder +import JavaKitShared /// Command-line utility, similar to `jextract` to export Swift types to Java. public struct SwiftToJava: ParsableCommand { diff --git a/Sources/Java2Swift/JavaToSwift+EmitConfiguration.swift b/Sources/Java2Swift/JavaToSwift+EmitConfiguration.swift new file mode 100644 index 00000000..fd567234 --- /dev/null +++ b/Sources/Java2Swift/JavaToSwift+EmitConfiguration.swift @@ -0,0 +1,122 @@ +//===----------------------------------------------------------------------===// +// +// 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 ArgumentParser +import Java2SwiftLib +import JavaKit +import JavaKitJar +import Java2SwiftLib +import JavaKitDependencyResolver +import JavaKitConfigurationShared + +extension JavaToSwift { + + // TODO: make this perhaps "emit type mappings" + mutating func emitConfiguration( + classpath: String, + environment: JNIEnvironment + ) throws { + print("[java-swift] Generate Java->Swift type mappings. Active filter: \(javaPackageFilter)") + print("[java-swift] Classpath: \(classpath)") + + if classpath.isEmpty { + print("[warning][java-swift] Classpath is empty!") + } + + // Get a fresh or existing configuration we'll amend + var (amendExistingConfig, configuration) = try getBaseConfigurationForWrite() + if amendExistingConfig { + print("[swift-java] Amend existing swift-java.config file...") + } + configuration.classpath = classpath // TODO: is this correct? + + // Import types from all the classpath entries; + // Note that we use the package level filtering, so users have some control over what gets imported. + for entry in classpath.split(separator: ":").map(String.init) { + print("[debug][swift-java] Importing classpath entry: \(entry)") + if entry.hasSuffix(".jar") { + let jarFile = try JarFile(entry, false, environment: environment) + try addJavaToSwiftMappings( + to: &configuration, + forJar: jarFile, + environment: environment + ) + } else if FileManager.default.fileExists(atPath: entry) { + print("[warning][swift-java] Currently unable handle directory classpath entries for config generation! Skipping: \(entry)") + } else { + print("[warning][swift-java] Classpath entry does not exist, skipping: \(entry)") + } + } + + // Encode the configuration. + let contents = try configuration.renderJSON() + + // Write the file. + try writeContents( + contents, + to: "swift-java.config", + description: "swift-java configuration file" + ) + } + + mutating func addJavaToSwiftMappings( + to configuration: inout Configuration, + forJar jarFile: JarFile, + environment: JNIEnvironment + ) throws { + for entry in jarFile.entries()! { + // We only look at class files in the Jar file. + guard entry.getName().hasSuffix(".class") else { + continue + } + + // Skip some "common" files we know that would be duplicated in every jar + guard !entry.getName().hasPrefix("META-INF") else { + continue + } + guard !entry.getName().hasSuffix("package-info") else { + continue + } + guard !entry.getName().hasSuffix("package-info.class") else { + continue + } + + // If this is a local class, it cannot be mapped into Swift. + if entry.getName().isLocalJavaClass { + continue + } + + let javaCanonicalName = String(entry.getName().replacing("/", with: ".") + .dropLast(".class".count)) + + if let javaPackageFilter { + if !javaCanonicalName.hasPrefix(javaPackageFilter) { + // Skip classes which don't match our expected prefix + continue + } + } + + if configuration.classes?[javaCanonicalName] != nil { + // We never overwrite an existing class mapping configuration. + // E.g. the user may have configured a custom name for a type. + continue + } + + configuration.classes?[javaCanonicalName] = + javaCanonicalName.defaultSwiftNameForJavaClass + } + } + +} \ No newline at end of file diff --git a/Sources/Java2Swift/JavaToSwift+FetchDependencies.swift b/Sources/Java2Swift/JavaToSwift+FetchDependencies.swift new file mode 100644 index 00000000..391abb27 --- /dev/null +++ b/Sources/Java2Swift/JavaToSwift+FetchDependencies.swift @@ -0,0 +1,122 @@ +//===----------------------------------------------------------------------===// +// +// 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 Java2SwiftLib +import JavaKit +import Foundation +import JavaKitJar +import Java2SwiftLib +import JavaKitDependencyResolver +import JavaKitConfigurationShared +import JavaKitShared + +extension JavaToSwift { + + /// Must be the same as `DependencyResolver#CLASSPATH_CACHE_FILENAME` on the java side. + var JavaKitDependencyResolverClasspathCacheFilename: String { + "JavaKitDependencyResolver.swift-java.classpath" + } + var JavaKitDependencyResolverClasspathCacheFilePath: String { + ".build/\(JavaKitDependencyResolverClasspathCacheFilename)" + } + + func fetchDependenciesCachedClasspath() -> [String]? { + let cachedClasspathURL = URL( + fileURLWithPath: FileManager.default.currentDirectoryPath + "/" + JavaKitDependencyResolverClasspathCacheFilePath) + + guard FileManager.default.fileExists(atPath: cachedClasspathURL.path) else { + return [] + } + + guard let javaKitDependencyResolverCachedClasspath = try? String(contentsOf: cachedClasspathURL) else { + return [] + } + + print("[debug][swift-java] Cached dependency resolver classpath: \(javaKitDependencyResolverCachedClasspath)") + return javaKitDependencyResolverCachedClasspath.split(separator: ":").map(String.init) + } + + func fetchDependencies(moduleName: String, + dependencies: [JavaDependencyDescriptor], + baseClasspath: [String], + environment: JNIEnvironment) throws -> ResolvedDependencyClasspath { + let deps = dependencies.map { $0.descriptionGradleStyle } + print("[debug][swift-java] Resolve and fetch dependencies for: \(deps)") + let resolverClass = try JavaClass(environment: environment) + + let fullClasspath = try resolverClass.resolveDependenciesToClasspath( + projectBaseDirectory: URL(fileURLWithPath: ".").path, + dependencies: deps) + .split(separator: ":") + + let classpathEntries = fullClasspath.filter { + $0.hasSuffix(".jar") + } + let classpath = classpathEntries.joined(separator: ":") + + print("[info][swift-java] Resolved classpath for \(deps.count) dependencies of '\(moduleName)', classpath entries: \(classpathEntries.count), ", terminator: "") + print("done.".green) + + return ResolvedDependencyClasspath(for: dependencies, classpath: classpath) + } +} + +extension JavaToSwift { + mutating func writeFetchedDependenciesClasspath( + moduleName: String, + cacheDir: 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 + + // Write the file + try writeContents( + contents, + outputDirectoryOverride: URL(fileURLWithPath: cacheDir), + to: "\(moduleName).swift-java.classpath", + description: "swift-java.classpath file for module \(moduleName)" + ) + } + + public func artifactIDAsModuleID(_ artifactID: String) -> String { + let components = artifactID.split(whereSeparator: { $0 == "-" }) + let camelCased = components.map { $0.capitalized }.joined() + return camelCased + } +} + +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/Java2Swift/JavaToSwift+GenerateWrappers.swift b/Sources/Java2Swift/JavaToSwift+GenerateWrappers.swift new file mode 100644 index 00000000..99f83b4e --- /dev/null +++ b/Sources/Java2Swift/JavaToSwift+GenerateWrappers.swift @@ -0,0 +1,145 @@ +//===----------------------------------------------------------------------===// +// +// 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 ArgumentParser +import Java2SwiftLib +import JavaKit +import JavaKitJar +import Java2SwiftLib +import JavaKitDependencyResolver +import JavaKitConfigurationShared + +extension JavaToSwift { + mutating func generateWrappers( + config: Configuration, + classpath: String, + dependentConfigs: [(String, Configuration)], + environment: JNIEnvironment + ) throws { + guard let moduleName else { + fatalError("--module-name must be set in 'generate wrappers' mode!") + } + let translator = JavaTranslator( + swiftModuleName: moduleName, + environment: environment, + translateAsClass: true + ) + + // Keep track of all of the Java classes that will have + // Swift-native implementations. + translator.swiftNativeImplementations = Set(swiftNativeImplementation) + + // Note all of the dependent configurations. + for (swiftModuleName, dependentConfig) in dependentConfigs { + translator.addConfiguration( + dependentConfig, + forSwiftModule: swiftModuleName + ) + } + + // Add the configuration for this module. + translator.addConfiguration(config, forSwiftModule: moduleName) + + // Load all of the explicitly-requested classes. + let classLoader = try JavaClass(environment: environment) + .getSystemClassLoader()! + var javaClasses: [JavaClass] = [] + for (javaClassName, _) in config.classes ?? [:] { + guard let javaClass = try classLoader.loadClass(javaClassName) else { + print("warning: could not find Java class '\(javaClassName)'") + continue + } + + // Add this class to the list of classes we'll translate. + javaClasses.append(javaClass) + } + + // Find all of the nested classes for each class, adding them to the list + // of classes to be translated if they were already specified. + var allClassesToVisit = javaClasses + var currentClassIndex: Int = 0 + while currentClassIndex < allClassesToVisit.count { + defer { + currentClassIndex += 1 + } + + // The current class we're in. + let currentClass = allClassesToVisit[currentClassIndex] + guard let currentSwiftName = translator.translatedClasses[currentClass.getName()]?.swiftType else { + continue + } + + // Find all of the nested classes that weren't explicitly translated + // already. + let nestedClasses: [JavaClass] = currentClass.getClasses().compactMap { nestedClass in + guard let nestedClass else { return nil } + + // If this is a local class, we're done. + let javaClassName = nestedClass.getName() + if javaClassName.isLocalJavaClass { + return nil + } + + // If this class has been explicitly mentioned, we're done. + if translator.translatedClasses[javaClassName] != nil { + return nil + } + + // Record this as a translated class. + let swiftUnqualifiedName = javaClassName.javaClassNameToCanonicalName + .defaultSwiftNameForJavaClass + + + let swiftName = "\(currentSwiftName).\(swiftUnqualifiedName)" + translator.translatedClasses[javaClassName] = (swiftName, nil) + return nestedClass + } + + // If there were no new nested classes, there's nothing to do. + if nestedClasses.isEmpty { + continue + } + + // Record all of the nested classes that we will visit. + translator.nestedClasses[currentClass.getName()] = nestedClasses + allClassesToVisit.append(contentsOf: nestedClasses) + } + + // Validate configurations before writing any files + try translator.validateClassConfiguration() + + // Translate all of the Java classes into Swift classes. + for javaClass in javaClasses { + translator.startNewFile() + let swiftClassDecls = try translator.translateClass(javaClass) + let importDecls = translator.getImportDecls() + + let swiftFileText = """ + // Auto-generated by Java-to-Swift wrapper generator. + \(importDecls.map { $0.description }.joined()) + \(swiftClassDecls.map { $0.description }.joined(separator: "\n")) + + """ + + let swiftFileName = try! translator.getSwiftTypeName(javaClass, preferValueTypes: false) + .swiftName.replacing(".", with: "+") + ".swift" + try writeContents( + swiftFileText, + to: swiftFileName, + description: "Java class '\(javaClass.getName())' translation" + ) + } + } +} \ No newline at end of file diff --git a/Sources/Java2Swift/JavaToSwift.swift b/Sources/Java2Swift/JavaToSwift.swift index 35bffc74..0d196933 100644 --- a/Sources/Java2Swift/JavaToSwift.swift +++ b/Sources/Java2Swift/JavaToSwift.swift @@ -21,6 +21,8 @@ import JavaKitNetwork import JavaKitReflection import SwiftSyntax import SwiftSyntaxBuilder +import JavaKitConfigurationShared +import JavaKitShared /// Command-line utility to drive the export of Java classes into Swift types. @main @@ -28,7 +30,7 @@ struct JavaToSwift: ParsableCommand { static var _commandName: String { "Java2Swift" } @Option(help: "The name of the Swift module into which the resulting Swift types will be generated.") - var moduleName: String + var moduleName: String? @Option( help: @@ -36,12 +38,16 @@ struct JavaToSwift: ParsableCommand { ) var dependsOn: [String] = [] + // TODO: This should be a "make wrappers" option that just detects when we give it a jar @Flag( help: "Specifies that the input is a Jar file whose public classes will be loaded. The output of Java2Swift will be a configuration file (Java2Swift.config) that can be used as input to a subsequent Java2Swift invocation to generate wrappers for those public classes." ) var jar: Bool = false + @Flag(help: "Fetch dependencies from given target (containing swift-java configuration) or dependency string") + var fetch: Bool = false + @Option( name: [.customLong("cp"), .customLong("classpath")], help: "Class search path of directories and zip/jar files from which Java classes can be loaded." @@ -56,6 +62,30 @@ struct JavaToSwift: ParsableCommand { @Option(name: .shortAndLong, help: "The directory in which to output the generated Swift files or the Java2Swift configuration file.") var outputDirectory: String? = nil + + @Option(name: .shortAndLong, help: "Directory where to write cached values (e.g. swift-java.classpath files)") + var cacheDirectory: String? = nil + + var effectiveCacheDirectory: String? { + if let cacheDirectory { + return cacheDirectory + } else if let outputDirectory { + return outputDirectory + } else { + return nil + } + } + + @Option(name: .shortAndLong, help: "How to handle an existing swift-java.config; by default 'overwrite' by can be changed to amending a configuration") + var existingConfig: ExistingConfigFileMode = .overwrite + public enum ExistingConfigFileMode: String, ExpressibleByArgument, Codable { + case overwrite + case amend + } + + @Option(name: .shortAndLong, help: "While scanning a classpath, inspect only types included in this package") + var javaPackageFilter: String? = nil + @Argument( help: "The input file, which is either a Java2Swift configuration file or (if '-jar' was specified) a Jar file." @@ -65,6 +95,27 @@ struct JavaToSwift: ParsableCommand { /// Whether we have ensured that the output directory exists. var createdOutputDirectory: Bool = false + var moduleBaseDir: Foundation.URL? { + if let outputDirectory { + if outputDirectory == "-" { + return nil + } + + return URL(fileURLWithPath: outputDirectory) + } + + guard let moduleName else { + return nil + } + + // Put the result into Sources/\(moduleName). + let baseDir = URL(fileURLWithPath: ".") + .appendingPathComponent("Sources", isDirectory: true) + .appendingPathComponent(moduleName, isDirectory: true) + + return baseDir + } + /// The output directory in which to place the generated files, which will /// be the specified directory (--output-directory or -o option) if given, /// or a default directory derived from the other command-line arguments. @@ -79,6 +130,10 @@ struct JavaToSwift: ParsableCommand { return URL(fileURLWithPath: outputDirectory) } + guard let moduleName else { + fatalError("--module-name must be set!") + } + // Put the result into Sources/\(moduleName). let baseDir = URL(fileURLWithPath: ".") .appendingPathComponent("Sources", isDirectory: true) @@ -97,200 +152,198 @@ struct JavaToSwift: ParsableCommand { return outputDir } - /// Describes what kind of generation action is being performed by - /// Java2Swift. - enum GenerationMode { + /// Describes what kind of generation action is being performed by swift-java. + enum ToolMode { /// Generate a configuration file given a Jar file. - case configuration(jarFile: String) + case configuration(extraClasspath: String) // FIXME: this is more like "extract" configuration from classpath /// Generate Swift wrappers for Java classes based on the given /// configuration. - case classWrappers(Configuration) - } + case classWrappers // (Configuration) - mutating func run() throws { - // Determine the mode in which we'll execute. - let generationMode: GenerationMode - if jar { - generationMode = .configuration(jarFile: input) - } else { - let config = try JavaTranslator.readConfiguration(from: URL(fileURLWithPath: input)) - generationMode = .classWrappers(config) - } + /// Fetch dependencies for a module + case fetchDependencies // (Configuration) + // FIXME each mode should have its own config? + } - // Load all of the dependent configurations and associate them with Swift - // modules. - let dependentConfigs = try dependsOn.map { dependentConfig in - guard let equalLoc = dependentConfig.firstIndex(of: "=") else { - throw JavaToSwiftError.badConfigOption(dependentConfig) + mutating func run() { + print("[info][swift-java] Run: \(CommandLine.arguments.joined(separator: " "))") + do { + let config: Configuration + + // Determine the mode in which we'll execute. + let toolMode: ToolMode + if jar { + if let moduleBaseDir { + config = try readConfiguration(sourceDir: moduleBaseDir.path) + } else { + config = Configuration() + } + toolMode = .configuration(extraClasspath: input) + } else if fetch { + config = try JavaTranslator.readConfiguration(from: URL(fileURLWithPath: input)) + guard let dependencies = config.dependencies else { + print("[swift-java] Running in 'fetch dependencies' mode but dependencies list was empty!") + print("[swift-java] Nothing to do: done.") + return + } + toolMode = .fetchDependencies + } else { + config = try JavaTranslator.readConfiguration(from: URL(fileURLWithPath: input)) + toolMode = .classWrappers } - let afterEqual = dependentConfig.index(after: equalLoc) - let swiftModuleName = String(dependentConfig[..] = currentClass.getClasses().compactMap { nestedClass in - guard let nestedClass else { return nil } - - // If this is a local class, we're done. - let javaClassName = nestedClass.getName() - if javaClassName.isLocalJavaClass { - return nil + // Add extra classpath entries which are specific to building the JavaKit project and samples + let classpathBuildJavaKitEntries = [ // FIXME: THIS IS A TRICK UNTIL WE FIGURE OUT HOW TO BOOTSTRAP THIS PART + FileManager.default.currentDirectoryPath, + FileManager.default.currentDirectoryPath + "/.build", + FileManager.default.currentDirectoryPath + "/JavaKit/build/libs", + URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + .deletingLastPathComponent() + .deletingLastPathComponent().absoluteURL.path + "/JavaKit/build/libs/JavaKit-1.0-SNAPSHOT.jar" + ] + classpathEntries += classpathBuildJavaKitEntries + + // Bring up the Java VM. + // TODO: print only in verbose mode + let classpath = classpathEntries.joined(separator: ":") + print("[debug][swift-java] Initialize JVM with classpath: \(classpath)") + + let jvm = try JavaVirtualMachine.shared(classpath: classpathEntries) + + // FIXME: we should resolve dependencies here perhaps + // if let dependencies = config.dependencies { + // print("[info][swift-java] Resolve dependencies...") + // let dependencyClasspath = try fetchDependencies( + // moduleName: moduleName, + // dependencies: dependencies, + // baseClasspath: classpathOptionEntries, + // environment: jvm.environment() + // ) + // classpathEntries += dependencyClasspath.classpathEntries + // } + + // * Classespaths from all dependent configuration files + for (_, config) in dependentConfigs { + // TODO: may need to resolve the dependent configs rather than just get their configs + // TODO: We should cache the resolved classpaths as well so we don't do it many times + config.classpath.map { entry in + print("[swift-java] Add dependent config classpath element: \(entry)") + classpathEntries.append(entry) } + } - // If this class has been explicitly mentioned, we're done. - if translator.translatedClasses[javaClassName] != nil { - return nil + // Run the task. + switch toolMode { + case .configuration: + try emitConfiguration( + classpath: classpath, + environment: jvm.environment() + ) + + case .classWrappers/*(let config)*/: + try generateWrappers( + config: config, + classpath: classpath, + dependentConfigs: dependentConfigs, + environment: jvm.environment() + ) + + case .fetchDependencies: + guard let dependencies = config.dependencies else { + fatalError("Configuration for fetching dependencies must have 'dependencies' defined!") + } + guard let moduleName = self.moduleName else { + fatalError("Fetching dependencies must specify module name (--module-name)!") + } + guard let effectiveCacheDirectory else { + fatalError("Fetching dependencies must effective cache directory! Specify --output-directory or --cache-directory") } - // Record this as a translated class. - let swiftUnqualifiedName = javaClassName.javaClassNameToCanonicalName - .defaultSwiftNameForJavaClass - + print("[debug][swift-java] Base classpath to fetch dependencies: \(classpathOptionEntries)") - let swiftName = "\(currentSwiftName).\(swiftUnqualifiedName)" - translator.translatedClasses[javaClassName] = (swiftName, nil) - return nestedClass - } + let dependencyClasspath = try fetchDependencies( + moduleName: moduleName, + dependencies: dependencies, + baseClasspath: classpathOptionEntries, + environment: jvm.environment() + ) - // If there were no new nested classes, there's nothing to do. - if nestedClasses.isEmpty { - continue + try writeFetchedDependenciesClasspath( + moduleName: moduleName, + cacheDir: effectiveCacheDirectory, + resolvedClasspath: dependencyClasspath) } - - // Record all of the nested classes that we will visit. - translator.nestedClasses[currentClass.getName()] = nestedClasses - allClassesToVisit.append(contentsOf: nestedClasses) + } catch { + // We fail like this since throwing out of the run often ends up hiding the failure reason when it is executed as SwiftPM plugin (!) + let message = "Failed with error: \(error)" + print("[error][java-swift] \(message)") + fatalError(message) } - // Validate configurations before writing any files - try translator.validateClassConfiguration() - - // Translate all of the Java classes into Swift classes. - for javaClass in javaClasses { - translator.startNewFile() - let swiftClassDecls = try translator.translateClass(javaClass) - let importDecls = translator.getImportDecls() - - let swiftFileText = """ - // Auto-generated by Java-to-Swift wrapper generator. - \(importDecls.map { $0.description }.joined()) - \(swiftClassDecls.map { $0.description }.joined(separator: "\n")) - - """ - - let swiftFileName = try! translator.getSwiftTypeName(javaClass, preferValueTypes: false) - .swiftName.replacing(".", with: "+") + ".swift" - try writeContents( - swiftFileText, - to: swiftFileName, - description: "Java class '\(javaClass.getName())' translation" - ) - } + // Just for debugging so it is clear which command has finished + print("[debug][swift-java] " + "Done: ".green + CommandLine.arguments.joined(separator: " ").green) } private func names(from javaClassNameOpt: String) -> (javaClassName: String, swiftName: String) { @@ -314,8 +367,22 @@ struct JavaToSwift: ParsableCommand { return (javaClassName, swiftName.javaClassNameToCanonicalName) } - mutating func writeContents(_ contents: String, to filename: String, description: String) throws { - guard let outputDir = actualOutputDirectory else { + mutating func writeContents( + _ contents: String, + to filename: String, description: String) throws { + try writeContents( + contents, + outputDirectoryOverride: self.actualOutputDirectory, + to: filename, + description: description) + } + + mutating func writeContents( + _ contents: String, + outputDirectoryOverride: Foundation.URL?, + to filename: String, + description: String) throws { + guard let outputDir = (outputDirectoryOverride ?? actualOutputDirectory) else { print("// \(filename) - \(description)") print(contents) return @@ -333,48 +400,28 @@ struct JavaToSwift: ParsableCommand { // Write the file: let file = outputDir.appendingPathComponent(filename) - print("Writing \(description) to '\(file.path)'...", terminator: "") + print("[debug][swift-java] Writing \(description) to '\(file.path)'... ", terminator: "") try contents.write(to: file, atomically: true, encoding: .utf8) - print(" done.") + print("done.".green) } +} - mutating func emitConfiguration( - forJarFile jarFileName: String, - classpath: String, - environment: JNIEnvironment - ) throws { - var configuration = Configuration(classpath: classpath) - - let jarFile = try JarFile(jarFileName, false, environment: environment) - for entry in jarFile.entries()! { - // We only look at class files in the Jar file. - guard entry.getName().hasSuffix(".class") else { - continue - } - - // If this is a local class, it cannot be mapped into Swift. - if entry.getName().isLocalJavaClass { - continue - } - - let javaCanonicalName = String(entry.getName().replacing("/", with: ".") - .dropLast(".class".count)) - configuration.classes[javaCanonicalName] = - javaCanonicalName.defaultSwiftNameForJavaClass +extension JavaToSwift { + /// Get base configuration, depending on if we are to 'amend' or 'overwrite' the existing configuration. + package func getBaseConfigurationForWrite() throws -> (Bool, Configuration) { + guard let actualOutputDirectory = self.actualOutputDirectory else { + // If output has no path there's nothing to amend + return (false, .init()) } - // Encode the configuration. - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted, .sortedKeys] - var contents = String(data: try encoder.encode(configuration), encoding: .utf8)! - contents.append("\n") - - // Write the file. - try writeContents( - contents, - to: "swift-java.config", - description: "swift-java configuration file" - ) + switch self.existingConfig { + case .overwrite: + // always make up a fresh instance if we're overwriting + return (false, .init()) + case .amend: + let configPath = actualOutputDirectory + return (true, try readConfiguration(sourceDir: configPath.path)) + } } } @@ -391,19 +438,6 @@ extension JavaToSwiftError: CustomStringConvertible { } } -extension String { - /// For a String that's of the form java.util.Vector, return the "Vector" - /// part. - fileprivate var defaultSwiftNameForJavaClass: String { - if let dotLoc = lastIndex(of: ".") { - let afterDot = index(after: dotLoc) - return String(self[afterDot...]).javaClassNameToCanonicalName.adjustedSwiftTypeName - } - - return javaClassNameToCanonicalName.adjustedSwiftTypeName - } -} - @JavaClass("java.lang.ClassLoader") public struct ClassLoader { @JavaMethod @@ -415,29 +449,3 @@ extension JavaClass { public func getSystemClassLoader() -> ClassLoader? } -extension String { - /// Replace all of the $'s for nested names with "." to turn a Java class - /// name into a Java canonical class name, - fileprivate var javaClassNameToCanonicalName: String { - return replacing("$", with: ".") - } - - /// Whether this is the name of an anonymous class. - fileprivate var isLocalJavaClass: Bool { - for segment in split(separator: "$") { - if let firstChar = segment.first, firstChar.isNumber { - return true - } - } - - return false - } - - /// Adjust type name for "bad" type names that don't work well in Swift. - fileprivate var adjustedSwiftTypeName: String { - switch self { - case "Type": return "JavaType" - default: return self - } - } -} diff --git a/Sources/Java2Swift/String+Extensions.swift b/Sources/Java2Swift/String+Extensions.swift new file mode 100644 index 00000000..ba26f892 --- /dev/null +++ b/Sources/Java2Swift/String+Extensions.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// 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 ArgumentParser +import Java2SwiftLib +import JavaKit +import JavaKitJar +import Java2SwiftLib +import JavaKitDependencyResolver +import JavaKitConfigurationShared + +extension String { + /// For a String that's of the form java.util.Vector, return the "Vector" + /// part. + var defaultSwiftNameForJavaClass: String { + if let dotLoc = lastIndex(of: ".") { + let afterDot = index(after: dotLoc) + return String(self[afterDot...]).javaClassNameToCanonicalName.adjustedSwiftTypeName + } + + return javaClassNameToCanonicalName.adjustedSwiftTypeName + } +} + +extension String { + /// Replace all of the $'s for nested names with "." to turn a Java class + /// name into a Java canonical class name, + var javaClassNameToCanonicalName: String { + return replacing("$", with: ".") + } + + /// Whether this is the name of an anonymous class. + var isLocalJavaClass: Bool { + for segment in split(separator: "$") { + if let firstChar = segment.first, firstChar.isNumber { + return true + } + } + + return false + } + + /// Adjust type name for "bad" type names that don't work well in Swift. + var adjustedSwiftTypeName: String { + switch self { + case "Type": return "JavaType" + default: return self + } + } +} diff --git a/Sources/Java2SwiftLib/Configuration.swift b/Sources/Java2SwiftLib/Configuration.swift deleted file mode 100644 index 2cda8476..00000000 --- a/Sources/Java2SwiftLib/Configuration.swift +++ /dev/null @@ -1,45 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 typealias JavaVersion = Int - -/// Configuration for the Java2Swift translation tool, provided on a per-target -/// basis. -/// -/// Note: there is a copy of this struct in the Java2Swift plugin. They -/// must be kept in sync. -package struct Configuration: Codable { - /// The Java class path that should be passed along to the Java2Swift tool. - package var classpath: String? = nil - - /// The Java classes that should be translated to Swift. The keys are - /// canonical Java class names (e.g., java.util.Vector) and the values are - /// the corresponding Swift names (e.g., JavaVector). - package var classes: [String: String] = [:] - - package var sourceCompatibility: JavaVersion? - package var targetCompatibility: JavaVersion? - - package init( - classpath: String? = nil, - classes: [String : String] = [:], - sourceCompatibility: JavaVersion? = nil, - targetCompatibility: JavaVersion? = nil - ) { - self.classpath = classpath - self.classes = classes - self.sourceCompatibility = sourceCompatibility - self.targetCompatibility = targetCompatibility - } -} diff --git a/Sources/Java2SwiftLib/JavaTranslator+Configuration.swift b/Sources/Java2SwiftLib/JavaTranslator+Configuration.swift index b84d7a1e..e0f6d0cb 100644 --- a/Sources/Java2SwiftLib/JavaTranslator+Configuration.swift +++ b/Sources/Java2SwiftLib/JavaTranslator+Configuration.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import Foundation +import JavaKitConfigurationShared extension JavaTranslator { /// Read a configuration file from the given URL. @@ -24,7 +25,11 @@ extension JavaTranslator { /// Load the configuration file with the given name to populate the known set of /// translated Java classes. package func addConfiguration(_ config: Configuration, forSwiftModule swiftModule: String) { - for (javaClassName, swiftName) in config.classes { + guard let classes = config.classes else { + return + } + + for (javaClassName, swiftName) in classes { translatedClasses[javaClassName] = ( swiftType: swiftName, swiftModule: swiftModule diff --git a/Sources/Java2SwiftLib/JavaTranslator+Validation.swift b/Sources/Java2SwiftLib/JavaTranslator+Validation.swift index c85cedb0..9d4d00ca 100644 --- a/Sources/Java2SwiftLib/JavaTranslator+Validation.swift +++ b/Sources/Java2SwiftLib/JavaTranslator+Validation.swift @@ -52,7 +52,7 @@ package extension JavaTranslator { } } - func validateClassConfiguration() throws { + func validateClassConfiguration() throws(ValidationError) { // Group all classes by swift name let groupedDictionary: [SwiftTypeName: [(String, (String, String?))]] = Dictionary(grouping: translatedClasses, by: { SwiftTypeName(swiftType: $0.value.swiftType, swiftModule: $0.value.swiftModule) }) // Find all that are mapped to multiple names diff --git a/Sources/Java2SwiftLib/JavaTranslator.swift b/Sources/Java2SwiftLib/JavaTranslator.swift index 69edac6c..225d3868 100644 --- a/Sources/Java2SwiftLib/JavaTranslator.swift +++ b/Sources/Java2SwiftLib/JavaTranslator.swift @@ -17,6 +17,7 @@ import JavaKitReflection import JavaTypes import SwiftBasicFormat import SwiftSyntax +import JavaKitConfigurationShared import SwiftSyntaxBuilder /// Utility that translates Java classes into Swift source code to access diff --git a/Sources/JavaKit/JavaKitVM/JavaVirtualMachine.swift b/Sources/JavaKit/JavaKitVM/JavaVirtualMachine.swift index 61f01f51..0800a89e 100644 --- a/Sources/JavaKit/JavaKitVM/JavaVirtualMachine.swift +++ b/Sources/JavaKit/JavaKitVM/JavaVirtualMachine.swift @@ -32,13 +32,16 @@ public final class JavaVirtualMachine: @unchecked Sendable { /// The Java virtual machine instance. private let jvm: JavaVMPointer + let classpath: [String] + /// Whether to destroy the JVM on deinit. - private let destroyOnDeinit: Bool + private let destroyOnDeinit: LockedState // FIXME: we should require macOS 15 and then use Synchronization /// Adopt an existing JVM pointer. public init(adoptingJVM jvm: JavaVMPointer) { self.jvm = jvm - self.destroyOnDeinit = false + self.classpath = [] // FIXME: bad... + self.destroyOnDeinit = .init(initialState: false) } /// Initialize a new Java virtual machine instance. @@ -56,6 +59,7 @@ public final class JavaVirtualMachine: @unchecked Sendable { vmOptions: [String] = [], ignoreUnrecognized: Bool = false ) throws { + self.classpath = classpath var jvm: JavaVMPointer? = nil var environment: UnsafeMutableRawPointer? = nil var vmArgs = JavaVMInitArgs() @@ -68,7 +72,8 @@ public final class JavaVirtualMachine: @unchecked Sendable { let fileManager = FileManager.default for path in classpath { if !fileManager.fileExists(atPath: path) { - throw JavaKitError.classpathEntryNotFound(entry: path, classpath: classpath) + // FIXME: this should be configurable, a classpath missing a directory isn't reason to blow up + print("[warning][swift-java][JavaVirtualMachine] Missing classpath element: \(URL(fileURLWithPath: path).absoluteString)") // TODO: stderr } } let colonSeparatedClassPath = classpath.joined(separator: ":") @@ -104,20 +109,38 @@ public final class JavaVirtualMachine: @unchecked Sendable { } self.jvm = jvm! - self.destroyOnDeinit = true + self.destroyOnDeinit = .init(initialState: true) + } + + public func destroyJVM() throws { + try self.detachCurrentThread() + if let error = VMError(fromJNIError: jvm.pointee!.pointee.DestroyJavaVM(jvm)) { + throw error + } + + destroyOnDeinit.withLock { $0 = false } // we destroyed explicitly, disable destroy in deinit } deinit { - if destroyOnDeinit { - // Destroy the JVM. - if let resultError = VMError(fromJNIError: jvm.pointee!.pointee.DestroyJavaVM(jvm)) { - fatalError("Failed to destroy the JVM: \(resultError)") + if destroyOnDeinit.withLock({ $0 }) { + do { + try destroyJVM() + } catch { + fatalError("Failed to destroy the JVM: \(error)") } } } } +extension JavaVirtualMachine: CustomStringConvertible { + public var description: String { + "\(Self.self)(\(jvm))" + } +} + +// ==== ------------------------------------------------------------------------ // MARK: Java thread management. + extension JavaVirtualMachine { /// Produce the JNI environment for the active thread, attaching this /// thread to the JVM if it isn't already. @@ -148,6 +171,7 @@ extension JavaVirtualMachine { // If we failed to attach, report that. if let attachError = VMError(fromJNIError: attachResult) { + fatalError("JVM Error: \(attachError)") throw attachError } @@ -164,8 +188,9 @@ extension JavaVirtualMachine { } } } - +// ==== ------------------------------------------------------------------------ // MARK: Shared Java Virtual Machine management. + extension JavaVirtualMachine { /// The globally shared JavaVirtualMachine instance, behind a lock. /// @@ -190,15 +215,26 @@ extension JavaVirtualMachine { /// be prefixed by the class-path argument described above. /// - ignoreUnrecognized: Whether the JVM should ignore any VM options it /// does not recognize. + /// - replace: replace the existing shared JVM instance public static func shared( classpath: [String] = [], vmOptions: [String] = [], - ignoreUnrecognized: Bool = false + ignoreUnrecognized: Bool = false, + replace: Bool = false ) throws -> JavaVirtualMachine { - try sharedJVM.withLock { (sharedJVMPointer: inout JavaVirtualMachine?) in + precondition(!classpath.contains(where: { $0.contains(":") }), "Classpath element must not contain `:`! Split the path into elements! Was: \(classpath)") + + return try sharedJVM.withLock { (sharedJVMPointer: inout JavaVirtualMachine?) in // If we already have a JavaVirtualMachine instance, return it. - if let existingInstance = sharedJVMPointer { - return existingInstance + if replace { + print("[swift-java] Replace JVM instance!") + try sharedJVMPointer?.destroyJVM() + sharedJVMPointer = nil + } else { + if let existingInstance = sharedJVMPointer { + // FIXME: this isn't ideal; we silently ignored that we may have requested a different classpath or options + return existingInstance + } } while true { @@ -273,9 +309,9 @@ extension JavaVirtualMachine { case invalidArguments /// Unknown JNI error. - case unknown(jint) + case unknown(jint, file: String, line: UInt) - init?(fromJNIError error: jint) { + init?(fromJNIError error: jint, file: String = #fileID, line: UInt = #line) { switch error { case JNI_OK: return nil case JNI_EDETACHED: self = .threadDetached @@ -283,7 +319,7 @@ extension JavaVirtualMachine { case JNI_ENOMEM: self = .outOfMemory case JNI_EEXIST: self = .existingVM case JNI_EINVAL: self = .invalidArguments - default: self = .unknown(error) + default: self = .unknown(error, file: file, line: line) } } } diff --git a/Sources/JavaKitConfigurationShared/Configuration.swift b/Sources/JavaKitConfigurationShared/Configuration.swift new file mode 100644 index 00000000..2078c544 --- /dev/null +++ b/Sources/JavaKitConfigurationShared/Configuration.swift @@ -0,0 +1,191 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +//////////////////////////////////////////////////////////////////////////////// +// This file is only supposed to be edited in `Shared/` and must be symlinked // +// from everywhere else! We cannot share dependencies with or between plugins // +//////////////////////////////////////////////////////////////////////////////// + +public typealias JavaVersion = Int + +/// Configuration for the SwiftJava plugins, provided on a per-target basis. +public struct Configuration: Codable { + // ==== swift 2 java --------------------------------------------------------- + + public var javaPackage: String? + + // ==== java 2 swift --------------------------------------------------------- + + /// The Java class path that should be passed along to the Java2Swift tool. + public var classpath: String? = nil + + public var classpathEntries: [String] { + guard let classpath else { + return [] + } + + return classpath.split(separator: ":").map(String.init) + } + + /// The Java classes that should be translated to Swift. The keys are + /// canonical Java class names (e.g., java.util.Vector) and the values are + /// the corresponding Swift names (e.g., JavaVector). + public var classes: [String: String]? = [:] + + // Compile for the specified Java SE release. + public var sourceCompatibility: JavaVersion? + + // Generate class files suitable for the specified Java SE release. + public var targetCompatibility: JavaVersion? + + // ==== dependencies --------------------------------------------------------- + + // Java dependencies we need to fetch for this target. + public var dependencies: [JavaDependencyDescriptor]? + + public init() { + } + +} + +/// Represents a maven-style Java dependency. +public struct JavaDependencyDescriptor: Hashable, Codable { + public var groupID: String + public var artifactID: String + public var version: String + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + let parts = string.split(separator: ":") + + if parts.count == 1 && string.hasPrefix(":") { + self.groupID = "" + self.artifactID = ":" + String(parts.first!) + self.version = "" + return + } + + guard parts.count == 3 else { + throw JavaDependencyDescriptorError(message: "Illegal dependency, did not match: `groupID:artifactID:version`, parts: '\(parts)'") + } + + self.groupID = String(parts[0]) + self.artifactID = String(parts[1]) + self.version = String(parts[2]) + } + + public var descriptionGradleStyle: String { + [groupID, artifactID, version].joined(separator: ":") + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode("\(self.groupID):\(self.artifactID):\(self.version)") + } + + struct JavaDependencyDescriptorError: Error { + let message: String + } +} + +public func readConfiguration(sourceDir: String, file: String = #fileID, line: UInt = #line) throws -> Configuration { + // Workaround since filePath is macOS 13 + let sourcePath = + if sourceDir.hasPrefix("file://") { sourceDir } else { "file://" + sourceDir } + let configPath = URL(string: sourcePath)!.appendingPathComponent("swift-java.config", isDirectory: false) + + return try readConfiguration(configPath: configPath, file: file, line: line) +} + +public func readConfiguration(configPath: URL, file: String = #fileID, line: UInt = #line) throws -> Configuration { + do { + let configData = try Data(contentsOf: configPath) + return try JSONDecoder().decode(Configuration.self, from: configData) + } catch { + throw ConfigurationError(message: "Failed to parse SwiftJava configuration at '\(configPath)'!", error: error, + file: file, line: line) + } +} + +public func findSwiftJavaClasspaths(in basePath: String = FileManager.default.currentDirectoryPath) -> [String] { + let fileManager = FileManager.default + + let baseURL = URL(fileURLWithPath: basePath) + var classpathEntries: [String] = [] + + print("[debug][swift-java] Searching for *.swift-java.classpath files in: \(baseURL)") + guard let enumerator = fileManager.enumerator(at: baseURL, includingPropertiesForKeys: []) else { + print("[warning][swift-java] Failed to get enumerator for \(baseURL)") + return [] + } + + for case let fileURL as URL in enumerator { + if fileURL.lastPathComponent.hasSuffix(".swift-java.classpath") { + print("[debug][swift-java] Constructing classpath with entries from: \(fileURL.relativePath)") + if let contents = try? String(contentsOf: fileURL) { + let entries = contents.split(separator: ":").map(String.init) + for entry in entries { + print("[debug][swift-java] Classpath += \(entry)") + } + classpathEntries += entries + } + } + } + + return classpathEntries +} + +extension Configuration { + public var compilerVersionArgs: [String] { + var compilerVersionArgs = [String]() + + if let sourceCompatibility { + compilerVersionArgs += ["--source", String(sourceCompatibility)] + } + if let targetCompatibility { + compilerVersionArgs += ["--target", String(targetCompatibility)] + } + + return compilerVersionArgs + } +} + +extension Configuration { + /// Render the configuration as JSON text. + public func renderJSON() throws -> String { + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + var contents = String(data: try encoder.encode(self), encoding: .utf8)! + contents.append("\n") + return contents + } +} + +public struct ConfigurationError: Error { + let message: String + let error: any Error + + let file: String + let line: UInt + + init(message: String, error: any Error, file: String = #fileID, line: UInt = #line) { + self.message = message + self.error = error + self.file = file + self.line = line + } +} diff --git a/Sources/JavaKitDependencyResolver/DependencyResolver.swift b/Sources/JavaKitDependencyResolver/DependencyResolver.swift new file mode 100644 index 00000000..697e3d2a --- /dev/null +++ b/Sources/JavaKitDependencyResolver/DependencyResolver.swift @@ -0,0 +1,32 @@ +//===----------------------------------------------------------------------===// +// +// 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 JavaKit +import JavaRuntime + +@JavaInterface("org.swift.javakit.dependencies.DependencyResolver") +public struct DependencyResolver { +} + +extension JavaClass { + + @JavaStaticMethod + public func resolveDependenciesToClasspath( + projectBaseDirectory: String, + dependencies: [String]) throws -> String + + @JavaStaticMethod + public func hasDependencyResolverDependenciesLoaded() -> Bool + +} \ No newline at end of file diff --git a/Sources/JavaKitDependencyResolver/swift-java.config b/Sources/JavaKitDependencyResolver/swift-java.config new file mode 100644 index 00000000..d2d9f448 --- /dev/null +++ b/Sources/JavaKitDependencyResolver/swift-java.config @@ -0,0 +1,9 @@ +{ + "dependencies": [ + ":JavaKit", + ] + , + "__classes": { + "org.swift.javakit.dependencies.DependencyResolver": "DependencyResolver" + } + } diff --git a/Sources/JExtractSwift/TerminalColors.swift b/Sources/JavaKitShared/TerminalColors.swift similarity index 100% rename from Sources/JExtractSwift/TerminalColors.swift rename to Sources/JavaKitShared/TerminalColors.swift diff --git a/settings.gradle b/settings.gradle index fa0fa5bd..c1168a68 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,6 +18,7 @@ pluginManagement { rootProject.name = "swift-java" +include "JavaKit" include "SwiftKit" // Include sample apps -- you can run them via `gradle Name:run`