Files
luma/LumaGtk/Package.swift
Ole André Vadla Ravnås 531468e7b9 gtk: Visualize decoded patterns
Open a field's visualizer from its tree row, and draw each kind in the
render tests.
2026-10-01 20:49:11 +02:00

253 lines
9.3 KiB
Swift

// swift-tools-version: 6.0
import Foundation
import PackageDescription
let lumaGtkTargetDir = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.appendingPathComponent("Sources/LumaGtk", isDirectory: true).path
let lumaGtkExcludes: [String] = ["luma.rc", "luma.res"].filter { name in
FileManager.default.fileExists(atPath: lumaGtkTargetDir + "/" + name)
}
#if os(Windows)
let lumaExecutableIconResource = compileWindowsExecutableIcon()
#endif
#if os(macOS)
let cLumaSources: [String] = ["shim_gtk.c", "svg_paintable.c"]
let cLumaCSettings: [CSetting] = [
.unsafeFlags(pkgConfigFlags(["gtk4", "libadwaita-1", "epoxy", "librsvg-2.0"])),
]
let cLumaCxxSettings: [CXXSetting] = []
let cLumaLinkerSettings: [LinkerSetting] = [
.linkedFramework("CoreGraphics"),
.unsafeFlags(pkgConfigFlags(["libadwaita-1", "epoxy", "librsvg-2.0"], libs: true)),
]
let lumaGtkLinkerSettings: [LinkerSetting] = [
.unsafeFlags(["-Xlinker", "-export_dynamic"]),
]
#elseif os(Windows)
let cLumaSources: [String] = ["shim_gtk.c", "svg_paintable.c"]
let cLumaCSettings: [CSetting] = [
.unsafeFlags(pkgConfigFlags(["gtk4", "epoxy", "librsvg-2.0"])),
]
let cLumaCxxSettings: [CXXSetting] = []
let cLumaLinkerSettings: [LinkerSetting] = [
.linkedLibrary("user32"),
.linkedLibrary("epoxy"),
.linkedLibrary("rsvg-2"),
]
// Windows: produce a GUI app (no console window). Swift's runtime
// still calls main(), so redirect the linker entry to the C runtime's
// main-compatible start routine rather than WinMain. Tool binaries
// built by SwiftPM plugins (gir2swift-tool, Yams-tool, ...) need the
// default console subsystem, so keep these target-scoped.
//
// The /ignore:* warning filters also apply to those tool binaries and
// can't be set on targets outside this package — they live on the
// swift build command line (see scripts/windows/build.ps1 and the
// Windows CI job).
let windowsGuiLinkerFlags = ["-Xlinker", "/SUBSYSTEM:WINDOWS", "-Xlinker", "/ENTRY:mainCRTStartup"]
let lumaGtkLinkerSettings: [LinkerSetting] = [
.unsafeFlags(
windowsGuiLinkerFlags
+ pharoBridgeExports().flatMap { ["-Xlinker", "/EXPORT:\($0)"] }
+ (lumaExecutableIconResource.map { [$0] } ?? [])
)
]
#else
let cLumaSources: [String] = ["shim_gtk.c", "svg_paintable.c"]
let cLumaCSettings: [CSetting] = [
.unsafeFlags(pkgConfigFlags(["gtk4", "epoxy", "librsvg-2.0"])),
]
let cLumaCxxSettings: [CXXSetting] = []
let cLumaLinkerSettings: [LinkerSetting] = [
.linkedLibrary("X11"),
.linkedLibrary("epoxy"),
.linkedLibrary("rsvg-2"),
]
let lumaGtkLinkerSettings: [LinkerSetting] = [
// Fedora's Swift 6.2 ships libswiftObservation.so with an unresolved
// reference to swift::threading::fatal that no other shipped library
// exports. Until upstream fixes the packaging, allow the dynamic
// linker to defer the symbol — it's only ever called on a fatal
// assertion path inside Observation.
.unsafeFlags(["-Xlinker", "--allow-shlib-undefined"]),
.unsafeFlags(["-Xlinker", "--export-dynamic"]),
]
#endif
let package = Package(
name: "LumaGtk",
platforms: [
.macOS(.v15),
],
dependencies: [
.package(name: "luma", path: ".."),
.package(url: "https://github.com/frida/SwiftGtk.git", branch: "gtk4-development"),
.package(url: "https://github.com/frida/SwiftAdw.git", branch: "development"),
.package(url: "https://github.com/frida/SwiftGtkSourceView.git", branch: "development"),
],
targets: [
.target(
name: "CLuma",
path: "Sources/CLuma",
sources: cLumaSources,
publicHeadersPath: "include",
cSettings: cLumaCSettings,
cxxSettings: cLumaCxxSettings,
linkerSettings: cLumaLinkerSettings
),
.executableTarget(
name: "LumaGtk",
dependencies: [
.product(name: "LumaCore", package: "luma"),
.product(name: "Gtk", package: "SwiftGtk"),
.product(name: "Adw", package: "SwiftAdw"),
.product(name: "GtkSource", package: "SwiftGtkSourceView"),
"CLuma",
],
path: "Sources/LumaGtk",
exclude: lumaGtkExcludes,
resources: [
.copy("Resources/nowsecure-logo.svg"),
.copy("Resources/pharo"),
],
swiftSettings: [
.swiftLanguageMode(.v6),
// The GL widget is Swift, so CLuma's headers reach epoxy.
.unsafeFlags(pkgConfigFlags(["epoxy"]).flatMap { ["-Xcc", $0] }),
] + adwaitaFeatureDefines(),
linkerSettings: lumaGtkLinkerSettings,
plugins: [
.plugin(name: "EmbedLumaVersion"),
]
),
.executableTarget(
name: "LumaVersionWriter",
path: "Sources/LumaVersionWriter",
swiftSettings: [
.swiftLanguageMode(.v6),
]
),
.plugin(
name: "EmbedLumaVersion",
capability: .buildTool(),
dependencies: ["LumaVersionWriter"],
path: "Plugins/EmbedLumaVersion"
),
]
)
#if os(Windows)
func compileWindowsExecutableIcon() -> String? {
let pkg = URL(fileURLWithPath: #filePath).deletingLastPathComponent().path
let rcFile = pkg + "\\Sources\\LumaGtk\\luma.rc"
let resFile = pkg + "\\Sources\\LumaGtk\\luma.res"
guard FileManager.default.fileExists(atPath: rcFile) else { return nil }
if let rc = findOnPath("rc") {
let proc = Process()
proc.executableURL = URL(fileURLWithPath: rc)
proc.arguments = ["/nologo", "/fo", resFile, rcFile]
proc.standardOutput = FileHandle.nullDevice
proc.standardError = FileHandle.nullDevice
try? proc.run()
proc.waitUntilExit()
if proc.terminationStatus == 0 { return resFile }
}
return FileManager.default.fileExists(atPath: resFile) ? resFile : nil
}
#endif
func adwaitaFeatureDefines() -> [SwiftSetting] {
var defines: [SwiftSetting] = []
if pkgConfigAtLeast("libadwaita-1", "1.6") {
defines.append(.define("HAS_ADW_SPINNER"))
}
if pkgConfigAtLeast("libadwaita-1", "1.8") {
defines.append(.define("HAS_ADW_SHORTCUTS_DIALOG"))
}
if pkgConfigAtLeast("gtk4", "4.16") {
defines.append(.define("HAS_GDK_MEMORY_TEXTURE_BUILDER"))
}
if pkgConfigAtLeast("gtk4", "4.20") {
defines.append(.define("HAS_GTK_CALENDAR_SET_DATE"))
}
return defines
}
// The image looks these up by name and nothing in Swift calls them, so on
// Windows they reach the export table only by being named here.
func pharoBridgeExports() -> [String] {
let bridgeDir = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent("Sources/LumaCore/Pharo", isDirectory: true)
let sources = (try? FileManager.default.contentsOfDirectory(at: bridgeDir, includingPropertiesForKeys: nil)) ?? []
return sources.filter { $0.pathExtension == "swift" }
.flatMap { source -> [String] in
let text = (try? String(contentsOf: source, encoding: .utf8)) ?? ""
return text.components(separatedBy: "@_cdecl(\"").dropFirst().map {
String($0.prefix(while: { $0 != "\"" }))
}
}
.sorted()
}
func pkgConfigFlags(_ packages: [String], libs: Bool = false) -> [String] {
guard let pkgConfigPath = findOnPath("pkg-config") else { return [] }
let proc = Process()
proc.executableURL = URL(fileURLWithPath: pkgConfigPath)
proc.arguments = (libs ? ["--libs"] : ["--cflags"]) + packages
let pipe = Pipe()
proc.standardOutput = pipe
proc.standardError = FileHandle.nullDevice
try? proc.run()
proc.waitUntilExit()
guard proc.terminationStatus == 0 else { return [] }
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let tokens = String(data: data, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines)
.split(separator: " ")
.map(String.init) ?? []
var seen = Set<String>()
return tokens.filter { seen.insert($0).inserted }
}
func pkgConfigAtLeast(_ package: String, _ version: String) -> Bool {
guard let pkgConfigPath = findOnPath("pkg-config") else { return false }
let proc = Process()
proc.executableURL = URL(fileURLWithPath: pkgConfigPath)
proc.arguments = ["--atleast-version=\(version)", package]
proc.standardOutput = FileHandle.nullDevice
proc.standardError = FileHandle.nullDevice
try? proc.run()
proc.waitUntilExit()
return proc.terminationStatus == 0
}
func findOnPath(_ name: String) -> String? {
#if os(Windows)
let separator: Character = ";"
let extensions = ["", ".exe", ".cmd", ".bat"]
let pathSep = "\\"
#else
let separator: Character = ":"
let extensions = [""]
let pathSep = "/"
#endif
let env = ProcessInfo.processInfo.environment
guard let pathValue = env["PATH"] ?? env["Path"] else { return nil }
for dir in pathValue.split(separator: separator).map(String.init) where !dir.isEmpty {
for ext in extensions {
let candidate = dir + pathSep + name + ext
if FileManager.default.fileExists(atPath: candidate) {
return candidate
}
}
}
return nil
}