Skip to content

Commit 02cab48

Browse files
Add support for Windows (#753)
1 parent aa37de7 commit 02cab48

File tree

10 files changed

+129
-23
lines changed

10 files changed

+129
-23
lines changed

.github/workflows/main.yml

+6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ jobs:
1616
linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error"
1717
linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error"
1818
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error"
19+
windows_6_0_enabled: true
20+
windows_nightly_6_1_enabled: true
21+
windows_nightly_main_enabled: true
22+
windows_6_0_arguments_override: "--explicit-target-dependency-import-check error"
23+
windows_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error"
24+
windows_nightly_main_arguments_override: "--explicit-target-dependency-import-check error"
1925

2026
integration-test:
2127
name: Integration test

.github/workflows/pull_request.yml

+6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ jobs:
2222
linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error"
2323
linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error"
2424
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error"
25+
windows_6_0_enabled: true
26+
windows_nightly_6_1_enabled: true
27+
windows_nightly_main_enabled: true
28+
windows_6_0_arguments_override: "--explicit-target-dependency-import-check error"
29+
windows_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error"
30+
windows_nightly_main_arguments_override: "--explicit-target-dependency-import-check error"
2531

2632
integration-test:
2733
name: Integration test

Package.swift

+5-2
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ let package = Package(
5454
// Tests-only: Runtime library linked by generated code, and also
5555
// helps keep the runtime library new enough to work with the generated
5656
// code.
57-
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.3.2"),
57+
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.8.2"),
5858
.package(url: "https://github.com/apple/swift-http-types", from: "1.0.2"),
5959
],
6060
targets: [
@@ -111,7 +111,10 @@ let package = Package(
111111
.testTarget(
112112
name: "OpenAPIGeneratorTests",
113113
dependencies: [
114-
"swift-openapi-generator", .product(name: "ArgumentParser", package: "swift-argument-parser"),
114+
"_OpenAPIGeneratorCore",
115+
// Everything except windows: https://github.com/swiftlang/swift-package-manager/issues/6367
116+
.target(name: "swift-openapi-generator", condition: .when(platforms: [.android, .linux, .macOS, .openbsd, .wasi, .custom("freebsd")])),
117+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
115118
],
116119
resources: [.copy("Resources")],
117120
swiftSettings: swiftSettings

Plugins/PluginsShared/PluginUtils.swift

+24-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ enum PluginUtils {
6464

6565
/// Find the config file.
6666
private static func findConfig(inputFiles: FileList, targetName: String) -> Result<Path, FileError> {
67-
let matchedConfigs = inputFiles.filter { supportedConfigFiles.contains($0.path.lastComponent) }.map(\.path)
67+
let matchedConfigs = inputFiles.filter { supportedConfigFiles.contains($0.path.lastComponent_fixed) }
68+
.map(\.path)
6869
guard matchedConfigs.count > 0 else {
6970
return .failure(FileError(targetName: targetName, fileKind: .config, issue: .noFilesFound))
7071
}
@@ -78,7 +79,7 @@ enum PluginUtils {
7879

7980
/// Find the document file.
8081
private static func findDocument(inputFiles: FileList, targetName: String) -> Result<Path, FileError> {
81-
let matchedDocs = inputFiles.filter { supportedDocFiles.contains($0.path.lastComponent) }.map(\.path)
82+
let matchedDocs = inputFiles.filter { supportedDocFiles.contains($0.path.lastComponent_fixed) }.map(\.path)
8283
guard matchedDocs.count > 0 else {
8384
return .failure(FileError(targetName: targetName, fileKind: .document, issue: .noFilesFound))
8485
}
@@ -97,3 +98,24 @@ extension Array where Element == String {
9798
return "\(self.dropLast().joined(separator: separator))\(lastSeparator)\(self.last!)"
9899
}
99100
}
101+
102+
extension PackagePlugin.Path {
103+
/// Workaround for the ``lastComponent`` property being broken on Windows
104+
/// due to hardcoded assumptions about the path separator being forward slash.
105+
@available(_PackageDescription, deprecated: 6.0, message: "Use `URL` type instead of `Path`.") public
106+
var lastComponent_fixed: String
107+
{
108+
#if !os(Windows)
109+
lastComponent
110+
#else
111+
// Find the last path separator.
112+
guard let idx = string.lastIndex(where: { $0 == "/" || $0 == "\\" }) else {
113+
// No path separators, so the basename is the whole string.
114+
return self.string
115+
}
116+
// Otherwise, it's the string from (but not including) the last path
117+
// separator.
118+
return String(self.string.suffix(from: self.string.index(after: idx)))
119+
#endif
120+
}
121+
}

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,12 @@ See also [Supported OpenAPI features][supported-openapi-features].
9999

100100
### Supported platforms and minimum versions
101101

102-
The generator is used during development and is supported on macOS and Linux.
102+
The generator is used during development and is supported on macOS, Linux, and Windows.
103103

104104
The generated code, runtime library, and transports are supported on more
105105
platforms, listed below.
106106

107-
| Component | macOS | Linux | iOS | tvOS | watchOS | visionOS |
107+
| Component | macOS | Linux, Windows | iOS | tvOS | watchOS | visionOS |
108108
| ----------------------------------: | :--- | :--- | :- | :-- | :----- | :------ |
109109
| Generator plugin and CLI | ✅ 10.15+ || ✖️ | ✖️ | ✖️ | ✖️ |
110110
| Generated code and runtime library | ✅ 10.15+ || ✅ 13+ | ✅ 13+ | ✅ 6+ | ✅ 1+ |

Tests/OpenAPIGeneratorReferenceTests/CompatabilityTest.swift

+6-11
Original file line numberDiff line numberDiff line change
@@ -314,10 +314,9 @@ fileprivate extension CompatibilityTest {
314314

315315
// Build the package.
316316
let process = Process()
317-
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
317+
process.executableURL = try resolveExecutable("swift")
318318
process.arguments = [
319-
"swift", "build", "--package-path", packageDir.path, "-Xswiftc", "-Xllvm", "-Xswiftc",
320-
"-vectorize-slp=false",
319+
"build", "--package-path", packageDir.path, "-Xswiftc", "-Xllvm", "-Xswiftc", "-vectorize-slp=false",
321320
]
322321
if let numBuildJobs = compatibilityTestNumBuildJobs {
323322
process.arguments!.append(contentsOf: ["-j", String(numBuildJobs)])
@@ -358,14 +357,12 @@ fileprivate extension CompatibilityTest {
358357
func log(_ message: String) { print("\(name) \(message)") }
359358

360359
var testCaseName: String {
361-
/// The `name` property is `<test-suite-name>.<test-case-name>` on Linux,
362-
/// and `-[<test-suite-name> <test-case-name>]` on macOS.
360+
/// The `name` property is `-[<test-suite-name> <test-case-name>]` on Apple platforms (e.g. with an Objective-C runtime),
361+
/// and `<test-suite-name>.<test-case-name>` elsewhere.
363362
#if canImport(Darwin)
364363
return String(name.split(separator: " ", maxSplits: 2).last!.dropLast())
365-
#elseif os(Linux)
366-
return String(name.split(separator: ".", maxSplits: 2).last!)
367364
#else
368-
#error("Platform not supported")
365+
return String(name.split(separator: ".", maxSplits: 2).last!)
369366
#endif
370367
}
371368
}
@@ -417,7 +414,7 @@ fileprivate extension URLSession {
417414
func data(from url: URL) async throws -> (Data, URLResponse) {
418415
#if canImport(Darwin)
419416
return try await data(from: url, delegate: nil)
420-
#elseif os(Linux)
417+
#else
421418
return try await withCheckedThrowingContinuation { continuation in
422419
dataTask(with: URLRequest(url: url)) { data, response, error in
423420
if let error {
@@ -432,8 +429,6 @@ fileprivate extension URLSession {
432429
}
433430
.resume()
434431
}
435-
#else
436-
#error("Platform not supported")
437432
#endif
438433
}
439434
}

Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift

+20-2
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,24 @@ extension FileBasedReferenceTests {
190190
file: StaticString = #filePath,
191191
line: UInt = #line
192192
) {
193+
// Normalize newlines
194+
#if os(Windows)
195+
let hasCarriageReturns = String(
196+
decoding: FileManager.default.contents(atPath: referenceFile.path) ?? Data(),
197+
as: UTF8.self
198+
)
199+
.contains("\r\n")
200+
XCTAssertNoThrow(
201+
try Data(
202+
(String(decoding: FileManager.default.contents(atPath: generatedFile.path) ?? Data(), as: UTF8.self)
203+
.split(omittingEmptySubsequences: false, whereSeparator: { $0 == "\r" || $0 == "\n" })
204+
.joined(separator: hasCarriageReturns ? "\r\n" : "\n"))
205+
.utf8
206+
)
207+
.write(to: URL(fileURLWithPath: generatedFile.path))
208+
)
209+
#endif
210+
193211
if FileManager.default.contentsEqual(atPath: generatedFile.path, andPath: referenceFile.path) { return }
194212

195213
let diffOutput: String?
@@ -219,10 +237,10 @@ extension FileBasedReferenceTests {
219237

220238
private func runDiff(reference: URL, actual: URL) throws -> String {
221239
let process = Process()
222-
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
240+
process.executableURL = try resolveExecutable("git")
223241
process.currentDirectoryURL = self.referenceTestResourcesDirectory
224242
process.arguments = [
225-
"git", "diff", "--no-index", "-U5",
243+
"diff", "--no-index", "-U5",
226244
// The following arguments are useful for development.
227245
// "--ignore-space-change",
228246
// "--ignore-all-space",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
17+
func resolveExecutable(_ name: String) throws -> URL {
18+
struct Static {
19+
#if os(Windows)
20+
static let separator = ";"
21+
static let suffix = ".exe"
22+
#else
23+
static let separator = ":"
24+
static let suffix = ""
25+
#endif
26+
}
27+
28+
enum PathResolutionError: Error, CustomStringConvertible {
29+
case notFound(name: String, path: String)
30+
var description: String {
31+
switch self {
32+
case let .notFound(name, path): "Could not find \(name)\(Static.suffix) in PATH: \(path)"
33+
}
34+
}
35+
}
36+
let env = Dictionary(
37+
uniqueKeysWithValues: ProcessInfo.processInfo.environment.map { (k, v) in
38+
#if os(Windows)
39+
return (k.uppercased(), v)
40+
#else
41+
return (k, v)
42+
#endif
43+
}
44+
)
45+
let paths = (env["PATH"] ?? "").split(separator: Static.separator).map(String.init)
46+
for path in paths {
47+
let fullPath = path + "/" + name + Static.suffix
48+
if FileManager.default.fileExists(atPath: fullPath) { return URL(fileURLWithPath: fullPath) }
49+
}
50+
throw PathResolutionError.notFound(name: name, path: env["PATH"] ?? "")
51+
}

Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift

+2-4
Original file line numberDiff line numberDiff line change
@@ -6247,10 +6247,8 @@ private func XCTAssertSwiftEquivalent(
62476247

62486248
private func diff(expected: String, actual: String) throws -> String {
62496249
let process = Process()
6250-
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
6251-
process.arguments = [
6252-
"bash", "-c", "diff -U5 --label=expected <(echo '\(expected)') --label=actual <(echo '\(actual)')",
6253-
]
6250+
process.executableURL = try resolveExecutable("bash")
6251+
process.arguments = ["-c", "diff -U5 --label=expected <(echo '\(expected)') --label=actual <(echo '\(actual)')"]
62546252
let pipe = Pipe()
62556253
process.standardOutput = pipe
62566254
try process.run()

Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift

+7
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import XCTest
1515
import _OpenAPIGeneratorCore
1616
import OpenAPIKit
1717
import ArgumentParser
18+
19+
// https://github.com/swiftlang/swift-package-manager/issues/6367
20+
#if !os(Windows)
1821
@testable import swift_openapi_generator
22+
#endif
1923

2024
final class Test_GenerateOptions: XCTestCase {
2125

@@ -29,6 +33,8 @@ final class Test_GenerateOptions: XCTestCase {
2933
)
3034
}
3135

36+
// https://github.com/swiftlang/swift-package-manager/issues/6367
37+
#if !os(Windows)
3238
func testRunGeneratorThrowsErrorDiagnostic() async throws {
3339
let outputDirectory = URL(fileURLWithPath: "/invalid/path")
3440
let docsDirectory = resourcesDirectory.appendingPathComponent("Docs")
@@ -45,4 +51,5 @@ final class Test_GenerateOptions: XCTestCase {
4551
XCTAssertEqual(diagnostic.severity, .error, "Expected diagnostic severity to be `.error`")
4652
} catch { XCTFail("Expected to throw a Diagnostic `.error`, but threw a different error: \(error)") }
4753
}
54+
#endif
4855
}

0 commit comments

Comments
 (0)