252 lines
9.9 KiB
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) ]
|
|
)
|
|
|
|
"""
|
|
}
|
|
|
|
}
|