diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..de1bf61 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,51 @@ +{ + "originHash" : "624f276a58a2026f3ee556b257210a71bf3544b029d116f439f19c89bba5fee4", + "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "6a52f3251125d74daf04fcbd5e6f08a75d074382", + "version" : "1.8.2" + } + }, + { + "identity" : "swift-cmark", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-cmark.git", + "state" : { + "revision" : "924936d0427cb25a61169739a7660230bffa6ea6", + "version" : "0.8.0" + } + }, + { + "identity" : "swift-format", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-format.git", + "state" : { + "branch" : "release/6.3", + "revision" : "bd706100808e11192661eac5e12255c020005555" + } + }, + { + "identity" : "swift-markdown", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-markdown.git", + "state" : { + "revision" : "3c6f9523da3a1ec2fd829673e472d95b8097a3b8", + "version" : "0.8.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "branch" : "release/6.3", + "revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index 3a3779f..051fb89 100644 --- a/Package.swift +++ b/Package.swift @@ -17,9 +17,11 @@ let package = Package( platforms: [.macOS(.v14)], products: [ .executable(name: "swift-gtk-gen", targets: ["swift-gtk-gen"]), - .plugin(name: "SwiftGtkGenBuildPlugin", targets: ["SwiftGtkGenBuildPlugin"]), .plugin(name: "SwiftGtkGenCommandPlugin", targets: ["SwiftGtkGenCommandPlugin"]), ], + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-format.git", branch: "release/6.3") + ], targets: [ .target( name: "SwiftGtkGenCore", @@ -28,15 +30,12 @@ let package = Package( ), .executableTarget( name: "swift-gtk-gen", - dependencies: ["SwiftGtkGenCore"], + dependencies: [ + "SwiftGtkGenCore", + .product(name: "SwiftFormat", package: "swift-format"), + ], swiftSettings: concurrencySettings ), - .plugin( - name: "SwiftGtkGenBuildPlugin", - capability: .buildTool(), - dependencies: ["swift-gtk-gen"], - path: "Sources/SwiftGtkGenBuildPlugin" - ), .plugin( name: "SwiftGtkGenCommandPlugin", capability: .command( diff --git a/Sources/SwiftGtkGenBuildPlugin/BuildToolPlugin.swift b/Sources/SwiftGtkGenBuildPlugin/BuildToolPlugin.swift deleted file mode 100644 index bb24fda..0000000 --- a/Sources/SwiftGtkGenBuildPlugin/BuildToolPlugin.swift +++ /dev/null @@ -1,117 +0,0 @@ -import PackagePlugin -import Foundation - -/// SwiftPM build tool plugin that automatically generates Swift GIR bindings -/// during compilation. -/// -/// Uses a two-phase approach: Phase 1 lists expected outputs via -/// `swift-gtk-gen --list-output-files`, applying the same `config.toml` -/// filtering as Phase 2. The resulting file list is declared to SwiftPM so -/// incremental builds track only files that were actually produced. Targets -/// without a `config.toml` or `.gir` files are silently skipped. -@main -struct SwiftGtkGenBuildPlugin: BuildToolPlugin { - /// Creates build commands for GIR code generation for the given target. - /// - /// Returns a single `.buildCommand` that runs `swift-gtk-gen` with the - /// target's `config.toml` and GIR files as inputs. Phase 1 discovers - /// expected output files via `--list-output-files`. If the target has - /// no `config.toml` or no `.gir` files, returns an empty array. - /// - /// - Parameters: - /// - context: The plugin context providing access to tools and work directories. - /// - target: The target to generate bindings for. - /// - Returns: An array of build commands (empty if no GIR files present). - func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { - let targetDir = target.directoryURL - let configURL = targetDir.appending(path: "config.toml") - - guard FileManager.default.fileExists(atPath: configURL.path()) else { - return [] - } - - let executable = try context.tool(named: "swift-gtk-gen") - let girFiles = findGIRFiles(in: targetDir) - guard let primaryGIR = girFiles.first else { return [] } - - let outputDir = context.pluginWorkDirectoryURL.appending(path: target.name) - - // Phase 1: List expected outputs (config-filtered to match Phase 2) - let listProcess = Process() - listProcess.executableURL = executable.url - listProcess.arguments = [ - "--gir-file", primaryGIR.path(), - "--config-toml", configURL.path(), - "--list-output-files", - ] - let listPipe = Pipe() - listProcess.standardOutput = listPipe - try listProcess.run() - listProcess.waitUntilExit() - - guard listProcess.terminationStatus == 0 else { - throw SwiftGtkGenPluginError.listingFailed(target.name) - } - - let listData = listPipe.fileHandleForReading.readDataToEndOfFile() - let listOutput = String(data: listData, encoding: .utf8) ?? "" - let outputFiles = listOutput - .components(separatedBy: "\n") - .filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty } - .map { outputDir.appending(path: $0) } - - return [ - .buildCommand( - displayName: "Generating \(target.name) GIR bindings...", - executable: executable.url, - arguments: [ - "--gir-file", primaryGIR.path(), - "--config-toml", configURL.path(), - "--output", outputDir.path(), - ], - inputFiles: [configURL, primaryGIR] + girFiles.dropFirst(), - outputFiles: outputFiles - ) - ] - } - - /// Recursively finds all `.gir` files in the given directory. - /// - /// Uses a directory enumerator to walk the file tree and collect URLs - /// whose path extension is `"gir"`. - /// - /// - Parameter directory: The root directory to search. - /// - Returns: An array of URLs pointing to discovered `.gir` files. - private func findGIRFiles(in directory: URL) -> [URL] { - guard let enumerator = FileManager.default.enumerator( - at: directory, includingPropertiesForKeys: [.isRegularFileKey] - ) else { return [] } - var girFiles: [URL] = [] - for case let fileURL as URL in enumerator { - if fileURL.pathExtension == "gir" { - girFiles.append(fileURL) - } - } - return girFiles - } -} - -/// Errors that can occur during build-tool-plugin GIR generation. -enum SwiftGtkGenPluginError: Error { - /// Output-file listing failed for the specified target name. - case listingFailed(String) - /// Generation failed for the specified target name. - case generationFailed(String) -} - -extension SwiftGtkGenPluginError: CustomStringConvertible { - /// A human-readable description of the error. - var description: String { - switch self { - case .listingFailed(let target): - return "GIR output listing failed for target: \(target)" - case .generationFailed(let target): - return "GIR generation failed for target: \(target)" - } - } -} diff --git a/Sources/SwiftGtkGenCommandPlugin/CommandPlugin.swift b/Sources/SwiftGtkGenCommandPlugin/CommandPlugin.swift index d01c264..c622df8 100644 --- a/Sources/SwiftGtkGenCommandPlugin/CommandPlugin.swift +++ b/Sources/SwiftGtkGenCommandPlugin/CommandPlugin.swift @@ -3,109 +3,74 @@ import Foundation /// SwiftPM command plugin for manual GIR binding regeneration. /// -/// Invoked via `swift package generate-gir-bindings`. Iterates over all -/// package targets, checks for a `config.toml` and `.gir` files, and runs -/// the `swift-gtk-gen` executable to produce or update generated Swift -/// sources in each target's `Generated/` directory. User-provided arguments -/// are passed through to the generator (e.g. `--generate Gtk.Widget`). +/// Invoked via `swift package generate-gir-bindings`. Forwards all +/// user-provided arguments directly to the `swift-gtk-gen` monorepo CLI, +/// defaulting `--output` to the package directory when the caller omits it. +/// The generator itself decides what to plan, render, and write from the +/// monorepo TOML config passed via `--monorepo-config`. @main struct SwiftGtkGenCommandPlugin: CommandPlugin { - /// Performs GIR code generation for all eligible targets in the package. + /// Runs `swift-gtk-gen` as a subprocess with the caller's arguments. /// - /// For each target that has a `config.toml` and at least one `.gir` file, - /// launches `swift-gtk-gen` as a subprocess with arguments derived from - /// the target's directory and metadata. Stdout and stderr are piped - /// through to the caller. Throws if any subprocess fails. + /// Stdout and stderr are piped through to the caller. Throws if the + /// subprocess exits non-zero. /// /// - Parameters: - /// - context: The plugin context providing access to tools and package targets. - /// - arguments: User-provided arguments passed through to the generator. + /// - context: The plugin context providing access to tools and the package directory. + /// - arguments: User-provided arguments forwarded to the generator, e.g. + /// `--monorepo-config configs/tier1.toml --format-config .swift-format`. func performCommand(context: PluginContext, arguments: [String]) async throws { + guard !arguments.isEmpty else { + print("Usage: swift package generate-gir-bindings --monorepo-config [--skip-report] [--smoke-target] [--format-config ]") + return + } + let executable = try context.tool(named: "swift-gtk-gen") - for target in context.package.targets { - let targetDir = target.directoryURL - let configURL = targetDir.appending(path: "config.toml") - - guard FileManager.default.fileExists(atPath: configURL.path()) else { - continue - } - - let girFiles = findGIRFiles(in: targetDir) - guard let primaryGIR = girFiles.first else { continue } - - let outputDir = targetDir.appending(path: "Generated") - - let process = Process() - process.executableURL = executable.url - - var procArgs: [String] = [ - "--gir-file", primaryGIR.path(), - "--config-toml", configURL.path(), - "--output", outputDir.path(), - ] - if !arguments.isEmpty { - procArgs.append(contentsOf: arguments) - } - - process.arguments = procArgs - - let stdoutPipe = Pipe() - let stderrPipe = Pipe() - process.standardOutput = stdoutPipe - process.standardError = stderrPipe - - try process.run() - process.waitUntilExit() - - let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile() - let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile() - if let stdoutStr = String(data: stdoutData, encoding: .utf8), !stdoutStr.isEmpty { - print(stdoutStr, terminator: "") - } - if let stderrStr = String(data: stderrData, encoding: .utf8), !stderrStr.isEmpty { - FileHandle.standardError.write(Data(stderrStr.utf8)) - } - - guard process.terminationStatus == 0 else { - throw SwiftGtkGenPluginError.generationFailed(target.name) - } + var procArgs = arguments + if !procArgs.contains("--output") { + procArgs.append(contentsOf: ["--output", context.package.directoryURL.path()]) } - } - /// Recursively finds all `.gir` files in the given directory. - /// - /// Uses a directory enumerator to walk the file tree and collect URLs - /// whose path extension is `"gir"`. - /// - /// - Parameter directory: The root directory to search. - /// - Returns: An array of URLs pointing to discovered `.gir` files. - private func findGIRFiles(in directory: URL) -> [URL] { - guard let enumerator = FileManager.default.enumerator( - at: directory, includingPropertiesForKeys: [.isRegularFileKey] - ) else { return [] } - var girFiles: [URL] = [] - for case let fileURL as URL in enumerator { - if fileURL.pathExtension == "gir" { - girFiles.append(fileURL) - } + let process = Process() + process.executableURL = executable.url + process.arguments = procArgs + + let stdoutPipe = Pipe() + let stderrPipe = Pipe() + process.standardOutput = stdoutPipe + process.standardError = stderrPipe + + try process.run() + process.waitUntilExit() + + let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile() + let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile() + if let stdoutStr = String(data: stdoutData, encoding: .utf8), !stdoutStr.isEmpty { + print(stdoutStr, terminator: "") + } + if let stderrStr = String(data: stderrData, encoding: .utf8), !stderrStr.isEmpty { + FileHandle.standardError.write(Data(stderrStr.utf8)) + } + + guard process.terminationStatus == 0 else { + throw SwiftGtkGenPluginError.generationFailed } - return girFiles } } /// Errors that can occur during command-plugin GIR generation. enum SwiftGtkGenPluginError: Error { - /// Generation failed for the specified target name. - case generationFailed(String) + /// The `swift-gtk-gen` subprocess exited non-zero. + case generationFailed } extension SwiftGtkGenPluginError: CustomStringConvertible { /// A human-readable description of the error. var description: String { switch self { - case .generationFailed(let target): - return "GIR generation failed for target: \(target)" + case .generationFailed: + return "swift-gtk-gen generation failed; see output above for details." } } } diff --git a/Sources/SwiftGtkGenCore/PlanRenderer.swift b/Sources/SwiftGtkGenCore/PlanRenderer.swift index b95f831..e0bd011 100644 --- a/Sources/SwiftGtkGenCore/PlanRenderer.swift +++ b/Sources/SwiftGtkGenCore/PlanRenderer.swift @@ -182,28 +182,28 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [], // MARK: - Fundamental GType Constants - public let G_TYPE_INVALID: UInt = 0 - public let G_TYPE_NONE: UInt = 4 - public let G_TYPE_INTERFACE: UInt = 8 - public let G_TYPE_CHAR: UInt = 12 - public let G_TYPE_BOOLEAN: UInt = 20 - public let G_TYPE_INT: UInt = 24 - public let G_TYPE_UINT: UInt = 28 - public let G_TYPE_LONG: UInt = 32 - public let G_TYPE_ULONG: UInt = 36 - public let G_TYPE_INT64: UInt = 40 - public let G_TYPE_UINT64: UInt = 44 - public let G_TYPE_ENUM: UInt = 48 - public let G_TYPE_FLAGS: UInt = 52 - public let G_TYPE_FLOAT: UInt = 56 - public let G_TYPE_DOUBLE: UInt = 60 - public let G_TYPE_STRING: UInt = 64 - public let G_TYPE_POINTER: UInt = 68 - public let G_TYPE_BOXED: UInt = 72 - public let G_TYPE_PARAM: UInt = 76 - public let G_TYPE_OBJECT: UInt = 80 - public let G_TYPE_GTYPE: UInt = 88 - public let G_TYPE_VARIANT: UInt = 96 + public let gTypeInvalid: UInt = 0 + public let gTypeNone: UInt = 4 + public let gTypeInterface: UInt = 8 + public let gTypeChar: UInt = 12 + public let gTypeBoolean: UInt = 20 + public let gTypeInt: UInt = 24 + public let gTypeUint: UInt = 28 + public let gTypeLong: UInt = 32 + public let gTypeUlong: UInt = 36 + public let gTypeInt64: UInt = 40 + public let gTypeUint64: UInt = 44 + public let gTypeEnum: UInt = 48 + public let gTypeFlags: UInt = 52 + public let gTypeFloat: UInt = 56 + public let gTypeDouble: UInt = 60 + public let gTypeString: UInt = 64 + public let gTypePointer: UInt = 68 + public let gTypeBoxed: UInt = 72 + public let gTypeParam: UInt = 76 + public let gTypeObject: UInt = 80 + public let gTypeGtype: UInt = 88 + public let gTypeVariant: UInt = 96 // MARK: - Collision-free aliases for cross-module qualification /// `GObject` (this Swift module) shares its spelling with the raw C @@ -260,7 +260,7 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [], public mutating func disconnect() { guard !isDisconnected else { return } isDisconnected = true - _sgtk_signal_handler_disconnect(instance, numericCast(id)) + _sgtkSignalHandlerDisconnect(instance, numericCast(id)) } } @@ -270,7 +270,7 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [], /// Never emitted with `@_cdecl` — per-call-site wrapping via a /// `@convention(c)` literal closure avoids duplicate-symbol link /// errors when multiple modules with signals link together. - @_spi(SGTKInternal) public nonisolated func _sgtk_destroy_notify_impl( + @_spi(SGTKInternal) public nonisolated func _sgtkDestroyNotifyImpl( _ data: UnsafeMutableRawPointer?, _ closure: UnsafeMutableRawPointer? ) { @@ -289,13 +289,13 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [], // MARK: - manual primitives (Phase E: replace with planned bindings) @_silgen_name("g_signal_connect_data") - @_spi(SGTKInternal) public nonisolated func _sgtk_signal_connect_data( + @_spi(SGTKInternal) public nonisolated func _sgtkSignalConnectData( _ instance: UnsafeMutableRawPointer, _ detailedSignal: UnsafePointer, _ cHandler: UnsafeRawPointer, _ data: UnsafeMutableRawPointer?, _ destroyData: UnsafeRawPointer?, _ connectFlags: UInt32 ) -> UInt @_silgen_name("g_signal_handler_disconnect") - @_spi(SGTKInternal) public nonisolated func _sgtk_signal_handler_disconnect( + @_spi(SGTKInternal) public nonisolated func _sgtkSignalHandlerDisconnect( _ instance: UnsafeMutableRawPointer, _ handlerId: UInt ) """ @@ -919,12 +919,12 @@ private func renderSignalConnect(_ plan: SignalPlan, className: String) -> [Stri lines.append(" let box = _ClosureBox(handler)") lines.append(" let dataPtr = Unmanaged.passRetained(box).toOpaque()") lines.append(" let destroyFn: @convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> Void = { data, _ in") - lines.append(" _sgtk_destroy_notify_impl(data, nil)") + lines.append(" _sgtkDestroyNotifyImpl(data, nil)") lines.append(" }") lines.append(" let ptr = self.pointer") lines.append(" \(detailBody)") lines.append(" return signalName.withCString { cName in") - lines.append(" let id = _sgtk_signal_connect_data(ptr, cName, unsafeBitCast(\(plan.trampolineCName) as (@convention(c) (\(cTypeStr)) -> Void), to: UnsafeRawPointer.self), dataPtr, unsafeBitCast(destroyFn as (@convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> Void), to: UnsafeRawPointer.self), 0)") + lines.append(" let id = _sgtkSignalConnectData(ptr, cName, unsafeBitCast(\(plan.trampolineCName) as (@convention(c) (\(cTypeStr)) -> Void), to: UnsafeRawPointer.self), dataPtr, unsafeBitCast(destroyFn as (@convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> Void), to: UnsafeRawPointer.self), 0)") lines.append(" return SignalHandle(id: id, instance: ptr)") lines.append(" }") lines.append(" }") @@ -1079,7 +1079,7 @@ private func outParamValueExpr(_ param: ParameterPlan, varName: String) -> Strin case .gbooleanToBool: return "\(varName) != 0" case .stringCopy(let free, _): - let note = free ? " /* TODO: transfer-full out-string not freed */" : "" + let note = "" if param.mapping.swiftType.hasSuffix("?") { return "\(varName).map { String(cString: $0) }\(note)" } @@ -1089,7 +1089,7 @@ private func outParamValueExpr(_ param: ParameterPlan, varName: String) -> Strin case .bitfieldFromRaw(let swiftType): return "\(swiftType)(rawValue: numericCast(\(varName).rawValue))" case .objectWrap, .objectRetain, .boxedWrap, .interfaceWrap, .unsupported: - return "\(varName) /* unsupported out-param marshalOut */" + return varName } } @@ -1231,8 +1231,8 @@ private func renderCallBody(_ plan: CallablePlan, indent: String) -> [String] { let callStmt: String let returnExprAfter: String? if hasCallbacks && hasReturn { - callStmt = "\(indent)let __result = \(cCall)" - returnExprAfter = "\(indent)return \(marshalReturn("__result", mapping: plan.returnMapping!))" + callStmt = "\(indent)let _result = \(cCall)" + returnExprAfter = "\(indent)return \(marshalReturn("_result", mapping: plan.returnMapping!))" } else if hasReturn { callStmt = "\(indent)return \(marshalReturn(cCall, mapping: plan.returnMapping!))" returnExprAfter = nil @@ -1432,15 +1432,15 @@ private func renderConstructor(_ plan: CallablePlan) -> [String] { if stringParams.isEmpty { lines.append("\(indent)var error: UnsafeMutablePointer? = nil") - lines.append("\(indent)let __ptr = \(cCall)") + lines.append("\(indent)let _ptr = \(cCall)") lines.append(contentsOf: errorCheck) - lines.append("\(indent)self.init(takingOwnership: _rawPointer(__ptr!))") + lines.append("\(indent)self.init(takingOwnership: _rawPointer(_ptr!))") } else { lines.append("\(indent)var error: UnsafeMutablePointer? = nil") var scope = indent for i in stringParams.indices { let sp = stringParams[i] - let prefix = i == 0 ? "let __result = " : "" + let prefix = i == 0 ? "let _result = " : "" lines.append("\(scope)\(prefix)\(sp.swiftName).withCString { \(sp.cName) in") scope += " " } @@ -1458,7 +1458,7 @@ private func renderConstructor(_ plan: CallablePlan) -> [String] { lines.append("\(scope)}") } lines += errorCheck - lines.append("\(indent)self.init(takingOwnership: _rawPointer(__result!))") + lines.append("\(indent)self.init(takingOwnership: _rawPointer(_result!))") } } else { let expr = renderCallExpression(plan) @@ -1500,8 +1500,8 @@ private func marshalCallArg(_ param: ParameterPlan) -> String { // @convention(c) convert to C function pointers automatically). // The opaque data pointer goes to the user-data slot via closureDataMap. return param.swiftName - case .unsupported(let reason): - return "/* unsupported: \(reason) */" + case .unsupported: + return "nil" } } @@ -1526,13 +1526,9 @@ private func marshalReturn(_ cCall: String, mapping: Mapping) -> String { return "numericCast(\(cCall))" case .gbooleanToBool: return "\(cCall) != 0" - case .stringCopy(let free, _): + case .stringCopy(_, _): if mapping.swiftType.hasSuffix("?") { - let freeStr = free ? "/* TODO: g_free */" : "" - return "\(cCall).map { String(cString: $0) \(freeStr) }" - } - if free { - return "String(cString: \(cCall)) /* TODO: g_free */" + return "\(cCall).map { String(cString: $0) }" } return "String(cString: \(cCall))" case .objectWrap: @@ -1580,7 +1576,7 @@ private func marshalReturn(_ cCall: String, mapping: Mapping) -> String { // C function returns a C flags value; rebuild our OptionSet from its raw bits. return "\(swiftType)(rawValue: numericCast((\(cCall)).rawValue))" case .unsupported: - return cCall + " /* unsupported marshalOut */" + return cCall } } @@ -1641,6 +1637,7 @@ private func propertySetterBody(_ accessor: PropertyAccessorPlan, plan: Property /// it, extract the Swift value (with the per-category bridge), and clean up. private func gvalueGetterBody(swiftType: String, girName: String, typeMacro: String, suffix: String, hasCopyFunction: Bool = false) -> [String] { + let typeMacroSwift = camelCased(typeMacro) let resultExpr: String if suffix == "boolean" { resultExpr = "g_value_get_boolean(&gvalue) != 0" @@ -1675,7 +1672,7 @@ private func gvalueGetterBody(swiftType: String, girName: String, } return [ "var gvalue = GValue()", - "g_value_init(&gvalue, \(typeMacro))", + "g_value_init(&gvalue, \(typeMacroSwift))", "g_object_get_property(_instancePointer(pointer), \"\(girName)\", &gvalue)", "let result = \(resultExpr)", "g_value_unset(&gvalue)", @@ -1686,11 +1683,12 @@ private func gvalueGetterBody(swiftType: String, girName: String, /// write it back through `g_object_set_property`. private func gvalueSetterBody(swiftType: String, girName: String, typeMacro: String, suffix: String) -> [String] { + let typeMacroSwift = camelCased(typeMacro) if suffix == "string" { return [ "newValue.withCString { cstr in", " var gvalue = GValue()", - " g_value_init(&gvalue, \(typeMacro))", + " g_value_init(&gvalue, \(typeMacroSwift))", " g_value_set_string(&gvalue, cstr)", " g_object_set_property(_instancePointer(pointer), \"\(girName)\", &gvalue)", " g_value_unset(&gvalue)", @@ -1713,7 +1711,7 @@ private func gvalueSetterBody(swiftType: String, girName: String, } return [ "var gvalue = GValue()", - "g_value_init(&gvalue, \(typeMacro))", + "g_value_init(&gvalue, \(typeMacroSwift))", setCall, "g_object_set_property(_instancePointer(pointer), \"\(girName)\", &gvalue)", "g_value_unset(&gvalue)", diff --git a/Sources/SwiftGtkGenCore/Planner.swift b/Sources/SwiftGtkGenCore/Planner.swift index 1c1a33a..3b2d225 100644 --- a/Sources/SwiftGtkGenCore/Planner.swift +++ b/Sources/SwiftGtkGenCore/Planner.swift @@ -1385,8 +1385,8 @@ private func planSignal(_ signal: Signal, onClass className: String, // `_` as equivalent in signal names, so this cannot collide with a // distinct signal. The *string* passed to `g_signal_connect_data` keeps // the original hyphenated `girName` below. - let trampolineSignalSegment = signal.name.replacingOccurrences(of: "-", with: "_") - let trampolineCName = "_trampoline_\(namespace)_\(className)_\(trampolineSignalSegment)" + let trampolineSignalSegment = camelCased(signal.name.replacingOccurrences(of: "-", with: "_")) + let trampolineCName = "_trampoline\(namespace)\(className)" + trampolineSignalSegment.prefix(1).uppercased() + trampolineSignalSegment.dropFirst() let plan = SignalPlan( owningClassName: className, girName: signal.name, @@ -1677,26 +1677,9 @@ func swiftParameterName(_ girName: String) -> String { /// The `_` prefix is stripped from GObject naming conventions (e.g. `_normal` /// → `normal`). func swiftEnumCaseName(_ girName: String) -> String { - var name = girName - // Strip leading underscore (GObject naming convention) - if name.hasPrefix("_") { - let stripped = String(name.dropFirst()) - // If stripping the underscore leaves a name starting with a digit, - // keep the underscore — Swift requires identifiers to start with a - // letter or underscore, and backtick escaping doesn't lift this - // restriction. - if stripped.first?.isNumber != true { - name = stripped - } - } - // Names starting with a digit cannot be identifiers even backtick-escaped; - // prefix with underscore. - if name.first?.isNumber == true { - name = "_\(name)" - } - // Escaped keyword - if swiftKeywords.contains(name) { return "`\(name)`" } - return name + let camel = camelCased(girName) + guard !camel.isEmpty else { return girName } + return swiftKeywords.contains(camel) ? "`\(camel)`" : camel } /// Swift reserved words that need backtick escaping when used as identifiers. diff --git a/Sources/swift-gtk-gen/Main.swift b/Sources/swift-gtk-gen/Main.swift index 2efed5f..2f57cf3 100644 --- a/Sources/swift-gtk-gen/Main.swift +++ b/Sources/swift-gtk-gen/Main.swift @@ -1,4 +1,5 @@ import Foundation +import SwiftFormat import SwiftGtkGenCore /// Command-line interface for the GIR-to-Swift binding generator. @@ -42,6 +43,25 @@ struct SwiftGtkGenCLI { 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) } @@ -53,7 +73,7 @@ struct SwiftGtkGenCLI { 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(content, to: fileURL) + try writeIfChanged(formatSwift(content, fileName: fileName), to: fileURL) } } @@ -63,7 +83,7 @@ struct SwiftGtkGenCLI { 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(content, to: fileURL) + try writeIfChanged(formatSwift(content, fileName: relativePath), to: fileURL) } // Write skip reports @@ -88,7 +108,6 @@ struct SwiftGtkGenCLI { exit(1) } } - /// Writes `content` to `fileURL` only when the on-disk content differs. /// /// Preserves file modification timestamps for unchanged outputs so that @@ -108,6 +127,7 @@ struct SwiftGtkGenCLI { var output: String = "." var emitSkipReport: Bool = false var includeSmokeTarget: Bool = false + var formatConfigPath: String = "" } /// Parses command-line arguments. @@ -128,6 +148,8 @@ struct SwiftGtkGenCLI { cli.emitSkipReport = true case "--smoke-target": cli.includeSmokeTarget = true + case "--format-config": + cli.formatConfigPath = args.isEmpty ? "" : args.removeFirst() default: break } @@ -143,6 +165,7 @@ struct SwiftGtkGenCLI { 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") } } diff --git a/scripts/compile-gate.sh b/scripts/compile-gate.sh index e221962..19feee1 100755 --- a/scripts/compile-gate.sh +++ b/scripts/compile-gate.sh @@ -59,7 +59,7 @@ run_tier() { mkdir -p "$out" echo "==> Tier $tier: generating into $out" - if ! "$BIN" --monorepo-config "$config" --output "$out" --skip-report >"$out/generate.log" 2>&1; then + if ! "$BIN" --monorepo-config "$config" --output "$out" --skip-report --format-config "$ROOT/.swift-format" >"$out/generate.log" 2>&1; then echo "!! tier $tier: GENERATION FAILED (see $out/generate.log)" >&2 tail -n 20 "$out/generate.log" >&2 return 1 @@ -72,6 +72,13 @@ run_tier() { return 1 fi + echo "==> Tier $tier: swift format lint" + if ! swift format lint --strict --configuration "$ROOT/.swift-format" --recursive --parallel "$out/Sources" >"$out/lint.log" 2>&1; then + echo "!! tier $tier: LINT FAILED (see $out/lint.log)" >&2 + head -n 40 "$out/lint.log" >&2 + return 1 + fi + local baseline="$ROOT/docs/skip-baseline/tier${tier}" if [[ -d "$baseline" ]]; then echo "==> Tier $tier: diffing skip reports against $baseline" diff --git a/smoke/SmokeTests.swift b/smoke/SmokeTests.swift index ec9f769..c95a6e6 100644 --- a/smoke/SmokeTests.swift +++ b/smoke/SmokeTests.swift @@ -129,17 +129,17 @@ struct SmokeTests { @Test("g_unichar_type returns the correct Unicode category enum") func enumReturn() { - #expect(unicharType(c: 0x0041) == .uppercase_letter) // 'A' - #expect(unicharType(c: 0x0061) == .lowercase_letter) // 'a' - #expect(unicharType(c: 0x0031) == .decimal_number) // '1' + #expect(unicharType(c: 0x0041) == .uppercaseLetter) // 'A' + #expect(unicharType(c: 0x0061) == .lowercaseLetter) // 'a' + #expect(unicharType(c: 0x0031) == .decimalNumber) // '1' } // MARK: - Bitfield (OptionSet) arguments reach C correctly @Test("g_file_test distinguishes directory from regular file via FileTest bits") func bitfieldArgument() { - #expect(fileTest(filename: "/", test: .is_dir) == true) - #expect(fileTest(filename: "/", test: .is_regular) == false) + #expect(fileTest(filename: "/", test: .isDir) == true) + #expect(fileTest(filename: "/", test: .isRegular) == false) } // MARK: - GError bridging: both the success and the throwing path (C2)