Skip to content

chore: switch to an XcodeGen project file #32

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 1 commit into from
Jan 29, 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
13 changes: 13 additions & 0 deletions .github/actions/nix-devshell/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: "Setup Nix devshell"
description: "This action sets up a nix devshell environment"
runs:
using: "composite"
steps:
- name: Setup Nix
uses: DeterminateSystems/nix-installer-action@e50d5f73bfe71c2dd0aa4218de8f4afa59f8f81d # v16

- name: Setup GHA Nix cache
uses: DeterminateSystems/magic-nix-cache-action@6221693898146dc97e38ad0e013488a16477a4c4 # v9

- name: Enter devshell
uses: nicknovitski/nix-develop@9be7cfb4b10451d3390a75dc18ad0465bed4932a # v1.2.1
56 changes: 44 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -10,7 +10,6 @@ on:
paths-ignore:
- "README.md"


permissions:
contents: read

@@ -19,36 +18,69 @@ jobs:
name: test
runs-on: ${{ github.repository_owner == 'coder' && 'depot-macos-latest' || 'macos-latest'}}
steps:
- name: Harden Runner
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
with:
egress-policy: audit

- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 1

- name: Switch XCode Version
uses: maxim-lobanov/setup-xcode@v1
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
with:
xcode-version: '16.0.0'
- run: |
make test
# (ThomasK33): depot.dev does not yet support Xcode 16.1 or 16.2 GA, thus we're stuck with 16.0.0 for now.
# I've already reached out, so hopefully this comment will soon be obsolete.
xcode-version: "16.0.0"

- name: Setup Nix
uses: ./.github/actions/nix-devshell

- run: make test

format:
name: fmt
runs-on: ${{ github.repository_owner == 'coder' && 'depot-macos-latest' || 'macos-latest'}}
steps:
- name: Harden Runner
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
with:
egress-policy: audit

- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 1
- run: |
make fmt

- name: Switch XCode Version
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
with:
# (ThomasK33): depot.dev does not yet support Xcode 16.1 or 16.2 GA, thus we're stuck with 16.0.0 for now.
# I've already reached out, so hopefully this comment will soon be obsolete.
xcode-version: "16.0.0"

- name: Setup Nix
uses: ./.github/actions/nix-devshell

- run: make fmt

lint:
name: lint
runs-on: ${{ github.repository_owner == 'coder' && 'depot-macos-latest' || 'macos-latest'}}
steps:
- name: Harden Runner
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
with:
egress-policy: audit

- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 1
- name: Install Swiftlint
run: |
brew install swiftlint
- run: |
make lint

- name: Setup Nix
uses: ./.github/actions/nix-devshell

- run: make lint
294 changes: 290 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,293 @@
# Xcode specifics
# Created by https://www.toptal.com/developers/gitignore/api/xcode,jetbrains,macos,direnv,swift,swiftpm,objective-c
# Edit at https://www.toptal.com/developers/gitignore?templates=xcode,jetbrains,macos,direnv,swift,swiftpm,objective-c

### direnv ###
.direnv
.envrc

### JetBrains ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf

# AWS User-specific
.idea/**/aws.xml

# Generated files
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml

# Gradle
.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr

# CMake
cmake-build-*/

# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
*.iws

# IntelliJ
out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# SonarLint plugin
.idea/sonarlint/

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client
.idea/httpRequests

# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser

### JetBrains Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721

# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr

# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/

# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml

# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/

# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$

# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml

# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml

### macOS ###
# General
.DS_Store
UserInterfaceState.xcuserstate
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon


# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

### macOS Patch ###
# iCloud generated files
*.icloud

### Objective-C ###
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## User settings
xcuserdata/

# JetBrains
.idea/
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout

## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3

## Obj-C/Swift specific
*.hmap

## App packaging
*.ipa
*.dSYM.zip
*.dSYM

# CocoaPods
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
# Pods/
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace

# Carthage
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts

Carthage/Build/

# fastlane
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control

fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

# Code Injection
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode

iOSInjectionProject/

### Objective-C Patch ###

### Swift ###
# Xcode
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore






## Playgrounds
timeline.xctimeline
playground.xcworkspace

# Swift Package Manager
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
Packages/
Package.pins
Package.resolved
*.xcodeproj
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm

.build/

# CocoaPods
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
# Pods/
# Add this line if you want to avoid checking in source code from the Xcode workspace
*.xcworkspace

# Carthage
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts


# Accio dependency management
Dependencies/
.accio/

# fastlane
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control


# Code Injection
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode


### SwiftPM ###
Packages
xcuserdata
*.xcodeproj


### Xcode ###

## Xcode 8 and earlier

### Xcode Patch ###
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
/*.gcno
**/xcshareddata/WorkspaceSettings.xcsettings

# End of https://www.toptal.com/developers/gitignore/api/xcode,jetbrains,macos,direnv,swift,swiftpm,objective-c
142 changes: 142 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Contributing to Coder Desktop

Thank you for your interest in contributing to Coder Desktop! Below are the
guidelines to help you get started.

## Prerequisites

Before opening the project in Xcode, you need to generate the Xcode project files.
We use [**XcodeGen**](https://github.com/yonaskolb/XcodeGen) to handle this
process, and the project generation is integrated into the `Makefile`.

## Setting Up the Development Environment

To ensure a consistent and reliable development environment, we recommend using
[**Nix**](https://nix.dev/) with Flake support. All the tools required for
development are defined in the `flake.nix` file.

**Note:** Nix is the only supported development environment for this project.
While setups outside of Nix may work, we do not support custom tool installations
or address issues related to missing path setups or other tooling installation
problems. Using Nix ensures consistency across development environments and avoids
these potential issues.

### Installing Nix with Flakes Enabled

If you don’t already have Nix installed, you can:

1. Use the [Determinate Systems installer](https://nixinstaller.com/) for a
simple setup.
2. Alternatively, use the [official installer](https://nixos.org/download.html)
and enable Flake support by adding the following to your Nix configuration:

```nix
experimental-features = nix-command flakes
```

This project does **not** support non-Flake versions of Nix.

### Entering the Development Environment

Run the following command to enter the development environment with all necessary
tools:

```bash
nix develop
```

### Using `direnv` for Environment Automation (Optional)

As an optional recommendation, you can use [`direnv`](https://direnv.net/) to
automatically load and unload the Nix development environment when you navigate
to the project directory. After installing `direnv`, enable it for this project by:

1. Adding the following line to your `.envrc` file in the project directory:

```bash
use flake
```

2. Allowing the `.envrc` file by running:

```bash
direnv allow
```

With `direnv`, the development environment will automatically be set up whenever
you enter the project directory. This step is optional but can significantly
streamline your workflow.

## Generating the Xcode Project Files

Once your development environment is set up, generate the Xcode project files by
running:

```bash
make
```

This will use **XcodeGen** to create the required Xcode project files.
The configuration for the project is defined in `Coder Desktop/project.yml`.

## Common Make Commands

Here are some useful `make` commands for working with the project:

- `make fmt`: Format Swift files using SwiftFormat.
- `make lint`: Lint Swift files using SwiftLint.
- `make test`: Run all tests using `xcodebuild`.
- `make clean`: Clean the Xcode project.
- `make proto`: Generate Swift files from protobufs.
- `make help`: Display all available `make` commands with descriptions.

For continuous development, you can also use:

```bash
make watch-gen
```

This command watches for changes to `Coder Desktop/project.yml` and regenerates
the Xcode project file as needed.

## Testing and Formatting

To maintain code quality, ensure you run the following before submitting any changes:

1. **Format Swift files:**

```bash
make fmt
```

2. **Lint Swift files:**

```bash
make lint
```

3. **Run tests:**

```bash
make test
```

## Contributing Workflow

1. Fork the repository and create your feature branch:

```bash
git checkout -b feature/your-feature-name
```

2. Make your changes and commit them with clear messages.
3. Push your branch to your forked repository:

```bash
git push origin feature/your-feature-name
```

4. Open a pull request to the main repository.

Thank you for contributing! If you have any questions or need further assistance,
feel free to open an issue.
1,659 changes: 0 additions & 1,659 deletions Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

8 changes: 4 additions & 4 deletions Coder Desktop/Coder Desktop/NetworkExtension.swift
Original file line number Diff line number Diff line change
@@ -10,13 +10,13 @@ enum NetworkExtensionState: Equatable {
var description: String {
switch self {
case .unconfigured:
return "NetworkExtension not configured, try logging in again"
"NetworkExtension not configured, try logging in again"
case .enabled:
return "NetworkExtension tunnel enabled"
"NetworkExtension tunnel enabled"
case .disabled:
return "NetworkExtension tunnel disabled"
"NetworkExtension tunnel disabled"
case let .failed(error):
return "NetworkExtension config failed: \(error)"
"NetworkExtension config failed: \(error)"
}
}
}
Original file line number Diff line number Diff line change
@@ -24,6 +24,6 @@ class PreviewSession: Session {
}

func tunnelProviderProtocol() -> NETunnelProviderProtocol? {
return nil
nil
}
}
4 changes: 2 additions & 2 deletions Coder Desktop/Coder Desktop/State.swift
Original file line number Diff line number Diff line change
@@ -78,7 +78,7 @@ class SecureSession: ObservableObject, Session {
}

private func keychainSet(_ value: String?, for key: String) {
if let value = value {
if let value {
try? keychain.set(value, key: key)
} else {
try? keychain.remove(key)
@@ -132,6 +132,6 @@ struct LiteralHeader: Hashable, Identifiable, Equatable, Codable {

extension LiteralHeader {
func toSDKHeader() -> HTTPHeader {
return .init(header: header, value: value)
.init(header: header, value: value)
}
}
8 changes: 4 additions & 4 deletions Coder Desktop/Coder Desktop/SystemExtension.swift
Original file line number Diff line number Diff line change
@@ -11,13 +11,13 @@ enum SystemExtensionState: Equatable, Sendable {
var description: String {
switch self {
case .uninstalled:
return "VPN SystemExtension is waiting to be activated"
"VPN SystemExtension is waiting to be activated"
case .needsUserApproval:
return "VPN SystemExtension needs user approval to activate"
"VPN SystemExtension needs user approval to activate"
case .installed:
return "VPN SystemExtension is installed"
"VPN SystemExtension is installed"
case let .failed(error):
return "VPN SystemExtension failed with error: \(error)"
"VPN SystemExtension failed with error: \(error)"
}
}
}
8 changes: 4 additions & 4 deletions Coder Desktop/Coder Desktop/VPNService.swift
Original file line number Diff line number Diff line change
@@ -29,13 +29,13 @@ enum VPNServiceError: Error, Equatable {
var description: String {
switch self {
case .longTestError:
return "This is a long error to test the UI with long errors"
"This is a long error to test the UI with long errors"
case let .internalError(description):
return "Internal Error: \(description)"
"Internal Error: \(description)"
case let .systemExtensionError(state):
return state.description
state.description
case let .networkExtensionError(state):
return state.description
state.description
}
}
}
10 changes: 5 additions & 5 deletions Coder Desktop/Coder Desktop/Views/Agent.swift
Original file line number Diff line number Diff line change
@@ -16,10 +16,10 @@ enum AgentStatus: Equatable {

public var color: Color {
switch self {
case .okay: return .green
case .warn: return .yellow
case .error: return .red
case .off: return .gray
case .okay: .green
case .warn: .yellow
case .error: .red
case .off: .gray
}
}
}
@@ -41,7 +41,7 @@ struct AgentRowView: View {

private var wsURL: URL {
// TODO: CoderVPN currently only supports owned workspaces
return baseAccessURL.appending(path: "@me").appending(path: workspace.workspaceName)
baseAccessURL.appending(path: "@me").appending(path: workspace.workspaceName)
}

var body: some View {
2 changes: 1 addition & 1 deletion Coder Desktop/Coder Desktop/Views/Agents.swift
Original file line number Diff line number Diff line change
@@ -27,6 +27,6 @@ struct Agents<VPN: VPNService, S: Session>: View {
}.toggleStyle(.button).buttonStyle(.plain)
}
}
}.onReceive(inspection.notice) { self.inspection.visit(self, $0) } // ViewInspector
}.onReceive(inspection.notice) { inspection.visit(self, $0) } // ViewInspector
}
}
6 changes: 3 additions & 3 deletions Coder Desktop/Coder Desktop/Views/LoginForm.swift
Original file line number Diff line number Diff line change
@@ -55,7 +55,7 @@ struct LoginForm<S: Session>: View {
}.disabled(loading)
.frame(width: 550)
.fixedSize()
.onReceive(inspection.notice) { self.inspection.visit(self, $0) } // ViewInspector
.onReceive(inspection.notice) { inspection.visit(self, $0) } // ViewInspector
}

func submit() async {
@@ -177,9 +177,9 @@ enum LoginError {
var description: String {
switch self {
case .invalidURL:
return "Invalid URL"
"Invalid URL"
case let .failedAuth(err):
return "Could not authenticate with Coder deployment:\n\(err.description)"
"Could not authenticate with Coder deployment:\n\(err.description)"
}
}
}
Original file line number Diff line number Diff line change
@@ -26,8 +26,8 @@ struct LiteralHeaderModal: View {
}.padding(20)
}.onAppear {
if let existingHeader {
self.header = existingHeader.header
self.value = existingHeader.value
header = existingHeader.header
value = existingHeader.value
}
}
}
Original file line number Diff line number Diff line change
@@ -66,6 +66,6 @@ struct LiteralHeadersSection<VPN: VPNService>: View {
}.onTapGesture {
selectedHeader = nil
}.disabled(vpn.state != .disabled)
.onReceive(inspection.notice) { self.inspection.visit(self, $0) } // ViewInspector
.onReceive(inspection.notice) { inspection.visit(self, $0) } // ViewInspector
}
}
8 changes: 4 additions & 4 deletions Coder Desktop/Coder Desktop/Views/VPNMenu.swift
Original file line number Diff line number Diff line change
@@ -14,9 +14,9 @@ struct VPNMenu<VPN: VPNService, S: Session>: View {
VStack(alignment: .leading, spacing: Theme.Size.trayPadding) {
HStack {
Toggle(isOn: Binding(
get: { self.vpn.state == .connected || self.vpn.state == .connecting },
get: { vpn.state == .connected || vpn.state == .connecting },
set: { isOn in Task {
if isOn { await self.vpn.start() } else { await self.vpn.stop() }
if isOn { await vpn.start() } else { await vpn.stop() }
}
}
)) {
@@ -78,11 +78,11 @@ struct VPNMenu<VPN: VPNService, S: Session>: View {
}.padding(.bottom, Theme.Size.trayMargin)
.environmentObject(vpn)
.environmentObject(session)
.onReceive(inspection.notice) { self.inspection.visit(self, $0) } // ViewInspector
.onReceive(inspection.notice) { inspection.visit(self, $0) } // ViewInspector
}

private var vpnDisabled: Bool {
return !session.hasSession ||
!session.hasSession ||
vpn.state == .connecting ||
vpn.state == .disconnecting
}
2 changes: 1 addition & 1 deletion Coder Desktop/Coder DesktopTests/AgentsTests.swift
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ struct AgentsTests {
}

private func createMockAgents(count: Int) -> [Agent] {
return (1 ... count).map {
(1 ... count).map {
Agent(
id: UUID(),
name: "a\($0)",
2 changes: 1 addition & 1 deletion Coder Desktop/Coder DesktopTests/Util.swift
Original file line number Diff line number Diff line change
@@ -46,7 +46,7 @@ class MockSession: Session {
}

func tunnelProviderProtocol() -> NETunnelProviderProtocol? {
return nil
nil
}
}

22 changes: 11 additions & 11 deletions Coder Desktop/CoderSDK/Client.swift
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ public struct Client {
method: HTTPMethod,
body: Data? = nil
) async throws(ClientError) -> HTTPResponse {
let url = self.url.appendingPathComponent(path)
let url = url.appendingPathComponent(path)
var req = URLRequest(url: url)
if let token { req.addValue(token, forHTTPHeaderField: Headers.sessionToken) }
req.httpMethod = method.rawValue
@@ -49,10 +49,10 @@ public struct Client {
return HTTPResponse(resp: httpResponse, data: data, req: req)
}

func request<T: Encodable & Sendable>(
func request(
_ path: String,
method: HTTPMethod,
body: T
body: some Encodable & Sendable
) async throws(ClientError) -> HTTPResponse {
let encodedBody: Data?
do {
@@ -67,7 +67,7 @@ public struct Client {
_ path: String,
method: HTTPMethod
) async throws(ClientError) -> HTTPResponse {
return try await doRequest(path: path, method: method)
try await doRequest(path: path, method: method)
}

func responseAsError(_ resp: HTTPResponse) -> ClientError {
@@ -86,7 +86,7 @@ public struct Client {
}
}

public struct APIError: Decodable {
public struct APIError: Decodable, Sendable {
let response: Response
let statusCode: Int
let method: String
@@ -105,13 +105,13 @@ public struct APIError: Decodable {
}
}

public struct Response: Decodable {
public struct Response: Decodable, Sendable {
let message: String
let detail: String?
let validations: [FieldValidation]?
}

public struct FieldValidation: Decodable {
public struct FieldValidation: Decodable, Sendable {
let field: String
let detail: String
}
@@ -125,13 +125,13 @@ public enum ClientError: Error {
public var description: String {
switch self {
case let .api(error):
return error.description
error.description
case let .network(error):
return error.localizedDescription
error.localizedDescription
case let .unexpectedResponse(data):
return "Unexpected or non HTTP response: \(data)"
"Unexpected or non HTTP response: \(data)"
case let .encodeFailure(error):
return "Failed to encode body: \(error)"
"Failed to encode body: \(error)"
}
}
}
4 changes: 3 additions & 1 deletion Coder Desktop/CoderSDK/Deployment.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Foundation

public extension Client {
func buildInfo() async throws(ClientError) -> BuildInfoResponse {
let res = try await request("/api/v2/buildinfo", method: .get)
@@ -25,7 +27,7 @@ public struct BuildInfoResponse: Encodable, Decodable, Equatable, Sendable {

// `version` in the form `[0-9]+.[0-9]+.[0-9]+`
public var semver: String? {
return try? NSRegularExpression(pattern: #"v(\d+\.\d+\.\d+)"#)
try? NSRegularExpression(pattern: #"v(\d+\.\d+\.\d+)"#)
.firstMatch(in: version, range: NSRange(version.startIndex ..< version.endIndex, in: version))
.flatMap { Range($0.range(at: 1), in: version).map { String(version[$0]) } }
}
2 changes: 2 additions & 0 deletions Coder Desktop/CoderSDK/HTTP.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Foundation

public struct HTTPResponse {
let resp: HTTPURLResponse
let data: Data
1 change: 1 addition & 0 deletions Coder Desktop/CoderSDKTests/CoderSDKTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@testable import CoderSDK
import Foundation
import Mocker
import Testing

18 changes: 9 additions & 9 deletions Coder Desktop/VPN/Manager.swift
Original file line number Diff line number Diff line change
@@ -208,23 +208,23 @@ enum ManagerError: Error {
var description: String {
switch self {
case let .download(err):
return "Download error: \(err)"
"Download error: \(err)"
case let .tunnelSetup(err):
return "Tunnel setup error: \(err)"
"Tunnel setup error: \(err)"
case let .handshake(err):
return "Handshake error: \(err)"
"Handshake error: \(err)"
case let .validation(err):
return "Validation error: \(err)"
"Validation error: \(err)"
case .incorrectResponse:
return "Received unexpected response over tunnel"
"Received unexpected response over tunnel"
case let .failedRPC(err):
return "Failed rpc: \(err)"
"Failed rpc: \(err)"
case let .serverInfo(msg):
return msg
msg
case let .errorResponse(msg):
return msg
msg
case .noTunnelFileDescriptor:
return "Could not find a tunnel file descriptor"
"Could not find a tunnel file descriptor"
}
}
}
20 changes: 10 additions & 10 deletions Coder Desktop/VPN/TunnelHandle.swift
Original file line number Diff line number Diff line change
@@ -75,11 +75,11 @@ enum TunnelHandleError: Error {

var description: String {
switch self {
case let .pipe(err): return "pipe error: \(err)"
case let .dylib(d): return d
case let .symbol(symbol, message): return "\(symbol): \(message)"
case let .openTunnel(error): return "OpenTunnel: \(error.message)"
case let .close(errs): return "close tunnel: \(errs.map(\.localizedDescription).joined(separator: ", "))"
case let .pipe(err): "pipe error: \(err)"
case let .dylib(d): d
case let .symbol(symbol, message): "\(symbol): \(message)"
case let .openTunnel(error): "OpenTunnel: \(error.message)"
case let .close(errs): "close tunnel: \(errs.map(\.localizedDescription).joined(separator: ", "))"
}
}
}
@@ -93,11 +93,11 @@ enum OpenTunnelError: Int32 {

var message: String {
switch self {
case .errDupReadFD: return "Failed to duplicate read file descriptor"
case .errDupWriteFD: return "Failed to duplicate write file descriptor"
case .errOpenPipe: return "Failed to open the pipe"
case .errNewTunnel: return "Failed to create a new tunnel"
case .unknown: return "Unknown error code"
case .errDupReadFD: "Failed to duplicate read file descriptor"
case .errDupWriteFD: "Failed to duplicate write file descriptor"
case .errOpenPipe: "Failed to open the pipe"
case .errNewTunnel: "Failed to create a new tunnel"
case .unknown: "Unknown error code"
}
}
}
2 changes: 2 additions & 0 deletions Coder Desktop/VPN/VPN.entitlements
Original file line number Diff line number Diff line change
@@ -14,5 +14,7 @@
</array>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
24 changes: 12 additions & 12 deletions Coder Desktop/VPNLib/Download.swift
Original file line number Diff line number Diff line change
@@ -14,21 +14,21 @@ public enum ValidationError: Error {
public var errorDescription: String? {
switch self {
case .fileNotFound:
return "The file does not exist."
"The file does not exist."
case .unableToCreateStaticCode:
return "Unable to create a static code object."
"Unable to create a static code object."
case .invalidSignature:
return "The file's signature is invalid."
"The file's signature is invalid."
case .unableToRetrieveInfo:
return "Unable to retrieve signing information."
"Unable to retrieve signing information."
case let .invalidIdentifier(identifier):
return "Invalid identifier: \(identifier ?? "unknown")."
"Invalid identifier: \(identifier ?? "unknown")."
case let .invalidVersion(version):
return "Invalid runtime version: \(version ?? "unknown")."
"Invalid runtime version: \(version ?? "unknown")."
case let .invalidTeamIdentifier(identifier):
return "Invalid team identifier: \(identifier ?? "unknown")."
"Invalid team identifier: \(identifier ?? "unknown")."
case .missingInfoPList:
return "Info.plist is not embedded within the dylib."
"Info.plist is not embedded within the dylib."
}
}
}
@@ -159,13 +159,13 @@ public enum DownloadError: Error {
var localizedDescription: String {
switch self {
case let .unexpectedStatusCode(code):
return "Unexpected HTTP status code: \(code)"
"Unexpected HTTP status code: \(code)"
case let .networkError(error):
return "Network error: \(error.localizedDescription)"
"Network error: \(error.localizedDescription)"
case let .fileOpError(error):
return "File operation error: \(error.localizedDescription)"
"File operation error: \(error.localizedDescription)"
case .invalidResponse:
return "Received non-HTTP response"
"Received non-HTTP response"
}
}
}
4 changes: 2 additions & 2 deletions Coder Desktop/VPNLib/Speaker.swift
Original file line number Diff line number Diff line change
@@ -95,7 +95,7 @@ public actor Speaker<SendMsg: RPCMessage & Message, RecvMsg: RPCMessage & Messag

/// Send a unary RPC message and handle the response
public func unaryRPC(_ req: SendMsg) async throws -> RecvMsg {
return try await withCheckedThrowingContinuation { continuation in
try await withCheckedThrowingContinuation { continuation in
Task { [sender, secretary, logger] in
let msgID = await secretary.record(continuation: continuation)
var req = req
@@ -199,7 +199,7 @@ actor Handshaker {
}
}

let vStr = versions.map { $0.description }.joined(separator: ",")
let vStr = versions.map(\.description).joined(separator: ",")
let ours = String(format: "\(headerPreamble) \(role) \(vStr)\n")
do {
try writeFD.write(contentsOf: ours.data(using: .utf8)!)
2 changes: 1 addition & 1 deletion Coder Desktop/VPNLibTests/ProtoTests.swift
Original file line number Diff line number Diff line change
@@ -104,7 +104,7 @@ struct HandshakerTests {
let result: ProtoVersion

var description: String {
return "\(tun) vs \(mgr) -> \(result)"
"\(tun) vs \(mgr) -> \(result)"
}
}

296 changes: 296 additions & 0 deletions Coder Desktop/project.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
name: "Coder Desktop"
options:
bundleIdPrefix: com.coder
deploymentTarget:
macOS: "14.6"
xcodeVersion: "1600"
minimumXcodeGenVersion: "2.42.0"

settings:
base:
MARKETING_VERSION: "1.0" # Sets the version number.
CURRENT_PROJECT_VERSION: "1" # Sets the build number.

ALWAYS_SEARCH_USER_PATHS: NO
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS: YES
COPY_PHASE_STRIP: NO
DEAD_CODE_STRIPPING: YES
DEVELOPMENT_TEAM: "4399GN35BJ"
GENERATE_INFOPLIST_FILE: YES
PRODUCT_NAME: "$(TARGET_NAME)"
SWIFT_VERSION: ${SWIFT_VERSION}
ENABLE_USER_SCRIPT_SANDBOXING: YES
LD_RUNPATH_SEARCH_PATHS:
- "$(inherited)"
- "@executable_path/../Frameworks"
- "@loader_path/Frameworks"
GCC_C_LANGUAGE_STANDARD: gnu17
CLANG_CXX_LANGUAGE_STANDARD: "gnu++20"
CLANG_ENABLE_MODULES: YES
CLANG_ENABLE_OBJC_ARC: YES
CLANG_ENABLE_OBJC_WEAK: YES
ENABLE_STRICT_OBJC_MSGSEND: YES
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING: YES
CLANG_WARN_BOOL_CONVERSION: YES
CLANG_WARN_COMMA: YES
CLANG_WARN_CONSTANT_CONVERSION: YES
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS: YES
CLANG_WARN_DIRECT_OBJC_ISA_USAGE: YES_ERROR
CLANG_WARN_DOCUMENTATION_COMMENTS: YES
CLANG_WARN_EMPTY_BODY: YES
CLANG_WARN_ENUM_CONVERSION: YES
CLANG_WARN_INFINITE_RECURSION: YES
CLANG_WARN_INT_CONVERSION: YES
CLANG_WARN_NON_LITERAL_NULL_CONVERSION: YES
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF: YES
CLANG_WARN_OBJC_LITERAL_CONVERSION: YES
CLANG_WARN_OBJC_ROOT_CLASS: YES_ERROR
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER: YES
CLANG_WARN_RANGE_LOOP_ANALYSIS: YES
CLANG_WARN_STRICT_PROTOTYPES: YES
CLANG_WARN_SUSPICIOUS_MOVE: YES
CLANG_WARN_UNGUARDED_AVAILABILITY: YES_AGGRESSIVE
CLANG_WARN_UNREACHABLE_CODE: YES
CLANG_WARN__DUPLICATE_METHOD_MATCH: YES
GCC_WARN_64_TO_32_BIT_CONVERSION: YES
GCC_WARN_ABOUT_RETURN_TYPE: YES_ERROR
GCC_WARN_UNDECLARED_SELECTOR: YES
GCC_WARN_UNINITIALIZED_AUTOS: YES_AGGRESSIVE
GCC_WARN_UNUSED_FUNCTION: YES
GCC_WARN_UNUSED_VARIABLE: YES
GCC_NO_COMMON_BLOCKS: YES
CLANG_ANALYZER_NONNULL: YES
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION: YES_AGGRESSIVE
MTL_FAST_MATH: YES
LOCALIZATION_PREFERS_STRING_CATALOGS: YES
configs:
debug:
GCC_PREPROCESSOR_DEFINITIONS:
- "DEBUG=1"
- "$(inherited)"
ONLY_ACTIVE_ARCH: YES
SWIFT_ACTIVE_COMPILATION_CONDITIONS: "DEBUG $(inherited)"
SWIFT_OPTIMIZATION_LEVEL: "-Onone"
GCC_OPTIMIZATION_LEVEL: 0
DEBUG_INFORMATION_FORMAT: dwarf
ENABLE_TESTABILITY: YES
MTL_ENABLE_DEBUG_INFO: INCLUDE_SOURCE
release:
SWIFT_COMPILATION_MODE: wholemodule
DEBUG_INFORMATION_FORMAT: "dwarf-with-dsym"
ENABLE_NS_ASSERTIONS: NO
MTL_ENABLE_DEBUG_INFO: NO

packages:
ViewInspector:
url: https://github.com/nalexn/ViewInspector
from: 0.10.0
SwiftLintPlugins:
url: https://github.com/SimplyDanny/SwiftLintPlugins
from: 0.57.1
FluidMenuBarExtra:
url: https://github.com/lfroms/fluid-menu-bar-extra
from: 1.1.0
KeychainAccess:
url: https://github.com/kishikawakatsumi/KeychainAccess
branch: e0c7eebc5a4465a3c4680764f26b7a61f567cdaf
SwiftProtobuf:
url: https://github.com/apple/swift-protobuf.git
exactVersion: 1.28.2
Mocker:
url: https://github.com/WeTransfer/Mocker
from: 3.0.2
LaunchAtLogin:
url: https://github.com/sindresorhus/LaunchAtLogin-modern
from: 1.1.0

targets:
Coder Desktop:
type: application
platform: macOS
sources:
- path: Coder Desktop
entitlements:
path: Coder Desktop/Coder_Desktop.entitlements
properties:
com.apple.developer.networking.networkextension:
- packet-tunnel-provider
com.apple.developer.system-extension.install: true
com.apple.security.app-sandbox: true
com.apple.security.files.user-selected.read-only: true
com.apple.security.network.client: true
settings:
base:
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon # Sets the app icon to "AppIcon".
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
CODE_SIGN_IDENTITY: "Apple Development"
CODE_SIGN_STYLE: Automatic
COMBINE_HIDPI_IMAGES: YES
DEVELOPMENT_ASSET_PATHS: '"Coder Desktop/Preview Content"' # Adds development assets.
ENABLE_HARDENED_RUNTIME: YES
ENABLE_PREVIEWS: YES
INFOPLIST_KEY_LSUIElement: YES
INFOPLIST_KEY_NSHumanReadableCopyright: ""
SWIFT_EMIT_LOC_STRINGS: YES
PRODUCT_BUNDLE_IDENTIFIER: "com.coder.Coder-Desktop"

# (ThomasK33): Install the application into the /Applications folder
# so that macOS stops complaining about the app being run from an
# untrusted folder.
DEPLOYMENT_LOCATION: YES
DSTROOT: $(LOCAL_APPS_DIR)/Coder
INSTALL_PATH: /
SKIP_INSTALL: NO
dependencies:
- target: CoderSDK
embed: true
- target: VPN
embed: without-signing # Embed without signing.
- package: FluidMenuBarExtra
- package: KeychainAccess
- package: LaunchAtLogin
scheme:
testPlans:
- path: Coder Desktop.xctestplan
testTargets:
- Coder DesktopTests
- Coder DesktopUITests
buildToolPlugins:
- plugin: SwiftLintBuildToolPlugin
package: SwiftLintPlugins

Coder DesktopTests:
type: bundle.unit-test
platform: macOS
sources:
- path: Coder DesktopTests
settings:
base:
BUNDLE_LOADER: "$(TEST_HOST)"
TEST_HOST: "$(BUILT_PRODUCTS_DIR)/Coder Desktop.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Coder Desktop"
PRODUCT_BUNDLE_IDENTIFIER: "com.coder.Coder-DesktopTests"
dependencies:
- target: "Coder Desktop"
- target: CoderSDK
embed: false # Do not embed the framework.
- package: ViewInspector
- package: Mocker

Coder DesktopUITests:
type: bundle.ui-testing
platform: macOS
sources:
- path: Coder DesktopUITests
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: "com.coder.Coder-DesktopUITests"
dependencies:
- target: Coder Desktop

VPN:
type: system-extension
platform: macOS
sources:
- path: VPN
entitlements:
path: VPN/VPN.entitlements
properties:
com.apple.developer.networking.networkextension:
- packet-tunnel-provider
com.apple.security.app-sandbox: true
com.apple.security.application-groups:
- $(TeamIdentifierPrefix)com.coder.Coder-Desktop
com.apple.security.network.client: true
com.apple.security.network.server: true
settings:
base:
ENABLE_HARDENED_RUNTIME: YES
INFOPLIST_FILE: VPN/Info.plist
PRODUCT_BUNDLE_IDENTIFIER: "com.coder.Coder-Desktop.VPN"
PRODUCT_MODULE_NAME: "$(PRODUCT_NAME:c99extidentifier)"
PRODUCT_NAME: "$(PRODUCT_BUNDLE_IDENTIFIER)"
SWIFT_EMIT_LOC_STRINGS: YES
SWIFT_OBJC_BRIDGING_HEADER: "VPN/com_coder_Coder_Desktop_VPN-Bridging-Header.h"
dependencies:
- target: VPNLib
embed: true
- target: CoderSDK
embed: true
- sdk: NetworkExtension.framework

VPNLib:
type: framework
platform: macOS
sources:
- path: VPNLib
settings:
base:
PRODUCT_NAME: "$(TARGET_NAME:c99extidentifier)"
SWIFT_EMIT_LOC_STRINGS: YES
PRODUCT_BUNDLE_IDENTIFIER: "com.coder.Coder-Desktop.VPNLib"
DYLIB_COMPATIBILITY_VERSION: 1
DYLIB_CURRENT_VERSION: 1
DYLIB_INSTALL_NAME_BASE: "@rpath"
CODE_SIGN_IDENTITY: "Apple Development"
CODE_SIGN_STYLE: Automatic
LD_RUNPATH_SEARCH_PATHS:
- "@executable_path/../Frameworks"
- "@loader_path/Frameworks"
scheme:
testTargets:
- VPNLibTests
dependencies:
- package: SwiftProtobuf
- package: SwiftProtobuf
product: SwiftProtobufPluginLibrary
- target: CoderSDK
embed: false

VPNLibTests:
type: bundle.unit-test
platform: macOS
sources:
- path: VPNLibTests
settings:
base:
TEST_HOST: "$(BUILT_PRODUCTS_DIR)/Coder Desktop.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Coder Desktop"
PRODUCT_BUNDLE_IDENTIFIER: "com.coder.Coder-Desktop.VPNLibTests"
dependencies:
- target: Coder Desktop
- target: VPNLib
embed: false
- package: Mocker

CoderSDK:
type: framework
platform: macOS
sources:
- path: CoderSDK
settings:
base:
INFOPLIST_KEY_NSHumanReadableCopyright: ""
PRODUCT_NAME: "$(TARGET_NAME:c99extidentifier)"
SWIFT_EMIT_LOC_STRINGS: YES
GENERATE_INFOPLIST_FILE: YES
DYLIB_COMPATIBILITY_VERSION: 1
DYLIB_CURRENT_VERSION: 1
DYLIB_INSTALL_NAME_BASE: "@rpath"
scheme:
testTargets:
- CoderSDKTests
dependencies: []

CoderSDKTests:
type: bundle.unit-test
platform: macOS
sources:
- path: CoderSDKTests
dependencies:
- target: "Coder Desktop"
- target: CoderSDK
embed: false # Do not embed the framework.
- package: Mocker
settings:
base:
TEST_HOST: "$(BUILT_PRODUCTS_DIR)/Coder Desktop.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Coder Desktop"
PRODUCT_BUNDLE_IDENTIFIER: com.coder.Coder-Desktop.CoderSDKTests
50 changes: 40 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
@@ -6,31 +6,61 @@ LINTFLAGS :=
FMTFLAGS :=
endif

PROJECT := "Coder Desktop/Coder Desktop.xcodeproj"
SCHEME := "Coder Desktop"
PROJECT := Coder\ Desktop
XCPROJECT := Coder\ Desktop/Coder\ Desktop.xcodeproj
SCHEME := Coder\ Desktop
SWIFT_VERSION := 6.0

fmt:
.PHONY: setup
setup: \
$(XCPROJECT) \
$(PROJECT)/VPNLib/vpn.pb.swift

$(XCPROJECT): $(PROJECT)/project.yml
cd $(PROJECT); \
SWIFT_VERSION=$(SWIFT_VERSION) xcodegen

$(PROJECT)/VPNLib/vpn.pb.swift: $(PROJECT)/VPNLib/vpn.proto
protoc --swift_opt=Visibility=public --swift_out=. 'Coder Desktop/VPNLib/vpn.proto'

.PHONY: fmt
fmt: ## Run Swift file formatter
swiftformat \
--exclude '**.pb.swift' \
--swiftversion $(SWIFT_VERSION) \
$(FMTFLAGS) .

test:
.PHONY: test
test: $(XCPROJECT) ## Run all tests
set -o pipefail && xcodebuild test \
-project $(PROJECT) \
-project $(XCPROJECT) \
-scheme $(SCHEME) \
-testPlan $(SCHEME) \
-skipPackagePluginValidation \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO | xcbeautify

lint:
.PHONY: lint
lint: ## Lint swift files
swiftlint \
--strict \
--quiet $(LINTFLAGS)

clean:
.PHONY: clean
clean: ## Clean Xcode project
xcodebuild clean \
-project $(PROJECT)
-project $(XCPROJECT)
rm -rf $(XCPROJECT)

proto:
protoc --swift_opt=Visibility=public --swift_out=. 'Coder Desktop/VPNLib/vpn.proto'
.PHONY: proto
proto: $(PROJECT)/VPNLib/vpn.pb.swift ## Generate Swift files from protobufs

.PHONY: help
help: ## Show this help
@echo "Specify a command. The choices are:"
@grep -hE '^[0-9a-zA-Z_-]+:.*?## .*$$' ${MAKEFILE_LIST} | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-20s\033[m %s\n", $$1, $$2}'
@echo ""

.PHONY: watch-gen
watch-gen: ## Generate Xcode project file and watch for changes
watchexec -w 'Coder Desktop/project.yml' make $(XCPROJECT)
61 changes: 61 additions & 0 deletions flake.lock
49 changes: 49 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
description = "Coder Desktop macOS";

inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};

outputs =
{
self,
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachSystem
(with flake-utils.lib.system; [
aarch64-darwin
x86_64-darwin
])
(
system:
let
pkgs = import nixpkgs {
inherit system;
};

formatter = pkgs.nixfmt-rfc-style;
in
{
inherit formatter;

devShells.default = pkgs.mkShellNoCC {
buildInputs = with pkgs; [
apple-sdk_15
clang
formatter
gnumake
protobuf_28
protoc-gen-swift
swiftformat
swiftlint
watchexec
xcodegen
xcbeautify
];
};
}
);
}