519 lines
21 KiB
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
|
|
}
|
|
|