179 lines
8.2 KiB
Swift
179 lines
8.2 KiB
Swift
import Foundation
|
|
import SwiftFormat
|
|
import GObjectGeneratorCore
|
|
|
|
/// 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 GeneratorCLI {
|
|
/// 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(" generator --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)
|
|
var scaffolding = CodeGenerator.generateMonorepoScaffolding(
|
|
analysis: analysis, includeSmokeTarget: args.includeSmokeTarget)
|
|
|
|
// Preserve user-defined targets in Package.swift across generations
|
|
let existingPkgPath = outputRoot.appendingPathComponent("Package.swift")
|
|
let existingPkg = (try? String(contentsOf: existingPkgPath, encoding: .utf8)) ?? ""
|
|
if let pkgContent = scaffolding["Package.swift"] {
|
|
scaffolding["Package.swift"] = CodeGenerator.preserveUserTargets(
|
|
generated: pkgContent, existing: existingPkg)
|
|
}
|
|
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: generator --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")
|
|
}
|
|
}
|