diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..16f93aa --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,49 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + branches: + - "*" + +jobs: + test-xcode: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v6 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + - name: Build + run: swift build -v + - name: Build release + run: swift build -c release -v + + test-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Build + run: swift build -v + - name: Build release + run: swift build -c release -v + + test-template: + runs-on: macos-latest + steps: + - uses: actions/checkout@v6 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + - name: Build saga + run: swift build -c release + - name: Scaffold a site and build it + run: | + mkdir -p "$RUNNER_TEMP/smoke" + cd "$RUNNER_TEMP/smoke" + "$GITHUB_WORKSPACE/.build/release/saga" init testsite + cd testsite + swift build diff --git a/.swiftformat b/.swiftformat index bc573ed..d12dc83 100644 --- a/.swiftformat +++ b/.swiftformat @@ -2,4 +2,5 @@ --indentcase true --patternlet inline --disable unusedArguments ---disable redundantReturn \ No newline at end of file +--disable redundantReturn +--exclude .build,IntegrationTests/Fixture/Sources/Fixture/Generated.swift \ No newline at end of file diff --git a/IntegrationTests/Fixture/Package.swift b/IntegrationTests/Fixture/Package.swift index 60d600c..a3184fb 100644 --- a/IntegrationTests/Fixture/Package.swift +++ b/IntegrationTests/Fixture/Package.swift @@ -1,8 +1,8 @@ // swift-tools-version:6.0 import PackageDescription -// Stands in for a Saga site so the shutdown tests don't need the real Saga -// dependency graph. Built and driven by ../run-shutdown-tests.sh. +/// Stands in for a Saga site so the shutdown tests don't need the real Saga +/// dependency graph. Built and driven by ../run-shutdown-tests.sh. let package = Package( name: "fixture", products: [.executable(name: "Fixture", targets: ["Fixture"])], diff --git a/Sources/SagaCLI/DevCommand.swift b/Sources/SagaCLI/DevCommand.swift index ed8dde2..8b94697 100644 --- a/Sources/SagaCLI/DevCommand.swift +++ b/Sources/SagaCLI/DevCommand.swift @@ -1,7 +1,6 @@ import ArgumentParser import Foundation import SagaPathKit -import os struct Dev: ParsableCommand { static let configuration = CommandConfiguration( @@ -40,7 +39,7 @@ private final class DevCoordinator: @unchecked Sendable { var shuttingDown = false } - private let lifecycle = OSAllocatedUnfairLock(initialState: Lifecycle()) + private let lifecycle = Locked(initialState: Lifecycle()) /// Serializes recompiles. Shutdown never uses this, so Ctrl-C doesn't wait on /// an in-flight build. @@ -60,7 +59,7 @@ private final class DevCoordinator: @unchecked Sendable { sigintSrc.setEventHandler { [weak self] in print("\nShutting down...") guard let self else { Foundation.exit(0) } - self.shutdown() + shutdown() } sigintSrc.resume() @@ -97,7 +96,7 @@ private final class DevCoordinator: @unchecked Sendable { return (process, false) } if alreadyShuttingDown { - dispatchMain() // shutdown() is mid-flight and exits the process + dispatchMain() // shutdown() is mid-flight and exits the process } guard let siteProcess else { log("Failed to launch site process.") diff --git a/Sources/SagaCLI/DevServer.swift b/Sources/SagaCLI/DevServer.swift index 797e1df..f9a48ed 100644 --- a/Sources/SagaCLI/DevServer.swift +++ b/Sources/SagaCLI/DevServer.swift @@ -17,8 +17,8 @@ final class DevServer: @unchecked Sendable { } func start() throws { - let outputPath = self.outputPath - let sseConnections = self.sseConnections + let outputPath = outputPath + let sseConnections = sseConnections let baseDir = FileManager.default.currentDirectoryPath let bootstrap = ServerBootstrap(group: group) @@ -111,7 +111,7 @@ private final class HTTPHandler: ChannelInboundHandler, @unchecked Sendable { // Static file serving let filePath = resolveFilePath(uri: uri) - guard let filePath = filePath, + guard let filePath, FileManager.default.fileExists(atPath: filePath), let data = FileManager.default.contents(atPath: filePath) else { @@ -122,11 +122,10 @@ private final class HTTPHandler: ChannelInboundHandler, @unchecked Sendable { let contentType = mimeType(for: filePath) let isHTML = contentType == "text/html" - var responseData: Data - if isHTML, let html = String(data: data, encoding: .utf8) { - responseData = Data(injectReloadScript(into: html).utf8) + var responseData: Data = if isHTML, let html = String(data: data, encoding: .utf8) { + Data(injectReloadScript(into: html).utf8) } else { - responseData = data + data } var headers = HTTPHeaders() @@ -166,9 +165,8 @@ private final class HTTPHandler: ChannelInboundHandler, @unchecked Sendable { // Direct file match let directPath = outputPath + path - if fileManager.fileExists(atPath: directPath) { - var isDir: ObjCBool = false - fileManager.fileExists(atPath: directPath, isDirectory: &isDir) + var isDir: ObjCBool = false + if fileManager.fileExists(atPath: directPath, isDirectory: &isDir) { if !isDir.boolValue { return directPath } @@ -220,11 +218,10 @@ private final class HTTPHandler: ChannelInboundHandler, @unchecked Sendable { } private func mimeType(for path: String) -> String { - let ext: String - if let dotIndex = path.lastIndex(of: ".") { - ext = String(path[path.index(after: dotIndex)...]).lowercased() + let ext = if let dotIndex = path.lastIndex(of: ".") { + String(path[path.index(after: dotIndex)...]).lowercased() } else { - ext = "" + "" } switch ext { case "html", "htm": return "text/html" diff --git a/Sources/SagaCLI/Utils.swift b/Sources/SagaCLI/Utils.swift index 88379e3..0ff1243 100644 --- a/Sources/SagaCLI/Utils.swift +++ b/Sources/SagaCLI/Utils.swift @@ -145,3 +145,20 @@ func openBrowser(url: String) { try? process.run() #endif } + +/// State that can only be reached while its lock is held. Used instead of +/// `OSAllocatedUnfairLock`, which doesn't exist on Linux. +final class Locked: @unchecked Sendable { + private var state: State + private let lock = NSLock() + + init(initialState: State) { + state = initialState + } + + func withLock(_ body: (inout State) throws -> R) rethrows -> R { + lock.lock() + defer { lock.unlock() } + return try body(&state) + } +} diff --git a/justfile b/justfile index e376b56..b6f2325 100644 --- a/justfile +++ b/justfile @@ -1,8 +1,8 @@ build: swift build -build-swift510: - docker run --rm -v "$PWD":/src -w /src --tmpfs /src/.build:exec swift:5.10 swift build +build-linux: + docker run --rm -v "$PWD":/src -w /src --tmpfs /src/.build:exec swift:6.2 swift build format: - swiftformat -swift-version 5 . + swiftformat -swift-version 6 .