diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8f66170d..433b5629 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -268,3 +268,21 @@ jobs: SWIFT_VERSION: "${{ matrix.swift_version }}" - name: "Verify sample ${{ matrix.sample_app }}" run: .github/scripts/validate_sample.sh Samples/${{ matrix.sample_app }} + + linkage-test: + name: Linkage test (jammy swift:6.2) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + jdk_vendor: ['corretto'] + container: + image: swift:6.2-jammy + steps: + - uses: actions/checkout@v6 + - name: Prepare CI Environment + uses: ./.github/actions/prepare_env + - name: Run linkage test + run: ./scripts/run-linkage-test.sh + env: + JAVA_HOME: ${{ env.JAVA_HOME }} diff --git a/Tests/LinkageTest/Package.swift b/Tests/LinkageTest/Package.swift new file mode 100644 index 00000000..9d7a5ab2 --- /dev/null +++ b/Tests/LinkageTest/Package.swift @@ -0,0 +1,166 @@ +// swift-tools-version: 6.1 + +import Foundation +import PackageDescription + +// 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 + } + + if let home = getJavaHomeFromLibexecJavaHome(), + !home.isEmpty + { + return home + } + + if let home = getJavaHomeFromSDKMAN() { + return home + } + + if let home = getJavaHomeFromPath() { + return home + } + + if ProcessInfo.processInfo.environment["SPI_PROCESSING"] == "1" + && ProcessInfo.processInfo.environment["SPI_BUILD"] == nil + { + return "" + } + fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.") +} + +/// On MacOS we can use the java_home tool as a fallback if we can't find JAVA_HOME environment variable. +func getJavaHomeFromLibexecJavaHome() -> String? { + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/libexec/java_home") + + guard FileManager.default.fileExists(atPath: task.executableURL!.path) else { + return nil + } + + let pipe = Pipe() + task.standardOutput = pipe + task.standardError = pipe + + do { + try task.run() + task.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) + + if task.terminationStatus == 0 { + return output + } else { + return nil + } + } catch { + return nil + } +} + +func getJavaHomeFromSDKMAN() -> String? { + let home = FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent(".sdkman/candidates/java/current") + + let javaBin = home.appendingPathComponent("bin/java").path + if FileManager.default.isExecutableFile(atPath: javaBin) { + return home.path + } + return nil +} + +func getJavaHomeFromPath() -> String? { + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/bin/which") + task.arguments = ["java"] + + let pipe = Pipe() + task.standardOutput = pipe + + do { + try task.run() + task.waitUntilExit() + guard task.terminationStatus == 0 else { return nil } + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + guard + let javaPath = String(data: data, encoding: .utf8)? + .trimmingCharacters(in: .whitespacesAndNewlines), + !javaPath.isEmpty + else { return nil } + + let resolved = URL(fileURLWithPath: javaPath).resolvingSymlinksInPath() + return + resolved + .deletingLastPathComponent() + .deletingLastPathComponent() + .path + } catch { + return nil + } +} + +let javaHome = findJavaHome() + +let javaIncludePath = "\(javaHome)/include" +#if os(Linux) +let javaPlatformIncludePath = "\(javaIncludePath)/linux" +#elseif os(macOS) +let javaPlatformIncludePath = "\(javaIncludePath)/darwin" +#elseif os(Windows) +let javaPlatformIncludePath = "\(javaIncludePath)/win32" +#endif + +let package = Package( + name: "linkage-test", + dependencies: [ + .package(name: "swift-java", path: "../..") + ], + targets: [ + .executableTarget( + name: "LinkageTest", + dependencies: [ + .product(name: "SwiftJava", package: "swift-java") + ], + swiftSettings: [ + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) + ], + linkerSettings: [ + .unsafeFlags( + [ + "-L\(javaHome)/lib/server", + "-Xlinker", "-rpath", + "-Xlinker", "\(javaHome)/lib/server", + ], + .when(platforms: [.linux, .macOS]) + ), + .unsafeFlags( + [ + "-L\(javaHome)/lib" + ], + .when(platforms: [.windows]) + ), + .linkedLibrary( + "jvm", + .when(platforms: [.linux, .macOS, .windows]) + ), + ] + ) + ] +) diff --git a/Tests/LinkageTest/Sources/LinkageTest/main.swift b/Tests/LinkageTest/Sources/LinkageTest/main.swift new file mode 100644 index 00000000..73c095f4 --- /dev/null +++ b/Tests/LinkageTest/Sources/LinkageTest/main.swift @@ -0,0 +1,19 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2025 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 SwiftJava + +let _ = try? JavaVirtualMachine.shared() +let _ = SwiftPlatform.debugOrRelease +print("Linkage test passed: SwiftJava imported successfully.") diff --git a/scripts/run-linkage-test.sh b/scripts/run-linkage-test.sh new file mode 100755 index 00000000..255f8386 --- /dev/null +++ b/scripts/run-linkage-test.sh @@ -0,0 +1,53 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## 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 +## +##===----------------------------------------------------------------------===## +set -eu + +# Validate that we're running on Linux +if [[ "$(uname -s)" != "Linux" ]]; then + echo "Error: This script must be run on Linux. Current OS: $(uname -s)" >&2 + exit 1 +fi + +echo "Detected JAVA_HOME=${JAVA_HOME}" + +echo "Running on Linux - proceeding with linkage test..." + +# Build the linkage test package +echo "Building linkage test package..." +swift build --package-path Tests/LinkageTest + +# Construct build path +build_path=$(swift build --package-path Tests/LinkageTest --show-bin-path) +binary_path="$build_path/LinkageTest" + +# Verify the binary exists +if [[ ! -f "$binary_path" ]]; then + echo "Error: Built binary not found at $binary_path" >&2 + exit 1 +fi + +echo "Checking linkage for binary: $binary_path" + +# Run ldd and check if libFoundation.so is linked +ldd_output=$(ldd "$binary_path") +echo "LDD output:" +echo "$ldd_output" + +if echo "$ldd_output" | grep -q "libFoundation.so"; then + echo "Error: Binary is linked against libFoundation.so - this indicates incorrect linkage. Ensure the full Foundation is not linked on Linux when FoundationEssentials is available." >&2 + exit 1 +else + echo "Success: Binary is not linked against libFoundation.so - linkage test passed." +fi