1
0
Fork 0
gobject-generator/Sources/SwiftGtkGenCore/Config.swift

519 lines
21 KiB
Swift

/// Top-level configuration for generating Swift bindings from a GIR repository.
///
/// `GenerationConfig` is the Swift-native equivalent of a config file or TOML
/// approach used by other GIR-based generators. It specifies which `.gir` files
/// to read, where to emit output, which types to generate/manually implement/
/// ignore, and any per-type or per-function overrides.
///
/// ### Example
/// ```swift
/// GenerationConfig(
/// library: "Gtk",
/// version: "4.0",
/// girsDirectories: ["/usr/share/gir-1.0"],
/// targetDirectory: "Sources/Gtk",
/// externalLibraries: ["GLib", "GObject"],
/// generate: ["Gtk.Widget", "Gtk.Window"],
/// manual: ["Gtk.CustomWidget"],
/// ignore: ["Gtk.DeprecatedType"],
/// objects: []
/// )
/// ```
public struct GenerationConfig {
/// The name of the library being wrapped (e.g. `"Gtk"`, `"GLib"`).
public var library: String
/// The version string of the GIR namespace (e.g. `"4.0"`).
public var version: String
/// Directories to search for `.gir` files.
public var girsDirectories: [String]
/// Directory where generated Swift source files should be written.
public var targetDirectory: String
/// Names of external libraries whose types may be referenced (e.g. `"GLib"`, `"GObject"`).
public var externalLibraries: [String]
/// Fully-qualified type names for which Swift bindings should be generated.
public var generate: [String]
/// Fully-qualified type names that are implemented manually and should not be generated.
public var manual: [String]
/// Fully-qualified type names that should be skipped entirely.
public var ignore: [String]
/// Per-type and per-function override entries.
public var objects: [ObjectConfig]
/// Creates a complete generation configuration.
///
/// - Parameters:
/// - library: The library name (e.g. `"Gtk"`).
/// - version: The GIR namespace version (e.g. `"4.0"`).
/// - girsDirectories: Paths to search for `.gir` files.
/// - targetDirectory: Output directory for generated Swift sources.
/// - externalLibraries: Referenced external library names.
/// - generate: Types to generate bindings for.
/// - manual: Types handled by hand-written code.
/// - ignore: Types to skip.
/// - objects: Override entries for types, functions, signals, and properties.
public init(library: String, version: String, girsDirectories: [String],
targetDirectory: String, externalLibraries: [String],
generate: [String], manual: [String], ignore: [String],
objects: [ObjectConfig]) {
self.library = library
self.version = version
self.girsDirectories = girsDirectories
self.targetDirectory = targetDirectory
self.externalLibraries = externalLibraries
self.generate = generate
self.manual = manual
self.ignore = ignore
self.objects = objects
}
}
/// A configuration entry targeting a specific type, function, function pattern,
/// signal, or property in the GIR repository.
///
/// Each case carries the overrides or rename rules that should be applied
/// during code generation for the matched element.
public enum ObjectConfig {
/// Overrides for a specific GObject type identified by its fully-qualified name.
case object(_ name: String, overrides: ObjectOverrides)
/// Overrides for a specific function on a type.
case function(_ type: String, _ name: String, overrides: FunctionOverrides)
/// A regex-based rename rule applied to matching functions on a type.
case functionPattern(_ type: String, pattern: String, rename: RenameRule)
/// Overrides for a specific signal on a type.
case signal(_ type: String, _ name: String, overrides: SignalOverrides)
/// Overrides for a specific property on a type.
case property(_ type: String, _ name: String, overrides: PropertyOverrides)
}
/// Overrides that control how a single GObject type is treated during code generation.
///
/// All properties are optional; only the values that are explicitly set will
/// override the default generation behavior for the matched type.
public struct ObjectOverrides {
/// Whether to generate, mark as manual, or ignore this type.
public var status: ObjectStatus?
/// If `true`, mark the generated class as `final`.
public var finalType: Bool?
/// The concurrency model to apply (e.g. `@MainActor`, `Sendable`).
public var concurrency: ConcurrencyModel?
/// Minimum version string; the type is only generated when targeting this version or later.
public var version: String?
/// An optional `#if` compilation condition to guard the generated code.
public var cfgCondition: String?
/// If `true`, generate a builder pattern struct for constructing this type.
public var generateBuilder: Bool?
}
/// Controls how a generated type should be treated.
public enum ObjectStatus: String {
/// Generate Swift bindings for this type.
case generate
/// This type will be implemented manually; do not generate.
case manual
/// Skip this type entirely.
case ignore
}
/// The concurrency model to apply to a generated type.
public enum ConcurrencyModel: String {
/// Annotate the generated type with `@MainActor`.
case mainActor
/// Mark the generated type as `Sendable`.
case sendable
/// No special concurrency annotation.
case none
}
/// Overrides that control how a single function is treated during code generation.
///
/// All properties are optional; only the values that are explicitly set will
/// override the default generation behavior for the matched function.
public struct FunctionOverrides {
/// If `true`, skip generating this function entirely.
public var ignore: Bool?
/// A rename rule to apply to this function's Swift name.
public var rename: RenameRule?
/// Minimum version string; only generate when targeting this version or later.
public var version: String?
/// An optional `#if` compilation condition to guard the generated code.
public var cfgCondition: String?
/// If `true`, treat this function as a constructor (returns a new instance).
public var constructor: Bool?
/// Override the visibility of the generated method.
public var visibility: Visibility?
/// Per-parameter overrides keyed by the parameter's original name.
public var parameters: [String: ParameterOverride]?
}
/// The visibility level for a generated symbol.
public enum Visibility: String {
/// Visible outside the module.
case `public`
/// Visible only within the same module.
case `internal`
/// Visible to all modules in the same package.
case `package`
}
/// Overrides for a single function parameter in the generated API.
public struct ParameterOverride {
/// If set, overrides whether the parameter is treated as nullable.
public var nullable: Bool?
/// If set, renames this parameter in the generated Swift function signature.
public var newName: String?
}
/// Overrides that control how a GObject signal handler is generated.
///
/// All properties are optional; only the explicitly set values override
/// the default behavior for the matched signal.
public struct SignalOverrides {
/// If `true`, skip generating a handler API for this signal.
public var ignore: Bool?
/// If `true`, the signal can be inhibited (stopped from propagating).
public var inhibit: Bool?
/// Per-parameter overrides for the signal handler's closure parameters.
public var parameters: [String: ParameterOverride]?
}
/// Overrides that control how a GObject property is exposed in the generated API.
public struct PropertyOverrides {
/// The accessor methods to generate for this property.
///
/// If `nil`, the default set of accessors is generated based on the GIR
/// metadata. Provide an explicit array to override which accessors are emitted.
public var generate: [PropertyAccessor]?
}
/// The kind of accessor to generate for a GObject property.
public enum PropertyAccessor: String {
/// Generate a getter method for the property.
case get
/// Generate a setter method for the property.
case set
/// Generate a notification callback/handler for property changes.
case notify
}
/// A regex-based rename rule for transforming GIR symbol names into Swift names.
///
/// Matches the `regex` pattern against the original name and substitutes the
/// `replacement` string, following standard regex capture-group semantics
/// (e.g., `"$1"`, `"$2"`).
///
/// ### Example
/// ```swift
/// RenameRule(regex: "^gtk_", replacement: "")
/// ```
public struct RenameRule {
/// The regular expression pattern to match against the original name.
public var regex: String
/// The replacement string, which may reference capture groups with `$1`, `$2`, etc.
public var replacement: String
/// Creates a rename rule with the given regex pattern and replacement.
///
/// - Parameters:
/// - regex: A regular expression pattern.
/// - replacement: A replacement string (may include capture-group references).
public init(regex: String, replacement: String) {
self.regex = regex
self.replacement = replacement
}
}
extension GenerationConfig {
/// Constructs a `GenerationConfig` from a TOML-parsed dictionary and the
/// GIR namespace metadata.
///
/// Library name and version are taken from `namespace` automatically; the
/// TOML may optionally specify `target_directory` and
/// `girs_directories`. All other fields (generate, manual, ignore, object
/// overrides) are read from the TOML dictionary.
///
/// - Parameters:
/// - toml: The dictionary produced by `TOMLReader.parse(_:)`.
/// - namespace: The GIR namespace providing `name` and `version`.
/// - defaultTarget: Fallback target directory if TOML omits `target_directory`.
/// - Returns: A fully-populated `GenerationConfig`.
/// - Throws: `TOMLReaderError` if required fields are missing or malformed.
public static func from(toml: [String: Any], namespace: Namespace, defaultTarget: String) throws -> GenerationConfig {
let lib = namespace.name
let ver = namespace.version
let girsDirs = toml["girs_directories"] as? [String] ?? []
let targetDir = toml["target_directory"] as? String ?? defaultTarget
let extLibs = toml["external_libraries"] as? [String] ?? []
let gen = toml["generate"] as? [String] ?? []
let man = toml["manual"] as? [String] ?? []
let ign = toml["ignore"] as? [String] ?? []
var objects: [ObjectConfig] = []
if let objectDict = toml["object"] as? [String: [String: Any]] {
for name in objectDict.keys.sorted() {
let overrides = try parseObjectOverrides(from: objectDict[name]!)
objects.append(.object(name, overrides: overrides))
}
}
if let funcDict = toml["function"] as? [String: [String: [String: Any]]] {
for typeName in funcDict.keys.sorted() {
let methods = funcDict[typeName]!
for funcName in methods.keys.sorted() {
let fv = try parseFunctionOverrides(from: methods[funcName]!)
objects.append(.function(typeName, funcName, overrides: fv))
}
}
}
if let patternDict = toml["function_pattern"] as? [String: [String: [String: Any]]] {
for typeName in patternDict.keys.sorted() {
let entries = patternDict[typeName]!
for pattern in entries.keys.sorted() {
let pvDict = entries[pattern]!
guard let renameDict = pvDict["rename"] as? [String: String],
let regex = renameDict["regex"],
let replacement = renameDict["replacement"] else {
throw TOMLReaderError.parseError(line: 0, message: "function_pattern.\(typeName).\(pattern) missing rename rule")
}
let rename = RenameRule(regex: regex, replacement: replacement)
objects.append(.functionPattern(typeName, pattern: pattern, rename: rename))
}
}
}
if let sigDict = toml["signal"] as? [String: [String: [String: Any]]] {
for typeName in sigDict.keys.sorted() {
let signals = sigDict[typeName]!
for sigName in signals.keys.sorted() {
let ov = parseSignalOverrides(from: signals[sigName]!)
objects.append(.signal(typeName, sigName, overrides: ov))
}
}
}
if let propDict = toml["property"] as? [String: [String: [String: Any]]] {
for typeName in propDict.keys.sorted() {
let props = propDict[typeName]!
for propName in props.keys.sorted() {
let ov = parsePropertyOverrides(from: props[propName]!)
objects.append(.property(typeName, propName, overrides: ov))
}
}
}
return GenerationConfig(
library: lib, version: ver,
girsDirectories: girsDirs,
targetDirectory: targetDir,
externalLibraries: extLibs,
generate: gen, manual: man, ignore: ign,
objects: objects
)
}
}
private func parseObjectOverrides(from dict: [String: Any]) throws -> ObjectOverrides {
var ov = ObjectOverrides()
if let st = dict["status"] as? String, let status = ObjectStatus(rawValue: st) { ov.status = status }
if let ft = dict["final_type"] as? Bool { ov.finalType = ft }
if let cm = dict["concurrency"] as? String, let cmv = ConcurrencyModel(rawValue: cm) { ov.concurrency = cmv }
if let v = dict["version"] as? String { ov.version = v }
if let cfg = dict["cfg_condition"] as? String { ov.cfgCondition = cfg }
if let gb = dict["generate_builder"] as? Bool { ov.generateBuilder = gb }
return ov
}
private func parseFunctionOverrides(from dict: [String: Any]) throws -> FunctionOverrides {
var ov = FunctionOverrides()
if let i = dict["ignore"] as? Bool { ov.ignore = i }
if let renameDict = dict["rename"] as? [String: String] {
ov.rename = RenameRule(regex: renameDict["regex"] ?? "", replacement: renameDict["replacement"] ?? "")
}
if let v = dict["version"] as? String { ov.version = v }
if let cfg = dict["cfg_condition"] as? String { ov.cfgCondition = cfg }
if let ct = dict["constructor"] as? Bool { ov.constructor = ct }
if let vis = dict["visibility"] as? String, let vv = Visibility(rawValue: vis) { ov.visibility = vv }
if let params = dict["parameter"] as? [String: [String: Any]] {
ov.parameters = params.mapValues { pv in
var po = ParameterOverride()
if let n = pv["nullable"] as? Bool { po.nullable = n }
if let n = pv["new_name"] as? String { po.newName = n }
return po
}
}
return ov
}
private func parseSignalOverrides(from dict: [String: Any]) -> SignalOverrides {
var ov = SignalOverrides()
if let i = dict["ignore"] as? Bool { ov.ignore = i }
if let inh = dict["inhibit"] as? Bool { ov.inhibit = inh }
if let params = dict["parameter"] as? [String: [String: Any]] {
ov.parameters = params.mapValues { pv in
var po = ParameterOverride()
if let n = pv["nullable"] as? Bool { po.nullable = n }
if let n = pv["new_name"] as? String { po.newName = n }
return po
}
}
return ov
}
private func parsePropertyOverrides(from dict: [String: Any]) -> PropertyOverrides {
var ov = PropertyOverrides()
if let genValues = dict["generate"] as? [String] {
ov.generate = genValues.compactMap { PropertyAccessor(rawValue: $0) }
}
return ov
}
/// Configuration for generating a monorepo of Swift wrapper packages
/// from multiple GIR files in a single generator invocation.
///
/// `MonorepoConfig` allows a single generator run to produce multiple SwiftPM
/// packages, each wrapping one GObject library. Packages are processed in order,
/// with the first package treated as the leaf dependency and subsequent packages
/// as dependents.
///
/// ### Example
/// ```toml
/// [[packages]]
/// name = "GLib"
/// gir = "/usr/share/gir-1.0/GLib-2.0.gir"
///
/// [[packages]]
/// name = "GObject"
/// gir = "/usr/share/gir-1.0/GObject-2.0.gir"
/// concurrency = "sendable"
/// ```
public struct MonorepoConfig {
/// Root output directory for the monorepo (e.g. `"."` for the `gtk-swift/` workspace root).
public let outputDir: String
/// Entries for each package to generate, in dependency order (leaf dependency first).
public let packages: [PackageEntry]
/// Creates a monorepo generation configuration.
///
/// - Parameters:
/// - outputDir: The root directory for generated packages.
/// - packages: Ordered list of package entries to generate.
public init(outputDir: String, packages: [PackageEntry]) {
self.outputDir = outputDir
self.packages = packages
}
}
/// A single GObject library wrapper package definition within the monorepo.
///
/// Each `PackageEntry` specifies a `.gir` file to process and the Swift module
/// to produce, along with any per-package concurrency or type overrides.
public struct PackageEntry {
/// Swift module name (e.g. `"Gtk"`, `"GObject"`, `"GLib"`).
public let name: String
/// Path to the `.gir` file for this library (e.g. `"/usr/share/gir-1.0/Gtk-4.0.gir"`).
public let girPath: String
/// Optional concurrency model override for this package's generated types.
/// When `nil`, the default concurrency model derived from GIR metadata is used.
public let concurrency: ConcurrencyModel?
/// Per-type overrides for objects within this package (mirrors `GenerationConfig.objects`).
public let objects: [ObjectConfig]
/// Creates a package entry for the monorepo.
///
/// - Parameters:
/// - name: The Swift module name.
/// - girPath: Path to the `.gir` file to process.
/// - concurrency: An optional concurrency model override. Defaults to `nil`.
/// - objects: Per-type override entries. Defaults to an empty array.
public init(name: String, girPath: String, concurrency: ConcurrencyModel? = nil, objects: [ObjectConfig] = []) {
self.name = name
self.girPath = girPath
self.concurrency = concurrency
self.objects = objects
}
}
/// A GIR `<include>` dependency from one package to another.
public struct IncludeEntry {
/// The library name referenced by the include (e.g. "Gdk").
public let name: String
/// The GIR version (e.g. "4.0").
public let version: String
/// Derives the Swift module name (capitalized name portion).
/// "Gdk" -> "Gdk", "GObject" -> "GObject".
public var swiftModule: String { name }
}
extension MonorepoConfig {
/// Parses a monorepo TOML config file using `[packages.Name]` section headers
/// (the TOML reader supports `[section]` headers but not `[[array-of-tables]]`).
///
/// Example config:
/// ```toml
/// output_dir = "."
///
/// [packages.GLib]
/// gir = "/usr/share/gir-1.0/GLib-2.0.gir"
///
/// [packages.GObject]
/// gir = "/usr/share/gir-1.0/GObject-2.0.gir"
/// concurrency = "mainActor"
/// ```
public static func from(toml: [String: Any], defaultOutputDir: String) throws -> MonorepoConfig {
let outputDir = toml["output_dir"] as? String ?? defaultOutputDir
guard let packagesDict = toml["packages"] as? [String: [String: Any]] else {
throw TOMLReaderError.parseError(line: 0, message: "monorepo config missing [packages.*] sections")
}
var packages: [PackageEntry] = []
for (name, pkgDict) in packagesDict.sorted(by: { $0.key < $1.key }) {
guard let girPath = pkgDict["gir"] as? String else {
throw TOMLReaderError.parseError(line: 0, message: "packages.\(name) missing 'gir'")
}
let concurrency = (pkgDict["concurrency"] as? String).flatMap(ConcurrencyModel.init(rawValue:))
let objects = try parseMonorepoObjectConfigs(from: pkgDict)
packages.append(PackageEntry(name: name, girPath: girPath, concurrency: concurrency, objects: objects))
}
return MonorepoConfig(outputDir: outputDir, packages: packages)
}
}
private func parseMonorepoObjectConfigs(from pkgDict: [String: Any]) throws -> [ObjectConfig] {
var objects: [ObjectConfig] = []
if let objDict = pkgDict["object"] as? [String: [String: Any]] {
for name in objDict.keys.sorted() {
let overrides = try parseObjectOverrides(from: objDict[name]!)
objects.append(.object(name, overrides: overrides))
}
}
return objects
}