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 regression/tier1/config.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/.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") } }