1
0
Fork 0

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.
This commit is contained in:
Brendan Szymanski 2026-07-20 23:44:49 -04:00
parent 525aefa7aa
commit 57ff84bce5
9 changed files with 194 additions and 285 deletions

51
Package.resolved Normal file
View file

@ -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
}

View file

@ -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(

View file

@ -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)"
}
}
}

View file

@ -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 <cfg> [--skip-report] [--smoke-target] [--format-config <path>]")
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."
}
}
}

View file

@ -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<CChar>,
_ 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<GError>? = 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<GError>? = 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)",

View file

@ -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.

View file

@ -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/<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")
}
}

View file

@ -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"

View file

@ -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)