diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 282d5ea..6b21c8b 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -15,10 +15,24 @@ jobs: enable_windows_checks: true windows_swift_versions: '["nightly-main"]' enable_macos_checks: true + macos_pre_build_command: brew install openjdk enable_ios_checks: false enable_linux_checks: true linux_os_versions: '["amazonlinux2", "bookworm", "noble", "jammy", "rhel-ubi9"]' linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}]" + linux_pre_build_command: | + if command -v apt-get >/dev/null 2>&1 ; then # bookworm, noble, jammy + if command -v sudo &> /dev/null && [ "$EUID" -ne 0 ]; then + sudo apt-get update -y + sudo apt-get install -y default-jdk + else + apt-get update -y + apt-get install -y default-jdk + fi + elif command -v yum >/dev/null 2>&1 ; then # amazonlinux2, rhel-ubi9 + yum update -y + yum install -y java-devel + fi enable_android_sdk_build: true enable_android_sdk_checks: true soundness: diff --git a/Sources/SwiftJavaJNICore/VirtualMachine/JavaVirtualMachine.swift b/Sources/SwiftJavaJNICore/VirtualMachine/JavaVirtualMachine.swift index 04c2367..e5fdd3d 100644 --- a/Sources/SwiftJavaJNICore/VirtualMachine/JavaVirtualMachine.swift +++ b/Sources/SwiftJavaJNICore/VirtualMachine/JavaVirtualMachine.swift @@ -427,6 +427,42 @@ private func symbol(_ handle: DylibType, _ name: String) -> T? { } #endif +/// The Java installation folder for the system, found either with `JAVA_HOME` environment or by checking standard locations +func systemJavaHome() -> String? { + // always defer to JAVA_HOME when it is set in the environment + if let javaHome = ProcessInfo.processInfo.environment["JAVA_HOME"] { + return javaHome + } + + // if JAVA_HOME is unset, hunt around some standard locations + var defaultJavaPaths: [String] = [] + + #if os(macOS) + #if arch(x86_64) + defaultJavaPaths += ["/usr/local/opt/java"] // macOS Homebrew x86 + #else + defaultJavaPaths += ["/opt/homebrew/opt/java"] // macOS Homebrew + #endif + defaultJavaPaths += ["/System/Library/Frameworks/JavaVM.framework/Home"] // macOS legacy path + #endif + + #if os(Linux) + defaultJavaPaths += [ + "/usr/lib/jvm/default-java", // Ubuntu/Debian + "/usr/lib/jvm/default", // Arch + "/usr/lib/jvm/java", // rhel-ubi9, amazonlinux2 + ] + #endif + + if let javaHome = defaultJavaPaths.first(where: { + FileManager.default.fileExists(atPath: $0) + }) { + return javaHome + } + + return nil +} + /// Located the shared library that includes the `JNI_GetCreatedJavaVMs` and `JNI_CreateJavaVM` entry points to the `JNINativeInterface` function table private func loadLibJava() throws -> DylibType { #if os(Android) @@ -437,20 +473,7 @@ private func loadLibJava() throws -> DylibType { } #endif - guard - let javaHome = ProcessInfo.processInfo.environment["JAVA_HOME"] - ?? { - // if JAVA_HOME is unset, look in some standard locations - [ - "/opt/homebrew/opt/java", // macOS Homebrew - "/usr/local/opt/java", - "/usr/lib/jvm/default-java", // Ubuntu/Debian - "/usr/lib/jvm/default", // Arch - ].first(where: { - FileManager.default.fileExists(atPath: $0) - }) - }() - else { + guard let javaHome = systemJavaHome() else { throw JavaVirtualMachine.VMError.javaHomeNotFound } diff --git a/Tests/SwiftJavaJNICoreTests/JavaVirtualMachineTests.swift b/Tests/SwiftJavaJNICoreTests/JavaVirtualMachineTests.swift new file mode 100644 index 0000000..d2dedd5 --- /dev/null +++ b/Tests/SwiftJavaJNICoreTests/JavaVirtualMachineTests.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 Testing + +@testable import SwiftJavaJNICore + +@Suite +struct JavaVirtualMachineTests { + + static var isSupportedPlatform: Bool { + #if os(Android) + // Android tests are not currently run within an .apk and so do not have any ambient JVM + return false + #else + // disable test when we cannot find a system Java + return systemJavaHome() != nil + #endif + } + + @Test(.enabled(if: isSupportedPlatform)) + func loadJVMAndCallMethod() throws { + // Load or create a shared JVM instance + let jvm = try JavaVirtualMachine.shared() + + // Get the JNI environment for the current thread + let env = try jvm.environment() + + // Find java.lang.System + let systemClass = env.interface.FindClass(env, "java/lang/System") + #expect(systemClass != nil, "Failed to find java.lang.System") + + // Look up the static method currentTimeMillis()J + let methodID = env.interface.GetStaticMethodID( + env, + systemClass, + "currentTimeMillis", + "()J" + ) + #expect(methodID != nil, "Failed to find System.currentTimeMillis") + + // Call System.currentTimeMillis() — returns jlong + let timeMillis = env.interface.CallStaticLongMethodA( + env, + systemClass, + methodID, + nil + ) + #expect(timeMillis > 0, "Expected a positive timestamp, got \(timeMillis)") + } +}