Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions GraphcodeKit/Sources/DaemonBootstrap.swift
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -226,3 +231,5 @@ public enum DaemonBootstrap {
process.waitUntilExit()
}
}

#endif
15 changes: 12 additions & 3 deletions GraphcodeKit/Sources/IPC/DaemonSocketClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<sockaddr_un>.size)
#if canImport(Darwin)
address.sun_len = UInt8(MemoryLayout<sockaddr_un>.size)
#endif
withUnsafeMutablePointer(to: &address.sun_path) { field in
field.withMemoryRebound(
to: CChar.self, capacity: MemoryLayout.size(ofValue: field.pointee)
Expand Down Expand Up @@ -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<timeval>.size))
}
Expand Down
2 changes: 2 additions & 0 deletions GraphcodeKit/Sources/IPC/FramedMessageIO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion GraphcodeKit/Sources/Sessions/PTYProcessSession.swift
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
24 changes: 24 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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)]
),
]
)
13 changes: 11 additions & 2 deletions graphcoded/Sources/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import GraphcodeKit

#if canImport(Darwin)
import Darwin
#else
import Glibc
#endif

// graphcoded — the graphcode orchestrator daemon.
Expand Down Expand Up @@ -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<sockaddr_un>.size)
#if canImport(Darwin)
address.sun_len = UInt8(MemoryLayout<sockaddr_un>.size)
#endif

let path = socketURL.path
withUnsafeMutablePointer(to: &address.sun_path) { pathField in
Expand Down
Loading