Skip to content

chore: manage mutagen daemon lifecycle #98

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Mar 12, 2025
Merged
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
4 changes: 4 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# TODO: Remove this once the grpc-swift-protobuf generator adds a lint disable comment
excluded:
- "**/*.pb.swift"
- "**/*.grpc.swift"
2 changes: 1 addition & 1 deletion Coder Desktop/.swiftformat
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
--selfrequired log,info,error,debug,critical,fault
--exclude **.pb.swift
--exclude **.pb.swift,**.grpc.swift
--condassignment always
16 changes: 14 additions & 2 deletions Coder Desktop/Coder Desktop/Coder_DesktopApp.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import FluidMenuBarExtra
import NetworkExtension
import SwiftUI
import VPNLib

@main
struct DesktopApp: App {
@@ -30,10 +31,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private var menuBar: MenuBarController?
let vpn: CoderVPNService
let state: AppState
let fileSyncDaemon: MutagenDaemon

override init() {
vpn = CoderVPNService()
state = AppState(onChange: vpn.configureTunnelProviderProtocol)
fileSyncDaemon = MutagenDaemon()
}

func applicationDidFinishLaunching(_: Notification) {
@@ -56,14 +59,23 @@ class AppDelegate: NSObject, NSApplicationDelegate {
state.reconfigure()
}
}
// TODO: Start the daemon only once a file sync is configured
Task {
await fileSyncDaemon.start()
}
}

// This function MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)`
// or return `.terminateNow`
func applicationShouldTerminate(_: NSApplication) -> NSApplication.TerminateReply {
if !state.stopVPNOnQuit { return .terminateNow }
Task {
await vpn.stop()
async let vpnTask: Void = {
if await self.state.stopVPNOnQuit {
await self.vpn.stop()
}
}()
async let fileSyncTask: Void = self.fileSyncDaemon.stop()
_ = await (vpnTask, fileSyncTask)
NSApp.reply(toApplicationShouldTerminate: true)
}
return .terminateLater
1 change: 1 addition & 0 deletions Coder Desktop/Coder Desktop/State.swift
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import KeychainAccess
import NetworkExtension
import SwiftUI

@MainActor
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drive-by fix: I believe this is implicit in the ObservableObject conformance, but can't hurt to make it explicit.

class AppState: ObservableObject {
let appId = Bundle.main.bundleIdentifier!

Empty file.
225 changes: 225 additions & 0 deletions Coder Desktop/VPNLib/FileSync/FileSyncDaemon.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import Foundation
import GRPC
import NIO
import os
import Subprocess

@MainActor
public protocol FileSyncDaemon: ObservableObject {
var state: DaemonState { get }
func start() async
func stop() async
}

@MainActor
public class MutagenDaemon: FileSyncDaemon {
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "mutagen")

@Published public var state: DaemonState = .stopped {
didSet {
logger.info("daemon state changed: \(self.state.description, privacy: .public)")
}
}

private var mutagenProcess: Subprocess?
private let mutagenPath: URL!
private let mutagenDataDirectory: URL
private let mutagenDaemonSocket: URL

private var group: MultiThreadedEventLoopGroup?
private var channel: GRPCChannel?
private var client: Daemon_DaemonAsyncClient?

public init() {
#if arch(arm64)
mutagenPath = Bundle.main.url(forResource: "mutagen-darwin-arm64", withExtension: nil)
#elseif arch(x86_64)
mutagenPath = Bundle.main.url(forResource: "mutagen-darwin-amd64", withExtension: nil)
#else
fatalError("unknown architecture")
#endif
mutagenDataDirectory = FileManager.default.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
).first!.appending(path: "Coder Desktop").appending(path: "Mutagen")
mutagenDaemonSocket = mutagenDataDirectory.appending(path: "daemon").appending(path: "daemon.sock")
// It shouldn't be fatal if the app was built without Mutagen embedded,
// but file sync will be unavailable.
if mutagenPath == nil {
logger.warning("Mutagen not embedded in app, file sync will be unavailable")
state = .unavailable
}
}

public func start() async {
if case .unavailable = state { return }

// Stop an orphaned daemon, if there is one
try? await connect()
await stop()

mutagenProcess = createMutagenProcess()
// swiftlint:disable:next large_tuple
let (standardOutput, standardError, waitForExit): (Pipe.AsyncBytes, Pipe.AsyncBytes, @Sendable () async -> Void)
do {
(standardOutput, standardError, waitForExit) = try mutagenProcess!.run()
} catch {
state = .failed(DaemonError.daemonStartFailure(error))
return
}

Task {
await streamHandler(io: standardOutput)
logger.info("standard output stream closed")
}

Task {
await streamHandler(io: standardError)
logger.info("standard error stream closed")
}

Task {
await terminationHandler(waitForExit: waitForExit)
}

do {
try await connect()
} catch {
state = .failed(DaemonError.daemonStartFailure(error))
return
}

state = .running
logger.info(
"""
mutagen daemon started, pid:
\(self.mutagenProcess?.pid.description ?? "unknown", privacy: .public)
"""
)
}

private func connect() async throws(DaemonError) {
guard client == nil else {
// Already connected
return
}
group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
do {
channel = try GRPCChannelPool.with(
target: .unixDomainSocket(mutagenDaemonSocket.path),
transportSecurity: .plaintext,
eventLoopGroup: group!
)
client = Daemon_DaemonAsyncClient(channel: channel!)
logger.info(
"Successfully connected to mutagen daemon, socket: \(self.mutagenDaemonSocket.path, privacy: .public)"
)
} catch {
logger.error("Failed to connect to gRPC: \(error)")
try? await cleanupGRPC()
throw DaemonError.connectionFailure(error)
}
}

private func cleanupGRPC() async throws {
try? await channel?.close().get()
try? await group?.shutdownGracefully()

client = nil
channel = nil
group = nil
}

public func stop() async {
if case .unavailable = state { return }
state = .stopped
guard FileManager.default.fileExists(atPath: mutagenDaemonSocket.path) else {
// Already stopped
return
}

// "We don't check the response or error, because the daemon
// may terminate before it has a chance to send the response."
_ = try? await client?.terminate(
Daemon_TerminateRequest(),
callOptions: .init(timeLimit: .timeout(.milliseconds(500)))
)

try? await cleanupGRPC()

mutagenProcess?.kill()
mutagenProcess = nil
logger.info("Daemon stopped and gRPC connection closed")
}

private func createMutagenProcess() -> Subprocess {
let process = Subprocess([mutagenPath.path, "daemon", "run"])
process.environment = [
"MUTAGEN_DATA_DIRECTORY": mutagenDataDirectory.path,
]
logger.info("setting mutagen data directory: \(self.mutagenDataDirectory.path, privacy: .public)")
return process
}

private func terminationHandler(waitForExit: @Sendable () async -> Void) async {
await waitForExit()

switch state {
case .stopped:
logger.info("mutagen daemon stopped")
default:
logger.error(
"""
mutagen daemon exited unexpectedly with code:
\(self.mutagenProcess?.exitCode.description ?? "unknown")
"""
)
state = .failed(.terminatedUnexpectedly)
}
}

private func streamHandler(io: Pipe.AsyncBytes) async {
for await line in io.lines {
logger.info("\(line, privacy: .public)")
}
}
}

public enum DaemonState {
case running
case stopped
case failed(DaemonError)
case unavailable

var description: String {
switch self {
case .running:
"Running"
case .stopped:
"Stopped"
case let .failed(error):
"Failed: \(error)"
case .unavailable:
"Unavailable"
}
}
}

public enum DaemonError: Error {
case daemonStartFailure(Error)
case connectionFailure(Error)
case terminatedUnexpectedly

var description: String {
switch self {
case let .daemonStartFailure(error):
"Daemon start failure: \(error)"
case let .connectionFailure(error):
"Connection failure: \(error)"
case .terminatedUnexpectedly:
"Daemon terminated unexpectedly"
}
}

var localizedDescription: String { description }
}
299 changes: 299 additions & 0 deletions Coder Desktop/VPNLib/FileSync/daemon.grpc.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
//
// DO NOT EDIT.
// swift-format-ignore-file
//
// Generated by the protocol buffer compiler.
// Source: Coder Desktop/VPNLib/FileSync/daemon.proto
//
import GRPC
import NIO
import NIOConcurrencyHelpers
import SwiftProtobuf


/// Usage: instantiate `Daemon_DaemonClient`, then call methods of this protocol to make API calls.
internal protocol Daemon_DaemonClientProtocol: GRPCClient {
var serviceName: String { get }
var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? { get }

func terminate(
_ request: Daemon_TerminateRequest,
callOptions: CallOptions?
) -> UnaryCall<Daemon_TerminateRequest, Daemon_TerminateResponse>
}

extension Daemon_DaemonClientProtocol {
internal var serviceName: String {
return "daemon.Daemon"
}

/// Unary call to Terminate
///
/// - Parameters:
/// - request: Request to send to Terminate.
/// - callOptions: Call options.
/// - Returns: A `UnaryCall` with futures for the metadata, status and response.
internal func terminate(
_ request: Daemon_TerminateRequest,
callOptions: CallOptions? = nil
) -> UnaryCall<Daemon_TerminateRequest, Daemon_TerminateResponse> {
return self.makeUnaryCall(
path: Daemon_DaemonClientMetadata.Methods.terminate.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeTerminateInterceptors() ?? []
)
}
}

@available(*, deprecated)
extension Daemon_DaemonClient: @unchecked Sendable {}

@available(*, deprecated, renamed: "Daemon_DaemonNIOClient")
internal final class Daemon_DaemonClient: Daemon_DaemonClientProtocol {
private let lock = Lock()
private var _defaultCallOptions: CallOptions
private var _interceptors: Daemon_DaemonClientInterceptorFactoryProtocol?
internal let channel: GRPCChannel
internal var defaultCallOptions: CallOptions {
get { self.lock.withLock { return self._defaultCallOptions } }
set { self.lock.withLockVoid { self._defaultCallOptions = newValue } }
}
internal var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? {
get { self.lock.withLock { return self._interceptors } }
set { self.lock.withLockVoid { self._interceptors = newValue } }
}

/// Creates a client for the daemon.Daemon service.
///
/// - Parameters:
/// - channel: `GRPCChannel` to the service host.
/// - defaultCallOptions: Options to use for each service call if the user doesn't provide them.
/// - interceptors: A factory providing interceptors for each RPC.
internal init(
channel: GRPCChannel,
defaultCallOptions: CallOptions = CallOptions(),
interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? = nil
) {
self.channel = channel
self._defaultCallOptions = defaultCallOptions
self._interceptors = interceptors
}
}

internal struct Daemon_DaemonNIOClient: Daemon_DaemonClientProtocol {
internal var channel: GRPCChannel
internal var defaultCallOptions: CallOptions
internal var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol?

/// Creates a client for the daemon.Daemon service.
///
/// - Parameters:
/// - channel: `GRPCChannel` to the service host.
/// - defaultCallOptions: Options to use for each service call if the user doesn't provide them.
/// - interceptors: A factory providing interceptors for each RPC.
internal init(
channel: GRPCChannel,
defaultCallOptions: CallOptions = CallOptions(),
interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? = nil
) {
self.channel = channel
self.defaultCallOptions = defaultCallOptions
self.interceptors = interceptors
}
}

@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
internal protocol Daemon_DaemonAsyncClientProtocol: GRPCClient {
static var serviceDescriptor: GRPCServiceDescriptor { get }
var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? { get }

func makeTerminateCall(
_ request: Daemon_TerminateRequest,
callOptions: CallOptions?
) -> GRPCAsyncUnaryCall<Daemon_TerminateRequest, Daemon_TerminateResponse>
}

@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension Daemon_DaemonAsyncClientProtocol {
internal static var serviceDescriptor: GRPCServiceDescriptor {
return Daemon_DaemonClientMetadata.serviceDescriptor
}

internal var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? {
return nil
}

internal func makeTerminateCall(
_ request: Daemon_TerminateRequest,
callOptions: CallOptions? = nil
) -> GRPCAsyncUnaryCall<Daemon_TerminateRequest, Daemon_TerminateResponse> {
return self.makeAsyncUnaryCall(
path: Daemon_DaemonClientMetadata.Methods.terminate.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeTerminateInterceptors() ?? []
)
}
}

@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension Daemon_DaemonAsyncClientProtocol {
internal func terminate(
_ request: Daemon_TerminateRequest,
callOptions: CallOptions? = nil
) async throws -> Daemon_TerminateResponse {
return try await self.performAsyncUnaryCall(
path: Daemon_DaemonClientMetadata.Methods.terminate.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeTerminateInterceptors() ?? []
)
}
}

@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
internal struct Daemon_DaemonAsyncClient: Daemon_DaemonAsyncClientProtocol {
internal var channel: GRPCChannel
internal var defaultCallOptions: CallOptions
internal var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol?

internal init(
channel: GRPCChannel,
defaultCallOptions: CallOptions = CallOptions(),
interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? = nil
) {
self.channel = channel
self.defaultCallOptions = defaultCallOptions
self.interceptors = interceptors
}
}

internal protocol Daemon_DaemonClientInterceptorFactoryProtocol: Sendable {

/// - Returns: Interceptors to use when invoking 'terminate'.
func makeTerminateInterceptors() -> [ClientInterceptor<Daemon_TerminateRequest, Daemon_TerminateResponse>]
}

internal enum Daemon_DaemonClientMetadata {
internal static let serviceDescriptor = GRPCServiceDescriptor(
name: "Daemon",
fullName: "daemon.Daemon",
methods: [
Daemon_DaemonClientMetadata.Methods.terminate,
]
)

internal enum Methods {
internal static let terminate = GRPCMethodDescriptor(
name: "Terminate",
path: "/daemon.Daemon/Terminate",
type: GRPCCallType.unary
)
}
}

/// To build a server, implement a class that conforms to this protocol.
internal protocol Daemon_DaemonProvider: CallHandlerProvider {
var interceptors: Daemon_DaemonServerInterceptorFactoryProtocol? { get }

func terminate(request: Daemon_TerminateRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Daemon_TerminateResponse>
}

extension Daemon_DaemonProvider {
internal var serviceName: Substring {
return Daemon_DaemonServerMetadata.serviceDescriptor.fullName[...]
}

/// Determines, calls and returns the appropriate request handler, depending on the request's method.
/// Returns nil for methods not handled by this service.
internal func handle(
method name: Substring,
context: CallHandlerContext
) -> GRPCServerHandlerProtocol? {
switch name {
case "Terminate":
return UnaryServerHandler(
context: context,
requestDeserializer: ProtobufDeserializer<Daemon_TerminateRequest>(),
responseSerializer: ProtobufSerializer<Daemon_TerminateResponse>(),
interceptors: self.interceptors?.makeTerminateInterceptors() ?? [],
userFunction: self.terminate(request:context:)
)

default:
return nil
}
}
}

/// To implement a server, implement an object which conforms to this protocol.
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
internal protocol Daemon_DaemonAsyncProvider: CallHandlerProvider, Sendable {
static var serviceDescriptor: GRPCServiceDescriptor { get }
var interceptors: Daemon_DaemonServerInterceptorFactoryProtocol? { get }

func terminate(
request: Daemon_TerminateRequest,
context: GRPCAsyncServerCallContext
) async throws -> Daemon_TerminateResponse
}

@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension Daemon_DaemonAsyncProvider {
internal static var serviceDescriptor: GRPCServiceDescriptor {
return Daemon_DaemonServerMetadata.serviceDescriptor
}

internal var serviceName: Substring {
return Daemon_DaemonServerMetadata.serviceDescriptor.fullName[...]
}

internal var interceptors: Daemon_DaemonServerInterceptorFactoryProtocol? {
return nil
}

internal func handle(
method name: Substring,
context: CallHandlerContext
) -> GRPCServerHandlerProtocol? {
switch name {
case "Terminate":
return GRPCAsyncServerHandler(
context: context,
requestDeserializer: ProtobufDeserializer<Daemon_TerminateRequest>(),
responseSerializer: ProtobufSerializer<Daemon_TerminateResponse>(),
interceptors: self.interceptors?.makeTerminateInterceptors() ?? [],
wrapping: { try await self.terminate(request: $0, context: $1) }
)

default:
return nil
}
}
}

internal protocol Daemon_DaemonServerInterceptorFactoryProtocol: Sendable {

/// - Returns: Interceptors to use when handling 'terminate'.
/// Defaults to calling `self.makeInterceptors()`.
func makeTerminateInterceptors() -> [ServerInterceptor<Daemon_TerminateRequest, Daemon_TerminateResponse>]
}

internal enum Daemon_DaemonServerMetadata {
internal static let serviceDescriptor = GRPCServiceDescriptor(
name: "Daemon",
fullName: "daemon.Daemon",
methods: [
Daemon_DaemonServerMetadata.Methods.terminate,
]
)

internal enum Methods {
internal static let terminate = GRPCMethodDescriptor(
name: "Terminate",
path: "/daemon.Daemon/Terminate",
type: GRPCCallType.unary
)
}
}
83 changes: 83 additions & 0 deletions Coder Desktop/VPNLib/FileSync/daemon.pb.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
//
// Generated by the Swift generator plugin for the protocol buffer compiler.
// Source: Coder Desktop/VPNLib/FileSync/daemon.proto
//
// For information on using the generated types, please see the documentation:
// https://github.com/apple/swift-protobuf/

import SwiftProtobuf

// If the compiler emits an error on this type, it is because this file
// was generated by a version of the `protoc` Swift plug-in that is
// incompatible with the version of SwiftProtobuf to which you are linking.
// Please ensure that you are building against the same version of the API
// that was used to generate this file.
fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
typealias Version = _2
}

struct Daemon_TerminateRequest: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.

var unknownFields = SwiftProtobuf.UnknownStorage()

init() {}
}

struct Daemon_TerminateResponse: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.

var unknownFields = SwiftProtobuf.UnknownStorage()

init() {}
}

// MARK: - Code below here is support for the SwiftProtobuf runtime.

fileprivate let _protobuf_package = "daemon"

extension Daemon_TerminateRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".TerminateRequest"
static let _protobuf_nameMap = SwiftProtobuf._NameMap()

mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
// Load everything into unknown fields
while try decoder.nextFieldNumber() != nil {}
}

func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
try unknownFields.traverse(visitor: &visitor)
}

static func ==(lhs: Daemon_TerminateRequest, rhs: Daemon_TerminateRequest) -> Bool {
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}

extension Daemon_TerminateResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".TerminateResponse"
static let _protobuf_nameMap = SwiftProtobuf._NameMap()

mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
// Load everything into unknown fields
while try decoder.nextFieldNumber() != nil {}
}

func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
try unknownFields.traverse(visitor: &visitor)
}

static func ==(lhs: Daemon_TerminateResponse, rhs: Daemon_TerminateResponse) -> Bool {
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
11 changes: 11 additions & 0 deletions Coder Desktop/VPNLib/FileSync/daemon.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
syntax = "proto3";

package daemon;

message TerminateRequest{}

message TerminateResponse{}

service Daemon {
rpc Terminate(TerminateRequest) returns (TerminateResponse) {}
}
25 changes: 23 additions & 2 deletions Coder Desktop/project.yml
Original file line number Diff line number Diff line change
@@ -5,6 +5,9 @@ options:
macOS: "14.0"
xcodeVersion: "1600"
minimumXcodeGenVersion: "2.42.0"
fileTypes:
proto:
buildPhase: none

settings:
base:
@@ -105,13 +108,22 @@ packages:
LaunchAtLogin:
url: https://github.com/sindresorhus/LaunchAtLogin-modern
from: 1.1.0
GRPC:
url: https://github.com/grpc/grpc-swift
# v2 does not support macOS 14.0
exactVersion: 1.24.2
Subprocess:
url: https://github.com/jamf/Subprocess
revision: 9d67b79

targets:
Coder Desktop:
type: application
platform: macOS
sources:
- path: Coder Desktop
- path: Resources
buildPhase: resources
entitlements:
path: Coder Desktop/Coder_Desktop.entitlements
properties:
@@ -145,11 +157,16 @@ targets:
DSTROOT: $(LOCAL_APPS_DIR)/Coder
INSTALL_PATH: /
SKIP_INSTALL: NO
LD_RUNPATH_SEARCH_PATHS:
# Load frameworks from the SE bundle.
- "@executable_path/../../Contents/Library/SystemExtensions/com.coder.Coder-Desktop.VPN.systemextension/Contents/Frameworks"
- "@executable_path/../Frameworks"
- "@loader_path/Frameworks"
dependencies:
- target: CoderSDK
embed: true
embed: false # Loaded from SE bundle
- target: VPNLib
embed: true
embed: false # Loaded from SE bundle
- target: VPN
embed: without-signing # Embed without signing.
- package: FluidMenuBarExtra
@@ -224,8 +241,10 @@ targets:
# Empty outside of release builds
PROVISIONING_PROFILE_SPECIFIER: ${EXT_PROVISIONING_PROFILE_ID}
dependencies:
# The app loads the framework embedded here too
- target: VPNLib
embed: true
# The app loads the framework embedded here too
- target: CoderSDK
embed: true
- sdk: NetworkExtension.framework
@@ -253,6 +272,8 @@ targets:
- package: SwiftProtobuf
- package: SwiftProtobuf
product: SwiftProtobufPluginLibrary
- package: GRPC
- package: Subprocess
- target: CoderSDK
embed: false

11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -33,7 +33,8 @@ APP_SIGNING_KEYCHAIN := $(if $(wildcard $(KEYCHAIN_FILE)),$(shell realpath $(KEY
.PHONY: setup
setup: \
$(XCPROJECT) \
$(PROJECT)/VPNLib/vpn.pb.swift
$(PROJECT)/VPNLib/vpn.pb.swift \
$(PROJECT)/VPNLib/FileSync/daemon.pb.swift

$(XCPROJECT): $(PROJECT)/project.yml
cd $(PROJECT); \
@@ -48,6 +49,12 @@ $(XCPROJECT): $(PROJECT)/project.yml
$(PROJECT)/VPNLib/vpn.pb.swift: $(PROJECT)/VPNLib/vpn.proto
protoc --swift_opt=Visibility=public --swift_out=. 'Coder Desktop/VPNLib/vpn.proto'

$(PROJECT)/VPNLib/FileSync/daemon.pb.swift: $(PROJECT)/VPNLib/FileSync/daemon.proto
protoc \
--swift_out=.\
--grpc-swift_out=. \
'Coder Desktop/VPNLib/FileSync/daemon.proto'

$(KEYCHAIN_FILE):
security create-keychain -p "" "$(APP_SIGNING_KEYCHAIN)"
security set-keychain-settings -lut 21600 "$(APP_SIGNING_KEYCHAIN)"
@@ -130,7 +137,7 @@ clean/build:
rm -rf build/ release/ $$out

.PHONY: proto
proto: $(PROJECT)/VPNLib/vpn.pb.swift ## Generate Swift files from protobufs
proto: $(PROJECT)/VPNLib/vpn.pb.swift $(PROJECT)/VPNLib/FileSync/daemon.pb.swift ## Generate Swift files from protobufs

.PHONY: help
help: ## Show this help
63 changes: 63 additions & 0 deletions flake.lock
15 changes: 13 additions & 2 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -4,13 +4,23 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
flake-parts = {
url = "github:hercules-ci/flake-parts";
inputs.nixpkgs-lib.follows = "nixpkgs";
};
grpc-swift = {
url = "github:i10416/grpc-swift-flake";
inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-parts.follows = "flake-parts";
};
};

outputs =
{
self,
nixpkgs,
flake-utils,
grpc-swift,
...
}:
flake-utils.lib.eachSystem
(with flake-utils.lib.system; [
@@ -40,7 +50,8 @@
git
gnumake
protobuf_28
protoc-gen-swift
grpc-swift.packages.${system}.protoc-gen-grpc-swift
grpc-swift.packages.${system}.protoc-gen-swift
swiftformat
swiftlint
xcbeautify