1
0
Fork 0
gobject-generator/Sources/SwiftGtkGenCore/CodeGen+Scaffolding.swift

252 lines
9.9 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
let umbrellaHeader = cHeader.isEmpty
? "#include <\(lowerModName)/\(lowerModName).h>"
: "#include <\(cHeader)>"
// 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"] = 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)"
let pkgConfigName = analysis.repositories[name]?.packageName ?? ""
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) ]
)
"""
}
}