Skip to content

Commit e0f3745

Browse files
sidepelicanktoso
andauthored
jextract: Evaluate IfConfigDecl and add --static-build-config option (#671)
Co-authored-by: Konrad `ktoso` Malawski <konrad.malawski@project13.pl>
1 parent 5be4f45 commit e0f3745

File tree

16 files changed

+560
-18
lines changed

16 files changed

+560
-18
lines changed

Package.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,12 @@ let package = Package(
118118
],
119119
dependencies: [
120120
swiftJavaJNICoreDep,
121-
.package(url: "https://github.com/swiftlang/swift-syntax", from: "602.0.0"),
121+
.package(url: "https://github.com/swiftlang/swift-syntax", from: "603.0.0"),
122122
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"),
123123
.package(url: "https://github.com/apple/swift-system", from: "1.4.0"),
124124
.package(url: "https://github.com/apple/swift-log", from: "1.2.0"),
125125
.package(url: "https://github.com/apple/swift-collections", .upToNextMinor(from: "1.3.0")), // primarily for ordered collections
126-
.package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.2.1", traits: ["SubprocessFoundation"]),
126+
.package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.4.0", traits: ["SubprocessFoundation"]),
127127

128128
// Benchmarking
129129
.package(url: "https://github.com/ordo-one/package-benchmark", .upToNextMajor(from: "1.4.0")),
@@ -320,6 +320,7 @@ let package = Package(
320320
dependencies: [
321321
.product(name: "SwiftBasicFormat", package: "swift-syntax"),
322322
.product(name: "SwiftLexicalLookup", package: "swift-syntax"),
323+
.product(name: "SwiftIfConfig", package: "swift-syntax"),
323324
.product(name: "SwiftSyntax", package: "swift-syntax"),
324325
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
325326
.product(name: "ArgumentParser", package: "swift-argument-parser"),
@@ -331,6 +332,25 @@ let package = Package(
331332
],
332333
swiftSettings: [
333334
.swiftLanguageMode(.v5)
335+
],
336+
plugins: [
337+
.plugin(name: "_StaticBuildConfigPlugin")
338+
]
339+
),
340+
341+
.executableTarget(
342+
name: "StaticBuildConfigPluginExecutable",
343+
dependencies: [
344+
.product(name: "Subprocess", package: "swift-subprocess"),
345+
.product(name: "SwiftIfConfig", package: "swift-syntax"),
346+
]
347+
),
348+
349+
.plugin(
350+
name: "_StaticBuildConfigPlugin",
351+
capability: .buildTool(),
352+
dependencies: [
353+
"StaticBuildConfigPluginExecutable"
334354
]
335355
),
336356

Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
4646
// The name of the configuration file SwiftJava.config from the target for
4747
// which we are generating Swift wrappers for Java classes.
4848
let configFile = sourceDir.appending(path: "swift-java.config")
49-
let configuration = try readConfiguration(sourceDir: sourceDir)
49+
let configuration = try readConfiguration(configPath: configFile)
5050

5151
// We use the the usual maven-style structure of "src/[generated|main|test]/java/..."
5252
// that is common in JVM ecosystem
@@ -71,6 +71,13 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
7171
// We'll have to make up some caching inside the tool so we don't re-parse files which have not changed etc.
7272
]
7373

74+
if let staticBuildConfig = configuration?.staticBuildConfigurationFile {
75+
guard let resolvedURL = URL(string: staticBuildConfig, relativeTo: configFile) else {
76+
fatalError("Could not resolve 'staticBuildConfigurationFile' url: \(staticBuildConfig)")
77+
}
78+
arguments += ["--static-build-config", resolvedURL.absoluteURL.path(percentEncoded: false)]
79+
}
80+
7481
let dependentConfigFilesArguments = dependentConfigFiles.flatMap { moduleAndConfigFile in
7582
let (moduleName, configFile) = moduleAndConfigFile
7683
return [
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
import PackagePlugin
17+
18+
@main
19+
struct _StaticBuildConfigPlugin: BuildToolPlugin {
20+
func createBuildCommands(context: PluginContext, target: any Target) async throws -> [Command] {
21+
let outSwift = context.pluginWorkDirectoryURL.appending(path: "StaticBuildConfiguration+embedded.swift")
22+
return [
23+
.buildCommand(
24+
displayName: "Run -print-static-build-config",
25+
executable: try context.tool(named: "StaticBuildConfigPluginExecutable").url,
26+
arguments: [outSwift.absoluteURL.path(percentEncoded: false)],
27+
environment: [:],
28+
inputFiles: [],
29+
outputFiles: [outSwift.absoluteURL]
30+
)
31+
]
32+
}
33+
}

Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,10 @@ extension DeclSyntaxProtocol {
229229
} else {
230230
"var"
231231
}
232+
case .unexpectedCodeDecl(let node):
233+
node.trimmedDescription
232234
case .usingDecl(let node):
233-
node.nameForDebug
235+
node.trimmedDescription
234236
}
235237
}
236238

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
import SwiftIfConfig
17+
import SwiftSyntax
18+
19+
/// A default, fixed build configuration during static analysis for interface extraction.
20+
struct JExtractDefaultBuildConfiguration: BuildConfiguration {
21+
static let shared = JExtractDefaultBuildConfiguration()
22+
23+
private var base: StaticBuildConfiguration
24+
25+
init() {
26+
let decoder = JSONDecoder()
27+
do {
28+
base = try decoder.decode(StaticBuildConfiguration.self, from: StaticBuildConfiguration.embedded)
29+
} catch {
30+
fatalError("Embedded StaticBuildConfiguration is broken! data: \(String(data: StaticBuildConfiguration.embedded, encoding: .utf8) ?? "")")
31+
}
32+
}
33+
34+
func isCustomConditionSet(name: String) throws -> Bool {
35+
base.isCustomConditionSet(name: name)
36+
}
37+
38+
func hasFeature(name: String) throws -> Bool {
39+
base.hasFeature(name: name)
40+
}
41+
42+
func hasAttribute(name: String) throws -> Bool {
43+
base.hasAttribute(name: name)
44+
}
45+
46+
func canImport(importPath: [(TokenSyntax, String)], version: CanImportVersion) throws -> Bool {
47+
try base.canImport(importPath: importPath, version: version)
48+
}
49+
50+
func isActiveTargetOS(name: String) throws -> Bool {
51+
true
52+
}
53+
54+
func isActiveTargetArchitecture(name: String) throws -> Bool {
55+
true
56+
}
57+
58+
func isActiveTargetEnvironment(name: String) throws -> Bool {
59+
true
60+
}
61+
62+
func isActiveTargetRuntime(name: String) throws -> Bool {
63+
true
64+
}
65+
66+
func isActiveTargetPointerAuthentication(name: String) throws -> Bool {
67+
true
68+
}
69+
70+
func isActiveTargetObjectFormat(name: String) throws -> Bool {
71+
true
72+
}
73+
74+
var targetPointerBitWidth: Int {
75+
base.targetPointerBitWidth
76+
}
77+
78+
var targetAtomicBitWidths: [Int] {
79+
base.targetAtomicBitWidths
80+
}
81+
82+
var endianness: Endianness {
83+
base.endianness
84+
}
85+
86+
var languageVersion: VersionTuple {
87+
base.languageVersion
88+
}
89+
90+
var compilerVersion: VersionTuple {
91+
base.compilerVersion
92+
}
93+
}
94+
95+
extension BuildConfiguration where Self == JExtractDefaultBuildConfiguration {
96+
static var jextractDefault: JExtractDefaultBuildConfiguration {
97+
.shared
98+
}
99+
}

Sources/JExtractSwiftLib/Swift2Java.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ public struct SwiftToJava {
3434
}
3535

3636
let translator = Swift2JavaTranslator(config: config)
37-
translator.log.logLevel = config.logLevel ?? .info
3837
let log = translator.log
3938

4039
if config.javaPackage == nil || config.javaPackage!.isEmpty {

Sources/JExtractSwiftLib/Swift2JavaTranslator.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import Foundation
1616
import SwiftBasicFormat
17+
import SwiftIfConfig
1718
import SwiftJavaConfigurationShared
1819
import SwiftJavaJNICore
1920
import SwiftParser
@@ -27,6 +28,9 @@ public final class Swift2JavaTranslator {
2728

2829
let config: Configuration
2930

31+
/// The build configuration used to resolve #if conditional compilation blocks.
32+
let buildConfig: any BuildConfiguration
33+
3034
/// The name of the Swift module being translated.
3135
let swiftModuleName: String
3236

@@ -70,6 +74,19 @@ public final class Swift2JavaTranslator {
7074
self.log = Logger(label: "translator", logLevel: config.logLevel ?? .info)
7175
self.config = config
7276
self.swiftModuleName = swiftModule
77+
78+
if let staticBuildConfigPath = config.staticBuildConfigurationFile {
79+
do {
80+
let data = try Data(contentsOf: URL(fileURLWithPath: staticBuildConfigPath))
81+
let decoder = JSONDecoder()
82+
self.buildConfig = try decoder.decode(StaticBuildConfiguration.self, from: data)
83+
self.log.info("Using custom static build configuration from: \(staticBuildConfigPath)")
84+
} catch {
85+
fatalError("Failed to load static build configuration from '\(staticBuildConfigPath)': \(error)")
86+
}
87+
} else {
88+
self.buildConfig = .jextractDefault
89+
}
7390
}
7491
}
7592

@@ -152,6 +169,7 @@ extension Swift2JavaTranslator {
152169
moduleName: self.swiftModuleName,
153170
inputs + [dependenciesSource],
154171
config: self.config,
172+
buildConfig: self.buildConfig,
155173
log: self.log,
156174
)
157175
self.lookupContext = SwiftTypeLookupContext(symbolTable: symbolTable)

Sources/JExtractSwiftLib/Swift2JavaVisitor.swift

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import Foundation
16+
import SwiftIfConfig
1617
import SwiftJavaConfigurationShared
1718
import SwiftParser
1819
import SwiftSyntax
@@ -70,7 +71,8 @@ final class Swift2JavaVisitor {
7071
self.visit(subscriptDecl: node, in: parent)
7172
case .enumCaseDecl(let node):
7273
self.visit(enumCaseDecl: node, in: parent)
73-
74+
case .ifConfigDecl(let node):
75+
self.visit(ifConfigDecl: node, in: parent, sourceFilePath: sourceFilePath)
7476
default:
7577
break
7678
}
@@ -366,6 +368,30 @@ final class Swift2JavaVisitor {
366368
}
367369
}
368370

371+
private func visit(
372+
ifConfigDecl node: IfConfigDeclSyntax,
373+
in parent: ImportedNominalType?,
374+
sourceFilePath: String
375+
) {
376+
let (clause, _) = node.activeClause(in: translator.buildConfig)
377+
if let clause, let elements = clause.elements {
378+
switch elements {
379+
case .statements(let codeBlock):
380+
for codeItem in codeBlock {
381+
if let declNode = codeItem.item.as(DeclSyntax.self) {
382+
self.visit(decl: declNode, in: parent, sourceFilePath: sourceFilePath)
383+
}
384+
}
385+
case .decls(let memberBlock):
386+
for memberItem in memberBlock {
387+
self.visit(decl: memberItem.decl, in: parent, sourceFilePath: sourceFilePath)
388+
}
389+
default:
390+
break
391+
}
392+
}
393+
}
394+
369395
private func importAccessor(
370396
from node: DeclSyntax,
371397
in typeContext: ImportedNominalType?,

0 commit comments

Comments
 (0)