1
0
Fork 0
gobject-generator/Sources/swift-gtk-gen/Main.swift
Brendan Szymanski 57ff84bce5 Re-point plugins and re-enable lint gate (E6/E7)
E6: remove build-tool plugin (architecturally incompatible with the
monorepo-only CLI); rewrite the command plugin as a thin pass-through to
swift-gtk-gen.

E7: add swift-format as a dependency, format generated output in-process
before writeIfChanged, re-enable swift format lint --strict in
compile-gate.sh. CamelCase renderer identifiers (enum/bitfield cases,
signal trampoline names, _sgtk_* helpers, __result/__ptr temporaries,
hardcoded G_TYPE_* GValue constants) and drop generated block comments to
reach zero lint findings across all 6 tiers.
2026-07-20 23:44:49 -04:00

171 lines
7.7 KiB
Swift

import Foundation
import SwiftFormat
import SwiftGtkGenCore
/// Command-line interface for the GIR-to-Swift binding generator.
///
/// Reads a monorepo TOML config, parses every listed GIR file, plans and
/// renders Swift wrappers, and writes the result plus C bridge scaffolding
/// to the output directory. This is the subprocess entry point invoked by
/// both the build tool plugin and the command plugin.
@main
struct SwiftGtkGenCLI {
/// Program entry point. Parses arguments, generates code, and writes output.
static func main() {
let args = parseArguments()
if args.showHelp {
printHelp()
return
}
guard !args.monorepoConfigTOML.isEmpty else {
print("Error: --monorepo-config is required")
print(" swift-gtk-gen --monorepo-config configs/tier1.toml --output /tmp/out")
exit(1)
}
guard FileManager.default.fileExists(atPath: args.monorepoConfigTOML) else {
print("Error: monorepo config file not found: \(args.monorepoConfigTOML)")
exit(1)
}
do {
let tomlContent = try String(contentsOfFile: args.monorepoConfigTOML, encoding: .utf8)
let toml = try TOMLReader.parse(tomlContent)
let monorepoConfig = try MonorepoConfig.from(toml: toml, defaultOutputDir: args.output)
let monorepoAnalyzer = MultiPackageAnalyzer(config: monorepoConfig)
let analysis = try monorepoAnalyzer.analyze()
let outputRoot = URL(fileURLWithPath: monorepoConfig.outputDir)
// Plan engine (the only engine): build registry, plan, render.
let registry = TypeRegistry(repositories: analysis.repositories, directDependencies: analysis.directDependencies)
let modulePlans = planModules(analysis: analysis, registry: registry)
var outputs: [String: [String: String]] = [:]
let formatConfiguration: Configuration
if !args.formatConfigPath.isEmpty {
formatConfiguration = try Configuration(contentsOf: URL(fileURLWithPath: args.formatConfigPath))
} else {
formatConfiguration = Configuration()
}
let formatter = SwiftFormatter(configuration: formatConfiguration)
func formatSwift(_ content: String, fileName: String) -> String {
guard fileName.hasSuffix(".swift") else { return content }
var formatted = ""
do {
try formatter.format(
source: content, assumingFileURL: URL(fileURLWithPath: fileName),
selection: .infinite, to: &formatted)
} catch {
return content
}
return formatted
}
for (moduleName, plan) in modulePlans {
outputs[moduleName] = renderModule(plan)
}
// Write generated source files
for (moduleName, sourceFiles) in outputs {
let sourcesDir = outputRoot
.appendingPathComponent("Sources/\(moduleName)/Generated")
try FileManager.default.createDirectory(at: sourcesDir, withIntermediateDirectories: true)
for (fileName, content) in sourceFiles.sorted(by: { $0.key < $1.key }) {
let fileURL = sourcesDir.appendingPathComponent(fileName)
try writeIfChanged(formatSwift(content, fileName: fileName), to: fileURL)
}
}
// Write scaffolding (Package.swift, C module maps, umbrella headers)
let scaffolding = CodeGenerator.generateMonorepoScaffolding(
analysis: analysis, includeSmokeTarget: args.includeSmokeTarget)
for (relativePath, content) in scaffolding.sorted(by: { $0.key < $1.key }) {
let fileURL = outputRoot.appendingPathComponent(relativePath)
try FileManager.default.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true)
try writeIfChanged(formatSwift(content, fileName: relativePath), to: fileURL)
}
// Write skip reports
if args.emitSkipReport {
let reportsDir = outputRoot.appendingPathComponent("skip-reports")
try FileManager.default.createDirectory(at: reportsDir, withIntermediateDirectories: true)
var summary: [String: CoverageStats] = [:]
for (moduleName, plan) in modulePlans {
let report = plan.skipReport
let fileURL = reportsDir.appendingPathComponent("\(moduleName).json")
try writeIfChanged(String(decoding: report.jsonData(), as: UTF8.self) + "\n", to: fileURL)
summary[moduleName] = report.stats
}
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
let summaryData = try encoder.encode(summary)
let summaryURL = outputRoot.appendingPathComponent("coverage-summary.json")
try writeIfChanged(String(decoding: summaryData, as: UTF8.self) + "\n", to: summaryURL)
}
} catch {
print("Error: \(error)")
exit(1)
}
}
/// Writes `content` to `fileURL` only when the on-disk content differs.
///
/// Preserves file modification timestamps for unchanged outputs so that
/// incremental `swift build` runs over generated packages stay fast.
static func writeIfChanged(_ content: String, to fileURL: URL) throws {
if let existing = try? String(contentsOf: fileURL, encoding: .utf8), existing == content {
return
}
try content.write(to: fileURL, atomically: true, encoding: .utf8)
print("Generated \(fileURL.path)")
}
/// Parsed command-line arguments for the GIR generator CLI.
struct CLIArgs {
var showHelp: Bool = false
var monorepoConfigTOML: String = ""
var output: String = "."
var emitSkipReport: Bool = false
var includeSmokeTarget: Bool = false
var formatConfigPath: String = ""
}
/// Parses command-line arguments.
static func parseArguments() -> CLIArgs {
var args = Array(CommandLine.arguments.dropFirst())
var cli = CLIArgs()
while let flag = args.first {
args.removeFirst()
switch flag {
case "--help", "-h":
cli.showHelp = true
case "--monorepo-config":
cli.monorepoConfigTOML = args.isEmpty ? "" : args.removeFirst()
case "--output":
cli.output = args.isEmpty ? "." : args.removeFirst()
case "--skip-report":
cli.emitSkipReport = true
case "--smoke-target":
cli.includeSmokeTarget = true
case "--format-config":
cli.formatConfigPath = args.isEmpty ? "" : args.removeFirst()
default:
break
}
}
return cli
}
/// Prints usage information to stdout.
static func printHelp() {
print("Usage: swift-gtk-gen --monorepo-config PATH [options]")
print(" --monorepo-config PATH Monorepo TOML config (required)")
print(" --output DIR Output directory (default: .)")
print(" --skip-report Write skip-reports/<Module>.json and coverage-summary.json")
print(" --smoke-target Add a SmokeTests test target to the generated Package.swift")
print(" --format-config PATH .swift-format config to format generated output with")
print(" --help, -h Show this help")
}
}