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
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ let package = Package(
dependencies: [
"SwiftJava"
],
exclude: ["swift-java.config"],
swiftSettings: [
.swiftLanguageMode(.v5),
.unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"], .when(platforms: [.macOS, .linux, .windows])),
Expand Down
20 changes: 20 additions & 0 deletions Sources/SwiftJavaDocumentation/Documentation.docc/Android.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,24 @@ let package = Package(
]
)
```

### Android SDK Availability

When wrapping the Android SDK (`android.jar`) you can provide the optional `--android-api-version-file` option to `swift-java wrap-java`.

This file contains availability information for Android APIs, which swift-java will take into account when generating the wrappers.
All APIs will therefore be annotated with their respective availability, expressed using Swift's `@available`:

```swift
#if compiler(>=6.3)
@available(Android 3 /* Cupcake */, *)
@available(Android, deprecated: 29, message: "Deprecated in Android API 29 /* Android 10 */")
#endif
@JavaClass("com.example.OldVersionedClass")
open class OldVersionedClass: JavaObject {
}
```

Annotations are generated both for "since", "deprecated" and "removed" attributes.

> Note: To use Android platform availability you must use at least Swift 6.3, which introduced the `Android` platform.
11 changes: 11 additions & 0 deletions Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ extension SwiftJava {

@Option(help: "If specified, a single Swift file will be generated containing all the generated code")
var singleSwiftFileOutput: String?

@Option(name: .customLong("android-api-version-file"), help: "Path to Android api-versions.xml for generating @available attributes based on API level data")
var androidAPIVersionFile: String?
}
}

Expand Down Expand Up @@ -141,6 +144,14 @@ extension SwiftJava.WrapJavaCommand {
// Swift-native implementations.
translator.swiftNativeImplementations = Set(swiftNativeImplementation)

// Load Android API version data if provided.
if let androidAPIVersionFile {
let url = URL(fileURLWithPath: androidAPIVersionFile)
let apiVersions = try AndroidAPIVersionsParser.parse(contentsOf: url, log: Self.log)
translator.androidAPIVersions = apiVersions
log.info("Loaded Android API versions: \(apiVersions.stats())")
}

// Note all of the dependent configurations.
for (swiftModuleName, dependentConfig) in dependentConfigs {
translator.addConfiguration(
Expand Down
9 changes: 9 additions & 0 deletions Sources/SwiftJavaToolLib/AndroidAPILevel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,13 @@ public enum AndroidAPILevel: Int {
case .CUR_DEVELOPMENT: "CUR_DEVELOPMENT"
}
}

/// Create from an optional string (e.g. an XML attribute value).
/// Returns `nil` if the string is `nil`, not a valid integer, or not a known API level.
public init?(_ string: String?) {
guard let string, let raw = Int(string) else {
return nil
}
self.init(rawValue: raw)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// Fully-qualified Java class name in dot format, e.g. `com.example.MyClass`.
package typealias FullyQualifiedClassName = String

/// JVM method descriptor combining method name with parameter and return type descriptors,
/// e.g. `getDisplayId()I` or `<init>(Ljava/lang/String;)V`.
package typealias JVMMethodDescriptor = String

/// Java field name, e.g. `"ACCEPT_HANDOVER"`.
package typealias FieldName = String

/// Version info for a single API element (class, method, or field)
/// as recorded in the Android SDK's `api-versions.xml`.
package struct AndroidAPIAvailability {
/// The API level at which this element was introduced.
package var since: AndroidAPILevel?
/// The API level at which this element was removed.
package var removed: AndroidAPILevel?
/// The API level at which this element was deprecated.
package var deprecated: AndroidAPILevel?

package init(since: AndroidAPILevel? = nil, removed: AndroidAPILevel? = nil, deprecated: AndroidAPILevel? = nil) {
self.since = since
self.removed = removed
self.deprecated = deprecated
}
}

/// Stores the parsed `api-versions.xml` data and provides query methods
/// for looking up version information by class, method, or field.
///
/// Class names are stored internally in dot format (e.g. `"android.Manifest$permission"`).
/// Query methods accept either slash or dot format and convert automatically.
package struct AndroidAPIVersions {
/// class name -> version info
var classVersions: [FullyQualifiedClassName: AndroidAPIAvailability] = [:]
/// class name -> (method descriptor -> version info)
var methodVersions: [FullyQualifiedClassName: [JVMMethodDescriptor: AndroidAPIAvailability]] = [:]
/// class name -> (field name -> version info)
var fieldVersions: [FullyQualifiedClassName: [FieldName: AndroidAPIAvailability]] = [:]

package init() {}

/// Query version info for a class.
package func versionInfo(forClass className: FullyQualifiedClassName) -> AndroidAPIAvailability? {
classVersions[Self.normalizeClassName(className)]
}

/// Query version info for a method within a class.
package func versionInfo(forClass className: FullyQualifiedClassName, methodDescriptor: JVMMethodDescriptor) -> AndroidAPIAvailability? {
methodVersions[Self.normalizeClassName(className)]?[methodDescriptor]
}

/// Query version info for a field within a class.
package func versionInfo(forClass className: FullyQualifiedClassName, fieldName: FieldName) -> AndroidAPIAvailability? {
fieldVersions[Self.normalizeClassName(className)]?[fieldName]
}

/// Statistics about the parsed data.
package func stats() -> Stats {
Stats(
classCount: classVersions.count,
methodCount: methodVersions.values.reduce(0) { $0 + $1.count },
fieldCount: fieldVersions.values.reduce(0) { $0 + $1.count }
)
}

package struct Stats: CustomStringConvertible {
package var classCount: Int
package var methodCount: Int
package var fieldCount: Int

public var description: String {
"\(classCount) classes, \(methodCount) methods, \(fieldCount) fields"
}
}

/// Normalize a class name to dot format, converting slashes if needed.
static func normalizeClassName(_ name: String) -> FullyQualifiedClassName {
name.replacing("/", with: ".")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
//===----------------------------------------------------------------------===//
//
// 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 Foundation
import Logging

#if canImport(FoundationXML)
import FoundationXML
#endif

/// Parses an Android `api-versions.xml` file (format version 3) into an ``AndroidAPIVersions``.

public final class AndroidAPIVersionsParser: _AndroidAPIVersionsParserBase, XMLParserDelegate {
private var result = AndroidAPIVersions()
private var currentClassName: String?
private var currentClassSince: AndroidAPILevel?
private var parseError: Error?

private let log: Logger

private init(log: Logger) {
self.log = log
super.init()
}

/// Parse an `api-versions.xml` file from the given URL.
package static func parse(contentsOf url: URL, log: Logger = .noop) throws -> AndroidAPIVersions {
let data = try Data(contentsOf: url)
return try parse(data: data, log: log)
}

/// Parse `api-versions.xml` from in-memory data.
package static func parse(data: Data, log: Logger = .noop) throws -> AndroidAPIVersions {
let handler = AndroidAPIVersionsParser(log: log)
let parser = XMLParser(data: data)
parser.delegate = handler
guard parser.parse() else {
if let error = handler.parseError {
throw error
}
throw parser.parserError ?? AndroidAPIVersionsParserError.unknownParseError
}
if let error = handler.parseError {
throw error
}
return handler.result
}

/// Parse `api-versions.xml` from a string.
package static func parse(string: String, log: Logger = .noop) throws -> AndroidAPIVersions {
let data = Data(string.utf8)
return try parse(data: data, log: log)
}

// ===== ------------------------------------------------------------------------
// MARK: - XMLParserDelegate

public func parser(
_ parser: XMLParser,
didStartElement elementName: String,
namespaceURI: String?,
qualifiedName: String?,
attributes attrs: [String: String]
) {
switch elementName {
case "api": parseAPI(attrs: attrs)
case "class": parseClass(attrs: attrs)
case "method": parseMethod(attrs: attrs)
case "field": parseField(attrs: attrs)
default: break // ignore <sdk>, <extends>, <implements>, etc.
}
}

private func parseAPI(attrs: [String: String]) {
if let versionStr = attrs["version"], versionStr != "3" {
log.warning("api-versions.xml has version '\(versionStr)', expected '3'. Parsing may be incomplete.")
}
}

private func parseClass(attrs: [String: String]) {
guard let name = attrs["name"] else { return }
let dotName = name.replacing("/", with: ".")
currentClassName = dotName
let since = AndroidAPILevel(attrs["since"])
currentClassSince = since
let info = AndroidAPIAvailability(
since: since,
removed: AndroidAPILevel(attrs["removed"]),
deprecated: AndroidAPILevel(attrs["deprecated"])
)
result.classVersions[dotName] = info
}

private func parseMethod(attrs: [String: String]) {
guard let className = currentClassName,
let name = attrs["name"]
else { return }
let info = AndroidAPIAvailability(
since: AndroidAPILevel(attrs["since"]) ?? currentClassSince,
removed: AndroidAPILevel(attrs["removed"]),
deprecated: AndroidAPILevel(attrs["deprecated"])
)
result.methodVersions[className, default: [:]][name] = info
}

private func parseField(attrs: [String: String]) {
guard let className = currentClassName,
let name = attrs["name"]
else { return }
let info = AndroidAPIAvailability(
since: AndroidAPILevel(attrs["since"]) ?? currentClassSince,
removed: AndroidAPILevel(attrs["removed"]),
deprecated: AndroidAPILevel(attrs["deprecated"])
)
result.fieldVersions[className, default: [:]][name] = info
}

public func parser(
_ parser: XMLParser,
didEndElement elementName: String,
namespaceURI: String?,
qualifiedName: String?
) {
if elementName == "class" {
currentClassName = nil
currentClassSince = nil
}
}

public func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) {
self.parseError = parseError
}
}

// ===== ------------------------------------------------------------------------
// MARK: - Errors

public enum AndroidAPIVersionsParserError: Error, CustomStringConvertible {
case unknownParseError

public var description: String {
switch self {
case .unknownParseError:
"Unknown error parsing api-versions.xml"
}
}
}

// ===== ------------------------------------------------------------------------
// MARK: - Platform base class

/// On Apple platforms `XMLParserDelegate` is an `@objc` protocol, so the class
/// inherits from `NSObject`. On Linux (swift-corelibs-foundation) the protocol
/// is a plain Swift protocol and no base class is needed.
#if canImport(ObjectiveC)
public class _AndroidAPIVersionsParserBase: NSObject {}
#else
public class _AndroidAPIVersionsParserBase {}
#endif

// ===== ------------------------------------------------------------------------
// MARK: - Logger extensions

extension Logger {
/// A logger that silently discards all log messages.
public static var noop: Logger {
Logger(label: "noop", factory: { _ in SwiftLogNoOpLogHandler() })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ extension JavaClassFileReader {
let key: String
if includeDescriptor {
let descriptor = utf8Constants[descriptorIndex] ?? ""
key = "\(name):\(descriptor)"
key = "\(name)\(descriptor)"
} else {
key = name
}
Expand Down
Loading
Loading