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 `` element, and external library link flags /// from `` elements and `config.externalLibraries`. /// /// - Parameters: /// - config: The generation configuration (uses `library` and /// `externalLibraries` fields). /// - repository: The parsed GIR repository containing namespace metadata, /// ``, ``, and `` 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 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 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 `` 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 " } // 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 // ) 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[.. 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 ] ) """ } }