diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 0000000..21fb3f2 --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,20 @@ +# Builds the non-UI products (GraphcodeKit, graphcode CLI, graphcoded) on Linux via +# the root Package.swift — the portability gate for issue #83. The macOS app still +# builds only through Tuist/Xcode and is not covered here. +name: Linux + +on: + push: + branches: [main] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + container: swift:6.2 + steps: + - uses: actions/checkout@v4 + - name: Build + run: swift build + - name: Smoke-test CLI + run: .build/debug/graphcode diff --git a/GraphcodeKit/Sources/DaemonBootstrap.swift b/GraphcodeKit/Sources/DaemonBootstrap.swift index 404f7ed..ebea15e 100644 --- a/GraphcodeKit/Sources/DaemonBootstrap.swift +++ b/GraphcodeKit/Sources/DaemonBootstrap.swift @@ -1,5 +1,10 @@ import Foundation +// launchd, quarantine xattrs, `~/Library/LaunchAgents` — this whole mechanism is the +// macOS app's drag-to-Applications install, and only the app calls it. A Linux install +// story (systemd user unit or equivalent) would be a sibling, not a port of this. +#if os(macOS) + /// Installs the helpers a shipped `graphcode.app` carries inside itself — `graphcoded` and /// `zmx` — and loads the daemon, so dragging the app to `/Applications` is the whole /// installation. @@ -226,3 +231,5 @@ public enum DaemonBootstrap { process.waitUntilExit() } } + +#endif diff --git a/GraphcodeKit/Sources/IPC/DaemonSocketClient.swift b/GraphcodeKit/Sources/IPC/DaemonSocketClient.swift index a9b9470..99a2558 100644 --- a/GraphcodeKit/Sources/IPC/DaemonSocketClient.swift +++ b/GraphcodeKit/Sources/IPC/DaemonSocketClient.swift @@ -2,6 +2,8 @@ import Foundation #if canImport(Darwin) import Darwin +#else + import Glibc #endif /// A short-lived client for `graphcoded`'s socket — what the `graphcode` CLI talks @@ -90,12 +92,19 @@ public struct DaemonSocketClient: Sendable { throw ClientError.daemonNotRunning } - let descriptor = socket(AF_UNIX, SOCK_STREAM, 0) + #if canImport(Darwin) + let descriptor = socket(AF_UNIX, SOCK_STREAM, 0) + #else + // Glibc imports SOCK_STREAM as the `__socket_type` enum, not an Int32. + let descriptor = socket(AF_UNIX, Int32(SOCK_STREAM.rawValue), 0) + #endif guard descriptor >= 0 else { throw ClientError.connectionFailed(errno: errno) } var address = sockaddr_un() address.sun_family = sa_family_t(AF_UNIX) - address.sun_len = UInt8(MemoryLayout.size) + #if canImport(Darwin) + address.sun_len = UInt8(MemoryLayout.size) + #endif withUnsafeMutablePointer(to: &address.sun_path) { field in field.withMemoryRebound( to: CChar.self, capacity: MemoryLayout.size(ofValue: field.pointee) @@ -127,7 +136,7 @@ public struct DaemonSocketClient: Sendable { private static func applyReceiveTimeout(_ timeout: TimeInterval, to descriptor: Int32) { var interval = timeval( tv_sec: Int(timeout), - tv_usec: Int32((timeout - timeout.rounded(.down)) * 1_000_000)) + tv_usec: suseconds_t((timeout - timeout.rounded(.down)) * 1_000_000)) setsockopt( descriptor, SOL_SOCKET, SO_RCVTIMEO, &interval, socklen_t(MemoryLayout.size)) } diff --git a/GraphcodeKit/Sources/IPC/FramedMessageIO.swift b/GraphcodeKit/Sources/IPC/FramedMessageIO.swift index d25206e..93d9f74 100644 --- a/GraphcodeKit/Sources/IPC/FramedMessageIO.swift +++ b/GraphcodeKit/Sources/IPC/FramedMessageIO.swift @@ -2,6 +2,8 @@ import Foundation #if canImport(Darwin) import Darwin +#else + import Glibc #endif /// Length-prefixed framing over a raw socket file descriptor — a 4-byte big-endian diff --git a/GraphcodeKit/Sources/Sessions/PTYProcessSession.swift b/GraphcodeKit/Sources/Sessions/PTYProcessSession.swift index 4d66304..28414fa 100644 --- a/GraphcodeKit/Sources/Sessions/PTYProcessSession.swift +++ b/GraphcodeKit/Sources/Sessions/PTYProcessSession.swift @@ -1,6 +1,11 @@ -import Darwin import Foundation +#if canImport(Darwin) + import Darwin +#else + import Glibc +#endif + /// One event out of a running PTY-backed process: either a chunk of raw output, or a /// terminal lifecycle transition. See docs/04-cli-backends.md. public enum PTYSessionEvent: Sendable, Equatable { @@ -40,6 +45,9 @@ public final class PTYProcessSession: @unchecked Sendable { ) throws { var master: Int32 = 0 var slave: Int32 = 0 + // Portable as-is: Glibc's module includes pty.h, and glibc ≥ 2.34 ships openpty + // in libc proper, so no libutil link is needed on Linux. The POSIX pt* trio is + // not an option — stdlib.h's feature-macro guards keep it out of Swift's Glibc. guard openpty(&master, &slave, nil, nil, nil) == 0 else { throw SessionError.failedToOpenPTY } diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..3ace216 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,24 @@ +{ + "originHash" : "2ef7a1c0cce984b320f4784a7c1bd827ec8f3e73eaba10904b94d12d131c9bea", + "pins" : [ + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-identified-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-identified-collections", + "state" : { + "revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597", + "version" : "1.1.1" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..1c24e4d --- /dev/null +++ b/Package.swift @@ -0,0 +1,47 @@ +// swift-tools-version: 6.0 +import PackageDescription + +// SwiftPM manifest for the non-UI products — GraphcodeKit, the `graphcode` CLI, and +// `graphcoded` — so they build on Linux (and anywhere else swift-corelibs Foundation +// runs), where Tuist and Xcode don't. The macOS app keeps building through +// `Project.swift`; Tuist resolves its dependencies from `Tuist/Package.swift` and +// ignores this file. See issue #83. +let package = Package( + name: "graphcode", + platforms: [.macOS(.v15)], + products: [ + .library(name: "GraphcodeKit", targets: ["GraphcodeKit"]), + .executable(name: "graphcode", targets: ["graphcode-cli"]), + .executable(name: "graphcoded", targets: ["graphcoded"]), + ], + dependencies: [ + .package( + url: "https://github.com/pointfreeco/swift-identified-collections", + from: "1.1.0" + ) + ], + targets: [ + .target( + name: "GraphcodeKit", + dependencies: [ + .product(name: "IdentifiedCollections", package: "swift-identified-collections") + ], + path: "GraphcodeKit/Sources", + // Language mode 5 to match how Tuist/Xcode builds these same sources today; + // moving the tree to strict mode 6 is its own change, not the Linux port's. + swiftSettings: [.swiftLanguageMode(.v5)] + ), + .executableTarget( + name: "graphcode-cli", + dependencies: ["GraphcodeKit"], + path: "graphcode-cli/Sources", + swiftSettings: [.swiftLanguageMode(.v5)] + ), + .executableTarget( + name: "graphcoded", + dependencies: ["GraphcodeKit"], + path: "graphcoded/Sources", + swiftSettings: [.swiftLanguageMode(.v5)] + ), + ] +) diff --git a/graphcoded/Sources/main.swift b/graphcoded/Sources/main.swift index 5258a86..f871017 100644 --- a/graphcoded/Sources/main.swift +++ b/graphcoded/Sources/main.swift @@ -3,6 +3,8 @@ import GraphcodeKit #if canImport(Darwin) import Darwin +#else + import Glibc #endif // graphcoded — the graphcode orchestrator daemon. @@ -37,14 +39,21 @@ func fail(_ message: String) -> Never { exit(1) } -let socketDescriptor = socket(AF_UNIX, SOCK_STREAM, 0) +#if canImport(Darwin) + let socketDescriptor = socket(AF_UNIX, SOCK_STREAM, 0) +#else + // Glibc imports SOCK_STREAM as the `__socket_type` enum, not an Int32. + let socketDescriptor = socket(AF_UNIX, Int32(SOCK_STREAM.rawValue), 0) +#endif guard socketDescriptor >= 0 else { fail("failed to create socket (errno \(errno))") } var address = sockaddr_un() address.sun_family = sa_family_t(AF_UNIX) -address.sun_len = UInt8(MemoryLayout.size) +#if canImport(Darwin) + address.sun_len = UInt8(MemoryLayout.size) +#endif let path = socketURL.path withUnsafeMutablePointer(to: &address.sun_path) { pathField in