Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,11 @@ jobs:
image: swift:6.3-noble
steps:
- uses: actions/checkout@v7
- name: Install jq
shell: bash
run: |
apt-get update -qq
apt-get install -qq -y jq
- name: Prepare CI Environment
uses: ./.github/actions/prepare_env
- name: Cache Tests/LinkageTest/.build
Expand Down
28 changes: 22 additions & 6 deletions Tests/LinkageTest/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@
import Foundation
import PackageDescription

// Every runtime product exported from swift-java should be included here.
// This is to verify none of the modules would accidentally pull in Foundation.
// `scripts/run-linkage-test.sh` enforces set equality with the root package.
let runtimeLibraryProducts: [String] = [
// RUNTIME_LIBRARY_PRODUCTS:START
"JavaIO",
"JavaLangReflect",
"JavaNet",
"JavaUtil",
"JavaUtilFunction",
"JavaUtilJar",
"SwiftJava",
"SwiftRuntimeFunctions",
// RUNTIME_LIBRARY_PRODUCTS:END
]

let runtimeDependencies: [Target.Dependency] = runtimeLibraryProducts.map {
.product(name: $0, package: "swift-java")
}

let package = Package(
name: "linkage-test",
platforms: [
Expand All @@ -14,15 +34,11 @@ let package = Package(
targets: [
.executableTarget(
name: "LinkageTest",
dependencies: [
.product(name: "SwiftJava", package: "swift-java")
]
dependencies: runtimeDependencies
),
.executableTarget(
name: "JExtractLinkageTest",
dependencies: [
.product(name: "SwiftJava", package: "swift-java")
],
dependencies: runtimeDependencies,
exclude: [
"swift-java.config"
],
Expand Down
13 changes: 12 additions & 1 deletion Tests/LinkageTest/Sources/JExtractLinkageTest/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors
// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand All @@ -12,7 +12,18 @@
//
//===----------------------------------------------------------------------===//

// See `Package.swift` for the authoritative list.
// IMPORT_RUNTIME_LIBRARY_PRODUCTS:START
import JavaIO
import JavaLangReflect
import JavaNet
import JavaUtil
import JavaUtilFunction
import JavaUtilJar
import SwiftJava
import SwiftRuntimeFunctions

// IMPORT_RUNTIME_LIBRARY_PRODUCTS:END

let _ = try? JavaVirtualMachine.shared()
let _ = SwiftPlatform.debugOrRelease
Expand Down
16 changes: 15 additions & 1 deletion Tests/LinkageTest/Sources/LinkageTest/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024-2025 Apple Inc. and the Swift.org project authors
// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand All @@ -12,7 +12,21 @@
//
//===----------------------------------------------------------------------===//

// Import every runtime library product exported from swift-java. If any of
// these libraries accidentally does `import Foundation` we'll catch it via `ldd`.
//
// See `Package.swift` for the authoritative list.
// IMPORT_RUNTIME_LIBRARY_PRODUCTS:START
import JavaIO
import JavaLangReflect
import JavaNet
import JavaUtil
import JavaUtilFunction
import JavaUtilJar
import SwiftJava
import SwiftRuntimeFunctions

// IMPORT_RUNTIME_LIBRARY_PRODUCTS:END

let _ = try? JavaVirtualMachine.shared()
let _ = SwiftPlatform.debugOrRelease
Expand Down
178 changes: 175 additions & 3 deletions scripts/run-linkage-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,185 @@
##===----------------------------------------------------------------------===##
set -eu

# Validate that we're running on Linux
if [[ "$(uname -s)" != "Linux" ]]; then
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--dry-run)
DRY_RUN=true
;;
-h|--help)
cat <<'EOF'
Usage: run-linkage-test.sh [--dry-run]

Verifies that the linkage test package and its source files cover every
runtime library product exported from swift-java, then (on Linux) builds
those executables and confirms none of them link libFoundation.so.

--dry-run Only run the imports-in-sync check.
Skips the Linux-only build + ldd steps.
EOF
exit 0
;;
*)
echo "Error: unknown argument: $arg" >&2
echo "Run with --help for usage." >&2
exit 1
;;
esac
done

# Validate that we're running on Linux (unless we're only doing the
# imports-in-sync check).
if ! $DRY_RUN && [[ "$(uname -s)" != "Linux" ]]; then
echo "Error: This script must be run on Linux. Current OS: $(uname -s)" >&2
echo "Hint: pass --dry-run to run the imports-in-sync check on any OS." >&2
exit 1
fi

echo "Detected JAVA_HOME=${JAVA_HOME}"
if ! $DRY_RUN; then
echo "Detected JAVA_HOME=${JAVA_HOME}"
fi

# Verify that Tests/LinkageTest/Package.swift and tests include all the runtime libraries.
# This is because if any of them accidentally pulls Foundation, that'd be bad.
#
# We inspect:
# - any library product whose name starts with "Java"
# - "SwiftJava"
# - "SwiftRuntimeFunctions"
# Anything else (tooling, docs, samples, `*Static` linkage variants) is
# ignored on purpose.
#
# The expected list is derived from `swift package describe --type json` on
# the root package so it stays honest as products are added or renamed.
verify_linkage_test_covers_runtime_products() {
local linkage_pkg="Tests/LinkageTest/Package.swift"

if [[ ! -f "Package.swift" || ! -f "$linkage_pkg" ]]; then
echo "Error: cannot find Package.swift or $linkage_pkg from $(pwd)" >&2
exit 1
fi

if ! command -v jq >/dev/null 2>&1; then
echo "Error: 'jq' is required to parse 'swift package describe --type json'." >&2
echo "Install it with your package manager (e.g. 'apt-get install -y jq') and re-run." >&2
exit 1
fi

local expected
expected=$(
swift package --disable-sandbox describe --type json \
| jq -r '
.products[]
| select(.type.library != null)
| .name
| select(startswith("Java") or . == "SwiftJava" or . == "SwiftRuntimeFunctions")
' \
| sort -u
)

if [[ -z "$expected" ]]; then
echo "Error: could not extract runtime library products from 'swift package describe'." >&2
echo "The parser in run-linkage-test.sh likely needs updating." >&2
exit 1
fi

# Actual list, between the markers in Tests/LinkageTest/Package.swift.
local actual
actual=$(
awk '
/RUNTIME_LIBRARY_PRODUCTS:START/ { collecting = 1; next }
/RUNTIME_LIBRARY_PRODUCTS:END/ { collecting = 0 }
collecting && match($0, /"[^"]+"/) {
name = substr($0, RSTART + 1, RLENGTH - 2)
print name
}
' "$linkage_pkg" | sort -u
)

if [[ -z "$actual" ]]; then
echo "Error: no entries found between RUNTIME_LIBRARY_PRODUCTS:START/END in $linkage_pkg." >&2
echo "The linkage test would silently under-cover the runtime libraries." >&2
exit 1
fi

local missing extra
missing=$(comm -23 <(printf '%s\n' "$expected") <(printf '%s\n' "$actual"))
extra=$(comm -13 <(printf '%s\n' "$expected") <(printf '%s\n' "$actual"))

if [[ -n "$missing" || -n "$extra" ]]; then
echo "Error: $linkage_pkg RUNTIME_LIBRARY_PRODUCTS list is out of sync with the root package." >&2
if [[ -n "$missing" ]]; then
echo " Missing from linkage test (add these):" >&2
printf '%s\n' "$missing" | sed 's/^/ /' >&2
fi
if [[ -n "$extra" ]]; then
echo " Present in linkage test but not a runtime library product:" >&2
printf '%s\n' "$extra" | sed 's/^/ /' >&2
fi
echo "Update the RUNTIME_LIBRARY_PRODUCTS list and re-run." >&2
exit 1
fi

# Every linkage-test source file that imports the runtime products must also stay in sync.
# Sources delimit their `import` block with `IMPORT_RUNTIME_LIBRARY_PRODUCTS:START` / `:END`.
local import_source
for import_source in \
Tests/LinkageTest/Sources/LinkageTest/main.swift \
Tests/LinkageTest/Sources/JExtractLinkageTest/main.swift
do
if [[ ! -f "$import_source" ]]; then
echo "Error: expected linkage-test source $import_source not found." >&2
exit 1
fi

local imported
imported=$(
awk '
/IMPORT_RUNTIME_LIBRARY_PRODUCTS:START/ { collecting = 1; next }
/IMPORT_RUNTIME_LIBRARY_PRODUCTS:END/ { collecting = 0 }
collecting && match($0, /^[[:space:]]*import[[:space:]]+[A-Za-z_][A-Za-z0-9_]*/) {
line = substr($0, RSTART, RLENGTH)
sub(/^[[:space:]]*import[[:space:]]+/, "", line)
print line
}
' "$import_source" | sort -u
)

if [[ -z "$imported" ]]; then
echo "Error: no imports found between IMPORT_RUNTIME_LIBRARY_PRODUCTS:START/END in $import_source." >&2
exit 1
fi

local src_missing src_extra
src_missing=$(comm -23 <(printf '%s\n' "$expected") <(printf '%s\n' "$imported"))
src_extra=$(comm -13 <(printf '%s\n' "$expected") <(printf '%s\n' "$imported"))

if [[ -n "$src_missing" || -n "$src_extra" ]]; then
echo "Error: imports in $import_source are out of sync with the root package." >&2
if [[ -n "$src_missing" ]]; then
echo " Missing imports (add these):" >&2
printf '%s\n' "$src_missing" | sed 's/^/ import /' >&2
fi
if [[ -n "$src_extra" ]]; then
echo " Imports not backed by a runtime library product:" >&2
printf '%s\n' "$src_extra" | sed 's/^/ import /' >&2
fi
echo "Update the import block between the IMPORT_RUNTIME_LIBRARY_PRODUCTS markers and re-run." >&2
exit 1
fi
done

echo "Linkage test covers all runtime library products:"
printf '%s\n' "$expected" | sed 's/^/ /'
}

verify_linkage_test_covers_runtime_products

if $DRY_RUN; then
echo "--dry-run: imports-in-sync check passed; skipping Linux build + ldd."
exit 0
fi

echo "Running on Linux - proceeding with linkage test..."

Expand Down
Loading