1
0
Fork 0
gobject-generator/Sources/GObjectGeneratorCore/CodeGen+Scaffolding.swift
Brendan Szymanski 917cd29646 Define __GI_SCANNER__ in generated umbrella headers
Avoids Clang module conflicts when a NEON/SIMD-gated system header (e.g. Graphene's graphene_simd4f_t backend) is reachable from two different .systemLibrary modules on aarch64 Linux: one pulls in the real _Builtin_intrinsics.arm.acle module, the other re-parses the same header textually, and Clang rejects the resulting conflicting uint32_t/__fsid_t redeclarations. __GI_SCANNER__ is the escape hatch these libraries already ship for exactly this class of consumer (a declaration-only binding generator, same as gobject-introspection's own scanner).
2026-08-12 21:24:25 -04:00

285 lines
12 KiB
Swift

import Foundation
extension CodeGenerator {
// MARK: - Package Scaffolding Generation
/// Generates the package scaffolding files (Package.swift, module map, umbrella header)
/// for a self-contained SwiftPM wrapper package.
///
/// Returns a dictionary mapping relative file paths (e.g. `"Package.swift"`,
/// `"Sources/CGtk/module.modulemap"`) to their textual content. The library
/// name in `config.library` determines the package name, the C interop target
/// name (prefixed with `C`). Link names and pkg-config names are derived from
/// the repository's namespace `shared-library` attribute, the umbrella header
/// from the repository's `<c:include>` element, and external library link flags
/// from `<include>` elements and `config.externalLibraries`.
///
/// - Parameters:
/// - config: The generation configuration (uses `library` and
/// `externalLibraries` fields).
/// - repository: The parsed GIR repository containing namespace metadata,
/// `<c:include>`, `<include>`, and `<package>` values.
/// - Returns: A dictionary of relative path file content.
@available(*, deprecated, message: "Use generateMonorepoScaffolding(analysis:) for monorepo packages.")
public static func generatePackageScaffolding(config: GenerationConfig, repository: Repository) -> [String: String] {
var files: [String: String] = [:]
let cName = "C\(config.library)"
let lowerName = config.library.lowercased()
// Extract namespace data from the repository
let ns = repository.namespaces.first { $0.name == config.library }
let sharedLibrary = ns?.cSharedLibrary ?? ""
// Parse shared-library to derive all link names
// shared-library format: "libgtk-4.so.1" or "libgio-2.0.so.0,libglib-2.0.so.0"
let libraries = sharedLibrary.split(separator: ",").map(String.init)
let allLinkNames = libraries.compactMap { extractLinkName(from: $0) }
let lowerLibName = config.library.lowercased()
// Pick the best primary link: prefer the one matching the library name
let linkName = allLinkNames.first { $0.lowercased().contains(lowerLibName) }
?? allLinkNames.first
?? lowerName
// Use <package> name as pkgConfig when available (it matches the system .pc file),
// otherwise fall back to the derived link name
let pkgConfigName = repository.packageName.isEmpty ? linkName : repository.packageName
// Derive umbrella header from repository <c:include>
let cHeader = repository.cHeaderPath
let umbrellaHeader = cHeader.isEmpty
? "#include <\(lowerName)/\(lowerName).h>"
: "#include <\(cHeader)>"
// Build external link names from GIR dependencies + config
var externalLinks: [String] = []
for linkName in repository.includedLibraryLinks {
if !externalLinks.contains(linkName) {
externalLinks.append(linkName)
}
}
for ext in config.externalLibraries {
let el = ext.lowercased()
if !externalLinks.contains(el) {
externalLinks.append(el)
}
}
// Package.swift
let packageSwift = """
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "\(config.library)",
platforms: [.macOS(.v14)],
products: [
.library(
name: "\(config.library)",
targets: ["\(config.library)"]
)
],
targets: [
.target(
name: "\(config.library)",
dependencies: ["\(cName)"],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency=complete"),
.defaultIsolation(MainActor.self),
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
]
),
.systemLibrary(
name: "\(cName)",
path: "Sources/\(cName)",
pkgConfig: "\(pkgConfigName)"
),
]
)
"""
files["Package.swift"] = packageSwift
// Module map: use the generated umbrella header which includes the system header
let linkLines = (allLinkNames + externalLinks).map { " link \"\($0)\"" }.joined(separator: "\n")
let moduleMap = """
module \(cName) [system] {
header "\(cName).h"
\(linkLines)
}
"""
files["Sources/\(cName)/module.modulemap"] = moduleMap
files["Sources/\(cName)/\(cName).h"] = umbrellaHeader + "\n"
return files
}
// MARK: - Monorepo Package Scaffolding Generation
/// Generates all scaffolding files for the monorepo: Package.swift,
/// C bridge module maps and umbrella headers, and re-export files.
///
/// - Parameter analysis: The resolved multi-package analysis.
/// - Returns: A dictionary of relative path file content.
public static func generateMonorepoScaffolding(analysis: MultiPackageAnalysis, includeSmokeTarget: Bool = false) -> [String: String] {
var files: [String: String] = [:]
// Re-export umbrella files
let umbrellas = generateReexportUmbrellas(analysis: analysis)
for (path, content) in umbrellas {
files[path] = content
}
// C bridge files
for (moduleName, repo) in analysis.repositories {
let cName = "C\(moduleName)"
let lowerModName = moduleName.lowercased()
let cHeader = repo.cHeaderPath
var umbrellaHeader = cHeader.isEmpty
? "#include <\(lowerModName)/\(lowerModName).h>"
: "#include <\(cHeader)>"
// GTK4's own `<c:include name="gtk/gtk.h"/>` does not cover the
// `gtk/gtkunixprint.h` header that must be included
// explicitly, even though the GIR declares them as ordinary Gtk
// namespace members. Root-cause fix, not a skip: the C symbols
// are real and exported by libgtk-4.so; only the umbrella header
// GTK's own GIR points at omits them.
if moduleName == "Gtk" {
umbrellaHeader += "\n#include <gtk/gtkunixprint.h>"
}
// Every umbrella header defines `__GI_SCANNER__` before pulling in
// the system header. GLib, GTK, and Graphene all gate architecture-
// specific `static inline` fast paths (most notably Graphene's
// SSE/ARM-NEON `graphene_simd4f_t` backend) behind `#ifndef
// __GI_SCANNER__`, falling back to a portable scalar/definition-only
// path when it is set - the same escape hatch upstream ships for
// gobject-introspection's scanner, which (like Clang's ClangImporter
// here) only needs declarations, never compiled SIMD bodies. Without
// it, a NEON-using header reachable from two different `.systemLibrary`
// modules (e.g. both CGraphene and CGsk textually include
// <graphene-simd4f.h>) is compiled with the real Clang builtin
// `_Builtin_intrinsics.arm.acle` module in one and not the other,
// and Clang's module visibility rules reject the resulting
// conflicting `uint32_t`/`__fsid_t` redeclarations on aarch64 -
// reproducible on every aarch64 Linux target, not distro-specific.
// No `link` directives: the `.systemLibrary` target carries
// `pkgConfig:`, so pkg-config `--libs` supplies the exact linker
// flags. Emitting `link "glib-2"` here both duplicates that and
// gets the name wrong (the library is `glib-2.0`), which breaks
// linking of any executable (e.g. the smoke-test runner).
let moduleMap = """
module \(cName) [system] {
header "\(cName).h"
}
"""
files["Sources/\(cName)/module.modulemap"] = moduleMap
files["Sources/\(cName)/\(cName).h"] = "#define __GI_SCANNER__ 1\n" + umbrellaHeader + "\n"
}
// Monorepo Package.swift
let packageSwift = generateMonorepoPackage(analysis: analysis, includeSmokeTarget: includeSmokeTarget)
files["Package.swift"] = packageSwift
return files
}
/// Extracts link name from a shared-library string like "libgtk-4.so.1" -> "gtk-4".
/// Used by the single-GIR (`generatePackageScaffolding`) path only.
private static func extractLinkName(from sharedLib: String) -> String? {
guard sharedLib.hasPrefix("lib") else { return nil }
let withoutLib = String(sharedLib.dropFirst(3))
guard let soRange = withoutLib.range(of: ".so") else { return withoutLib }
return String(withoutLib[..<soRange.lowerBound])
}
/// Generates the single monorepo Package.swift containing all targets.
private static func generateMonorepoPackage(analysis: MultiPackageAnalysis, includeSmokeTarget: Bool = false) -> String {
let moduleNames = analysis.repositories.keys.sorted()
// Products
var products = ""
for name in moduleNames {
products += " .library(name: \"\(name)\", targets: [\"\(name)\"]),\n"
}
// C system library targets
var cTargets = ""
for name in moduleNames {
let cName = "C\(name)"
// Gtk needs `gtk4-unix-print`'s cflags (a strict superset of `gtk4`'s,
// adding only the `-I.../unix-print` search path the umbrella
// header above pulls `gtk/gtkunixprint.h` from) same libs, same .so.
var pkgConfigName = analysis.repositories[name]?.packageName ?? ""
if name == "Gtk" && pkgConfigName == "gtk4" { pkgConfigName = "gtk4-unix-print" }
let pkgConfigArg = pkgConfigName.isEmpty
? ""
: ", pkgConfig: \"\(pkgConfigName)\", providers: [.apt([\"\(pkgConfigName)\"]), .brew([\"\(pkgConfigName)\"])]"
cTargets += " .systemLibrary(name: \"\(cName)\", path: \"Sources/\(cName)\"\(pkgConfigArg)),\n"
}
// Swift wrapper targets
var swiftTargets = ""
for name in moduleNames {
let cName = "C\(name)"
let dirDeps = analysis.directDependencies[name] ?? []
var depList = "\"\(cName)\""
for dep in dirDeps.sorted() {
depList += ", \"\(dep)\""
}
swiftTargets += """
.target(
name: "\(name)",
dependencies: [\(depList)],
swiftSettings: swiftSettings
),
"""
}
// Optional smoke-test target depending on every generated module, used
// by scripts/smoke-test.sh to exercise the bindings against the real C
// libraries at runtime.
var smokeTarget = ""
if includeSmokeTarget {
let deps = moduleNames.map { "\"\($0)\"" }.joined(separator: ", ")
smokeTarget = """
.testTarget(
name: "SmokeTests",
dependencies: [\(deps)],
path: "Tests/SmokeTests",
swiftSettings: swiftSettings
),
"""
}
return """
// swift-tools-version: 6.2
import PackageDescription
let swiftSettings: [SwiftSetting] = [
.enableExperimentalFeature("StrictConcurrency=complete"),
.defaultIsolation(MainActor.self),
]
let package = Package(
name: "gtk-swift",
platforms: [.macOS(.v14)],
products: [
\(products) ],
targets: [
\(cTargets)
\(swiftTargets)\(smokeTarget)
// === USER TARGETS — preserved across generations ===
// BEGIN_USER_TARGETS
// END_USER_TARGETS
]
)
"""
}
}