From a294523aad8e2b41e497cce3b4cfc145596e3b1d Mon Sep 17 00:00:00 2001 From: Jake Petroules Date: Mon, 31 Mar 2025 14:40:40 -0700 Subject: [PATCH] Add support for Windows Disable tests relying on the executable since those aren't yet supported by SwiftPM, and tweak a couple platform-conditional pieces of code to support the Windows path. --- .github/workflows/main.yml | 6 +++ .github/workflows/pull_request.yml | 6 +++ Package.swift | 7 ++- Plugins/PluginsShared/PluginUtils.swift | 26 +++++++++- README.md | 4 +- .../CompatabilityTest.swift | 17 +++---- .../FileBasedReferenceTests.swift | 22 +++++++- .../Helpers.swift | 51 +++++++++++++++++++ .../SnippetBasedReferenceTests.swift | 6 +-- .../Test_GenerateOptions.swift | 7 +++ 10 files changed, 129 insertions(+), 23 deletions(-) create mode 100644 Tests/OpenAPIGeneratorReferenceTests/Helpers.swift diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0cd8333ca..3bfd2aef3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,6 +16,12 @@ jobs: linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error" linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" + windows_6_0_enabled: true + windows_nightly_6_1_enabled: true + windows_nightly_main_enabled: true + windows_6_0_arguments_override: "--explicit-target-dependency-import-check error" + windows_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error" + windows_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" integration-test: name: Integration test diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index d93f1bc7f..28853beac 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -22,6 +22,12 @@ jobs: linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error" linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" + windows_6_0_enabled: true + windows_nightly_6_1_enabled: true + windows_nightly_main_enabled: true + windows_6_0_arguments_override: "--explicit-target-dependency-import-check error" + windows_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error" + windows_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" integration-test: name: Integration test diff --git a/Package.swift b/Package.swift index ffee94358..bcbc3d6bd 100644 --- a/Package.swift +++ b/Package.swift @@ -54,7 +54,7 @@ let package = Package( // Tests-only: Runtime library linked by generated code, and also // helps keep the runtime library new enough to work with the generated // code. - .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.3.2"), + .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.8.2"), .package(url: "https://github.com/apple/swift-http-types", from: "1.0.2"), ], targets: [ @@ -111,7 +111,10 @@ let package = Package( .testTarget( name: "OpenAPIGeneratorTests", dependencies: [ - "swift-openapi-generator", .product(name: "ArgumentParser", package: "swift-argument-parser"), + "_OpenAPIGeneratorCore", + // Everything except windows: https://github.com/swiftlang/swift-package-manager/issues/6367 + .target(name: "swift-openapi-generator", condition: .when(platforms: [.android, .linux, .macOS, .openbsd, .wasi, .custom("freebsd")])), + .product(name: "ArgumentParser", package: "swift-argument-parser"), ], resources: [.copy("Resources")], swiftSettings: swiftSettings diff --git a/Plugins/PluginsShared/PluginUtils.swift b/Plugins/PluginsShared/PluginUtils.swift index d03cb875b..11bf2cc05 100644 --- a/Plugins/PluginsShared/PluginUtils.swift +++ b/Plugins/PluginsShared/PluginUtils.swift @@ -64,7 +64,8 @@ enum PluginUtils { /// Find the config file. private static func findConfig(inputFiles: FileList, targetName: String) -> Result { - let matchedConfigs = inputFiles.filter { supportedConfigFiles.contains($0.path.lastComponent) }.map(\.path) + let matchedConfigs = inputFiles.filter { supportedConfigFiles.contains($0.path.lastComponent_fixed) } + .map(\.path) guard matchedConfigs.count > 0 else { return .failure(FileError(targetName: targetName, fileKind: .config, issue: .noFilesFound)) } @@ -78,7 +79,7 @@ enum PluginUtils { /// Find the document file. private static func findDocument(inputFiles: FileList, targetName: String) -> Result { - let matchedDocs = inputFiles.filter { supportedDocFiles.contains($0.path.lastComponent) }.map(\.path) + let matchedDocs = inputFiles.filter { supportedDocFiles.contains($0.path.lastComponent_fixed) }.map(\.path) guard matchedDocs.count > 0 else { return .failure(FileError(targetName: targetName, fileKind: .document, issue: .noFilesFound)) } @@ -97,3 +98,24 @@ extension Array where Element == String { return "\(self.dropLast().joined(separator: separator))\(lastSeparator)\(self.last!)" } } + +extension PackagePlugin.Path { + /// Workaround for the ``lastComponent`` property being broken on Windows + /// due to hardcoded assumptions about the path separator being forward slash. + @available(_PackageDescription, deprecated: 6.0, message: "Use `URL` type instead of `Path`.") public + var lastComponent_fixed: String + { + #if !os(Windows) + lastComponent + #else + // Find the last path separator. + guard let idx = string.lastIndex(where: { $0 == "/" || $0 == "\\" }) else { + // No path separators, so the basename is the whole string. + return self.string + } + // Otherwise, it's the string from (but not including) the last path + // separator. + return String(self.string.suffix(from: self.string.index(after: idx))) + #endif + } +} diff --git a/README.md b/README.md index cf138a8f6..154187db4 100644 --- a/README.md +++ b/README.md @@ -99,12 +99,12 @@ See also [Supported OpenAPI features][supported-openapi-features]. ### Supported platforms and minimum versions -The generator is used during development and is supported on macOS and Linux. +The generator is used during development and is supported on macOS, Linux, and Windows. The generated code, runtime library, and transports are supported on more platforms, listed below. -| Component | macOS | Linux | iOS | tvOS | watchOS | visionOS | +| Component | macOS | Linux, Windows | iOS | tvOS | watchOS | visionOS | | ----------------------------------: | :--- | :--- | :- | :-- | :----- | :------ | | Generator plugin and CLI | ✅ 10.15+ | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | | Generated code and runtime library | ✅ 10.15+ | ✅ | ✅ 13+ | ✅ 13+ | ✅ 6+ | ✅ 1+ | diff --git a/Tests/OpenAPIGeneratorReferenceTests/CompatabilityTest.swift b/Tests/OpenAPIGeneratorReferenceTests/CompatabilityTest.swift index 5b96751b1..b99a4c89d 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/CompatabilityTest.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/CompatabilityTest.swift @@ -314,10 +314,9 @@ fileprivate extension CompatibilityTest { // Build the package. let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/env") + process.executableURL = try resolveExecutable("swift") process.arguments = [ - "swift", "build", "--package-path", packageDir.path, "-Xswiftc", "-Xllvm", "-Xswiftc", - "-vectorize-slp=false", + "build", "--package-path", packageDir.path, "-Xswiftc", "-Xllvm", "-Xswiftc", "-vectorize-slp=false", ] if let numBuildJobs = compatibilityTestNumBuildJobs { process.arguments!.append(contentsOf: ["-j", String(numBuildJobs)]) @@ -358,14 +357,12 @@ fileprivate extension CompatibilityTest { func log(_ message: String) { print("\(name) \(message)") } var testCaseName: String { - /// The `name` property is `.` on Linux, - /// and `-[ ]` on macOS. + /// The `name` property is `-[ ]` on Apple platforms (e.g. with an Objective-C runtime), + /// and `.` elsewhere. #if canImport(Darwin) return String(name.split(separator: " ", maxSplits: 2).last!.dropLast()) - #elseif os(Linux) - return String(name.split(separator: ".", maxSplits: 2).last!) #else - #error("Platform not supported") + return String(name.split(separator: ".", maxSplits: 2).last!) #endif } } @@ -417,7 +414,7 @@ fileprivate extension URLSession { func data(from url: URL) async throws -> (Data, URLResponse) { #if canImport(Darwin) return try await data(from: url, delegate: nil) - #elseif os(Linux) + #else return try await withCheckedThrowingContinuation { continuation in dataTask(with: URLRequest(url: url)) { data, response, error in if let error { @@ -432,8 +429,6 @@ fileprivate extension URLSession { } .resume() } - #else - #error("Platform not supported") #endif } } diff --git a/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift index f45902c3d..5596602bd 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift @@ -190,6 +190,24 @@ extension FileBasedReferenceTests { file: StaticString = #filePath, line: UInt = #line ) { + // Normalize newlines + #if os(Windows) + let hasCarriageReturns = String( + decoding: FileManager.default.contents(atPath: referenceFile.path) ?? Data(), + as: UTF8.self + ) + .contains("\r\n") + XCTAssertNoThrow( + try Data( + (String(decoding: FileManager.default.contents(atPath: generatedFile.path) ?? Data(), as: UTF8.self) + .split(omittingEmptySubsequences: false, whereSeparator: { $0 == "\r" || $0 == "\n" }) + .joined(separator: hasCarriageReturns ? "\r\n" : "\n")) + .utf8 + ) + .write(to: URL(fileURLWithPath: generatedFile.path)) + ) + #endif + if FileManager.default.contentsEqual(atPath: generatedFile.path, andPath: referenceFile.path) { return } let diffOutput: String? @@ -219,10 +237,10 @@ extension FileBasedReferenceTests { private func runDiff(reference: URL, actual: URL) throws -> String { let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/env") + process.executableURL = try resolveExecutable("git") process.currentDirectoryURL = self.referenceTestResourcesDirectory process.arguments = [ - "git", "diff", "--no-index", "-U5", + "diff", "--no-index", "-U5", // The following arguments are useful for development. // "--ignore-space-change", // "--ignore-all-space", diff --git a/Tests/OpenAPIGeneratorReferenceTests/Helpers.swift b/Tests/OpenAPIGeneratorReferenceTests/Helpers.swift new file mode 100644 index 000000000..d4804dced --- /dev/null +++ b/Tests/OpenAPIGeneratorReferenceTests/Helpers.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +func resolveExecutable(_ name: String) throws -> URL { + struct Static { + #if os(Windows) + static let separator = ";" + static let suffix = ".exe" + #else + static let separator = ":" + static let suffix = "" + #endif + } + + enum PathResolutionError: Error, CustomStringConvertible { + case notFound(name: String, path: String) + var description: String { + switch self { + case let .notFound(name, path): "Could not find \(name)\(Static.suffix) in PATH: \(path)" + } + } + } + let env = Dictionary( + uniqueKeysWithValues: ProcessInfo.processInfo.environment.map { (k, v) in + #if os(Windows) + return (k.uppercased(), v) + #else + return (k, v) + #endif + } + ) + let paths = (env["PATH"] ?? "").split(separator: Static.separator).map(String.init) + for path in paths { + let fullPath = path + "/" + name + Static.suffix + if FileManager.default.fileExists(atPath: fullPath) { return URL(fileURLWithPath: fullPath) } + } + throw PathResolutionError.notFound(name: name, path: env["PATH"] ?? "") +} diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 0ddd7b5e6..81cdeb108 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -6247,10 +6247,8 @@ private func XCTAssertSwiftEquivalent( private func diff(expected: String, actual: String) throws -> String { let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/env") - process.arguments = [ - "bash", "-c", "diff -U5 --label=expected <(echo '\(expected)') --label=actual <(echo '\(actual)')", - ] + process.executableURL = try resolveExecutable("bash") + process.arguments = ["-c", "diff -U5 --label=expected <(echo '\(expected)') --label=actual <(echo '\(actual)')"] let pipe = Pipe() process.standardOutput = pipe try process.run() diff --git a/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift b/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift index 6d73da7e9..a543961be 100644 --- a/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift +++ b/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift @@ -15,7 +15,11 @@ import XCTest import _OpenAPIGeneratorCore import OpenAPIKit import ArgumentParser + +// https://github.com/swiftlang/swift-package-manager/issues/6367 +#if !os(Windows) @testable import swift_openapi_generator +#endif final class Test_GenerateOptions: XCTestCase { @@ -29,6 +33,8 @@ final class Test_GenerateOptions: XCTestCase { ) } + // https://github.com/swiftlang/swift-package-manager/issues/6367 + #if !os(Windows) func testRunGeneratorThrowsErrorDiagnostic() async throws { let outputDirectory = URL(fileURLWithPath: "/invalid/path") let docsDirectory = resourcesDirectory.appendingPathComponent("Docs") @@ -45,4 +51,5 @@ final class Test_GenerateOptions: XCTestCase { XCTAssertEqual(diagnostic.severity, .error, "Expected diagnostic severity to be `.error`") } catch { XCTFail("Expected to throw a Diagnostic `.error`, but threw a different error: \(error)") } } + #endif }