From cd35feafd68d1a80f4737f355d288cf18f6aab5a Mon Sep 17 00:00:00 2001 From: Tim Morgan Date: Mon, 6 Jul 2026 19:43:41 -0700 Subject: [PATCH 1/3] Add Linux support for SwiftMETAR core library and DecodeWindsAloft URLSession and XMLParser move to FoundationNetworking/FoundationXML on Linux, and String(localized:)/LocalizedStringResource aren't implemented by swift-corelibs-foundation, so a small per-module shim resolves each key to itself there. METARFormatting (and the DecodeMETAR/DecodeTAF CLIs that depend on it) format Measurement values using Measurement.FormatStyle and the \(_, format:) string interpolation sugar, neither of which swift-corelibs-foundation implements, so those targets are only defined on Apple platforms; SwiftMETAR core and DecodeWindsAloft don't use those APIs and now build and test on Linux. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 9 +- Package.swift | 101 ++++++---- Sources/DecodeMETAR/DecodeMETAR.swift | 3 + Sources/DecodeTAF/DecodeTAF.swift | 3 + .../DecodeWindsAloft/DecodeWindsAloft.swift | 3 + Sources/SwiftMETAR/Error.swift | 180 +++++------------- Sources/SwiftMETAR/LinuxLocalization.swift | 21 ++ .../METAR/Parsers/METARXMLParser.swift | 3 + .../SwiftMETAR/TAF/Parsers/TAFXMLParser.swift | 3 + 9 files changed, 153 insertions(+), 173 deletions(-) create mode 100644 Sources/SwiftMETAR/LinuxLocalization.swift diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6f67a3..376723d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,10 @@ # Category A (tools-version 6.2 + macOS 26 min): macos-26 + Swift 6.2 # Category B (tools-version 6.2 + older macOS): macos-15 + 6.2, macos-26 + 6.2 # Category C (tools-version 6.0): macos-15 + 6.1, macos-15 + 6.3, macos-26 + 6.3 -# Linux (if viable): ubuntu + Swift 6.1, ubuntu + Swift 6.3 +# Linux: ubuntu + Swift 6.1, ubuntu + Swift 6.3 +# (Package.swift excludes METARFormatting/DecodeMETAR/DecodeTAF on Linux — they format +# Measurement values via APIs swift-corelibs-foundation doesn't implement; SwiftMETAR +# core + DecodeWindsAloft + tests are unaffected and build/test on Linux normally) # # When Swift 6.4 ships: bump 6.1→6.2 and 6.3→6.4 in Category C # When bumping tools-version to 6.2: drop 6.0/6.1, move to Category A or B @@ -33,6 +36,10 @@ jobs: swift: "6.3" - os: macos-26 swift: "6.3" + - os: ubuntu-latest + swift: "6.1" + - os: ubuntu-latest + swift: "6.3" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 diff --git a/Package.swift b/Package.swift index 95741ab..318c993 100644 --- a/Package.swift +++ b/Package.swift @@ -7,40 +7,55 @@ let approachableConcurrency: [SwiftSetting] = [ .enableUpcomingFeature("InferIsolatedConformances") ] -let package = Package( - name: "SwiftMETAR", - defaultLocalization: "en", - platforms: [.macOS(.v13), .iOS(.v16), .tvOS(.v16), .watchOS(.v9), .visionOS(.v1)], - products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. - .library( - name: "SwiftMETAR", - targets: ["SwiftMETAR"] - ), +// METARFormatting (and DecodeMETAR/DecodeTAF, which depend on it) format `Measurement` +// values using `Measurement.FormatStyle` and the `\(_, format:)` string interpolation +// sugar for custom `FormatStyle` types. Neither is implemented by swift-corelibs-foundation, +// so these targets/products are only defined on Apple platforms; SwiftMETAR and +// DecodeWindsAloft don't use these APIs and build fine everywhere. +var products: [Product] = [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "SwiftMETAR", + targets: ["SwiftMETAR"] + ), + .executable(name: "decode-winds-aloft", targets: ["DecodeWindsAloft"]) +] + +var targets: [Target] = [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "SwiftMETAR", + dependencies: [.product(name: "NumberKit", package: "swift-numberkit")], + resources: [.process("Resources")], + swiftSettings: approachableConcurrency + ), + .testTarget( + name: "SwiftMETARTests", + dependencies: ["SwiftMETAR"], + swiftSettings: approachableConcurrency + ), + .executableTarget( + name: "DecodeWindsAloft", + dependencies: [ + "SwiftMETAR", + .product(name: "ArgumentParser", package: "swift-argument-parser") + ], + swiftSettings: approachableConcurrency + ) +] + +#if !os(Linux) + products += [ .library( name: "METARFormatting", targets: ["METARFormatting"] ), .executable(name: "decode-metar", targets: ["DecodeMETAR"]), - .executable(name: "decode-taf", targets: ["DecodeTAF"]), - .executable(name: "decode-winds-aloft", targets: ["DecodeWindsAloft"]) - ], - dependencies: [ - // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/objecthub/swift-numberkit.git", from: "2.6.0"), - .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.4.3"), - .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), - .package(url: "https://github.com/riscfuture/BuildableMacro.git", from: "1.0.0") - ], - targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. - .target( - name: "SwiftMETAR", - dependencies: [.product(name: "NumberKit", package: "swift-numberkit")], - resources: [.process("Resources")], - swiftSettings: approachableConcurrency - ), + .executable(name: "decode-taf", targets: ["DecodeTAF"]) + ] + + targets += [ .target( name: "METARFormatting", dependencies: [ @@ -50,11 +65,6 @@ let package = Package( resources: [.process("Resources")], swiftSettings: approachableConcurrency ), - .testTarget( - name: "SwiftMETARTests", - dependencies: ["SwiftMETAR"], - swiftSettings: approachableConcurrency - ), .executableTarget( name: "DecodeMETAR", dependencies: [ @@ -72,15 +82,22 @@ let package = Package( .product(name: "ArgumentParser", package: "swift-argument-parser") ], swiftSettings: approachableConcurrency - ), - .executableTarget( - name: "DecodeWindsAloft", - dependencies: [ - "SwiftMETAR", - .product(name: "ArgumentParser", package: "swift-argument-parser") - ], - swiftSettings: approachableConcurrency ) + ] +#endif + +let package = Package( + name: "SwiftMETAR", + defaultLocalization: "en", + platforms: [.macOS(.v13), .iOS(.v16), .tvOS(.v16), .watchOS(.v9), .visionOS(.v1)], + products: products, + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(url: "https://github.com/objecthub/swift-numberkit.git", from: "2.6.0"), + .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.4.3"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), + .package(url: "https://github.com/riscfuture/BuildableMacro.git", from: "1.0.0") ], + targets: targets, swiftLanguageModes: [.v5, .v6] ) diff --git a/Sources/DecodeMETAR/DecodeMETAR.swift b/Sources/DecodeMETAR/DecodeMETAR.swift index ea8de75..6249a28 100644 --- a/Sources/DecodeMETAR/DecodeMETAR.swift +++ b/Sources/DecodeMETAR/DecodeMETAR.swift @@ -1,5 +1,8 @@ import ArgumentParser import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif import METARFormatting import SwiftMETAR diff --git a/Sources/DecodeTAF/DecodeTAF.swift b/Sources/DecodeTAF/DecodeTAF.swift index fae013e..a252388 100644 --- a/Sources/DecodeTAF/DecodeTAF.swift +++ b/Sources/DecodeTAF/DecodeTAF.swift @@ -1,5 +1,8 @@ import ArgumentParser import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif import METARFormatting import SwiftMETAR diff --git a/Sources/DecodeWindsAloft/DecodeWindsAloft.swift b/Sources/DecodeWindsAloft/DecodeWindsAloft.swift index 9dc5456..4846c3d 100644 --- a/Sources/DecodeWindsAloft/DecodeWindsAloft.swift +++ b/Sources/DecodeWindsAloft/DecodeWindsAloft.swift @@ -1,5 +1,8 @@ import ArgumentParser import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif import SwiftMETAR @available(macOS 15.0, *) diff --git a/Sources/SwiftMETAR/Error.swift b/Sources/SwiftMETAR/Error.swift index d99ac50..1d5d4a6 100644 --- a/Sources/SwiftMETAR/Error.swift +++ b/Sources/SwiftMETAR/Error.swift @@ -56,165 +56,85 @@ extension Error: LocalizedError { public var errorDescription: String? { switch self { case .invalidWindsAloftHeader, .invalidWindsAloftGroup, .invalidWindsAloftColumns: - #if canImport(Darwin) - return String( - localized: "Couldn’t parse Winds Aloft product.", - comment: "error description" - ) - #else - return "Couldn’t parse Winds Aloft product." - #endif + return String( + localized: "Couldn’t parse Winds Aloft product.", + comment: "error description" + ) default: - #if canImport(Darwin) - return String(localized: "Couldn’t parse METAR or TAF.", comment: "error description") - #else - return "Couldn’t parse METAR or TAF." - #endif + return String(localized: "Couldn’t parse METAR or TAF.", comment: "error description") } } public var failureReason: String? { switch self { case .badFormat: - #if canImport(Darwin) - return String(localized: "METAR or TAF format is incorrect.", comment: "failure reason") - #else - return "METAR or TAF format is incorrect." - #endif + return String(localized: "METAR or TAF format is incorrect.", comment: "failure reason") case .invalidDate(let date): - #if canImport(Darwin) - return String(localized: "Invalid date ‘\(date)’.", comment: "failure reason") - #else - return "Invalid date ‘\(date)’." - #endif + return String(localized: "Invalid date ‘\(date)’.", comment: "failure reason") case .invalidWinds(let winds): - #if canImport(Darwin) - return String(localized: "Invalid winds ‘\(winds)’.", comment: "failure reason") - #else - return "Invalid winds ‘\(winds)’." - #endif + return String(localized: "Invalid winds ‘\(winds)’.", comment: "failure reason") case .invalidVisibility(let visibility): - #if canImport(Darwin) - return String(localized: "Invalid visibility ‘\(visibility)’.", comment: "failure reason") - #else - return "Invalid visibility ‘\(visibility)’." - #endif + return String(localized: "Invalid visibility ‘\(visibility)’.", comment: "failure reason") case .invalidWeather(let weather): - #if canImport(Darwin) - return String(localized: "Invalid weather ‘\(weather)’.", comment: "failure reason") - #else - return "Invalid weather ‘\(weather)’." - #endif + return String(localized: "Invalid weather ‘\(weather)’.", comment: "failure reason") case .invalidConditions(let conditions): - #if canImport(Darwin) - return String(localized: "Invalid conditions ‘\(conditions)’.", comment: "failure reason") - #else - return "Invalid conditions ‘\(conditions)’." - #endif + return String(localized: "Invalid conditions ‘\(conditions)’.", comment: "failure reason") case .invalidTempDewpoint(let temps): - #if canImport(Darwin) - return String( - localized: "Invalid temperature and dewpoint ‘\(temps)’.", - comment: "failure reason" - ) - #else - return "Invalid temperature and dewpoint ‘\(temps)’." - #endif + return String( + localized: "Invalid temperature and dewpoint ‘\(temps)’.", + comment: "failure reason" + ) case .invalidAltimeter(let altimeter): - #if canImport(Darwin) - return String( - localized: "Invalid altimeter setting ‘\(altimeter)’.", - comment: "failure reason" - ) - #else - return "Invalid altimeter setting ‘\(altimeter)’." - #endif + return String( + localized: "Invalid altimeter setting ‘\(altimeter)’.", + comment: "failure reason" + ) case .invalidPeriod(let period): - #if canImport(Darwin) - return String(localized: "Invalid TAF period ‘\(period)’.", comment: "failure reason") - #else - return "Invalid TAF period ‘\(period)’." - #endif + return String(localized: "Invalid TAF period ‘\(period)’.", comment: "failure reason") case .invalidWindshear(let windshear): - #if canImport(Darwin) - return String( - localized: "Invalid low-level windshear ‘\(windshear)’.", - comment: "failure reason" - ) - #else - return "Invalid low-level windshear ‘\(windshear)’." - #endif + return String( + localized: "Invalid low-level windshear ‘\(windshear)’.", + comment: "failure reason" + ) case .invalidIcing(let icing): - #if canImport(Darwin) - return String(localized: "Invalid icing ‘\(icing)’", comment: "failure reason") - #else - return "Invalid icing ‘\(icing)’" - #endif + return String(localized: "Invalid icing ‘\(icing)’", comment: "failure reason") case .invalidTurbulence(let turbulence): - #if canImport(Darwin) - return String(localized: "Invalid turbulence ‘\(turbulence)’", comment: "failure reason") - #else - return "Invalid turbulence ‘\(turbulence)’" - #endif + return String(localized: "Invalid turbulence ‘\(turbulence)’", comment: "failure reason") case .invalidForecastTemperature(let temp): - #if canImport(Darwin) - return String( - localized: "Invalid forecast temperature '\(temp)'", - comment: "failure reason" - ) - #else - return "Invalid forecast temperature '\(temp)'" - #endif + return String( + localized: "Invalid forecast temperature '\(temp)'", + comment: "failure reason" + ) case .invalidWindsAloftHeader(let header): - #if canImport(Darwin) - return String( - localized: "Invalid winds aloft header '\(header)'.", - comment: "failure reason" - ) - #else - return "Invalid winds aloft header '\(header)'." - #endif + return String( + localized: "Invalid winds aloft header '\(header)'.", + comment: "failure reason" + ) case .invalidWindsAloftGroup(let group): - #if canImport(Darwin) - return String( - localized: "Invalid winds aloft data group '\(group)'.", - comment: "failure reason" - ) - #else - return "Invalid winds aloft data group '\(group)'." - #endif + return String( + localized: "Invalid winds aloft data group '\(group)'.", + comment: "failure reason" + ) case .invalidWindsAloftColumns(let columns): - #if canImport(Darwin) - return String( - localized: "Invalid winds aloft column layout '\(columns)'.", - comment: "failure reason" - ) - #else - return "Invalid winds aloft column layout '\(columns)'." - #endif + return String( + localized: "Invalid winds aloft column layout '\(columns)'.", + comment: "failure reason" + ) } } public var recoverySuggestion: String? { switch self { case .invalidWindsAloftHeader, .invalidWindsAloftGroup, .invalidWindsAloftColumns: - #if canImport(Darwin) - return String( - localized: "Verify the format of the Winds Aloft product.", - comment: "recovery suggestion" - ) - #else - return "Verify the format of the Winds Aloft product." - #endif + return String( + localized: "Verify the format of the Winds Aloft product.", + comment: "recovery suggestion" + ) default: - #if canImport(Darwin) - return String( - localized: "Verify the format of the METAR or TAF string.", - comment: "recovery suggestion" - ) - #else - return "Verify the format of the METAR or TAF string." - #endif + return String( + localized: "Verify the format of the METAR or TAF string.", + comment: "recovery suggestion" + ) } } } diff --git a/Sources/SwiftMETAR/LinuxLocalization.swift b/Sources/SwiftMETAR/LinuxLocalization.swift new file mode 100644 index 0000000..d02c767 --- /dev/null +++ b/Sources/SwiftMETAR/LinuxLocalization.swift @@ -0,0 +1,21 @@ +// `String(localized:)` (and `LocalizedStringResource`) come from +// FoundationInternationalization on Apple platforms but are unavailable on Linux. +// These packages use their default ("en") localization text as the lookup key, so on +// Linux we resolve each key to itself. This shim is excluded on Apple, where the real +// Foundation API is used instead. + +#if !canImport(Darwin) + import Foundation + + extension String { + init( + localized key: String, + table: String? = nil, + bundle: Bundle? = nil, + locale: Locale? = nil, + comment: StaticString? = nil + ) { + self = key + } + } +#endif diff --git a/Sources/SwiftMETAR/METAR/Parsers/METARXMLParser.swift b/Sources/SwiftMETAR/METAR/Parsers/METARXMLParser.swift index 6c77d19..211c89f 100644 --- a/Sources/SwiftMETAR/METAR/Parsers/METARXMLParser.swift +++ b/Sources/SwiftMETAR/METAR/Parsers/METARXMLParser.swift @@ -1,4 +1,7 @@ import Foundation +#if canImport(FoundationXML) + import FoundationXML +#endif actor METARXMLParser { diff --git a/Sources/SwiftMETAR/TAF/Parsers/TAFXMLParser.swift b/Sources/SwiftMETAR/TAF/Parsers/TAFXMLParser.swift index a98167c..cc7dd0b 100644 --- a/Sources/SwiftMETAR/TAF/Parsers/TAFXMLParser.swift +++ b/Sources/SwiftMETAR/TAF/Parsers/TAFXMLParser.swift @@ -1,4 +1,7 @@ import Foundation +#if canImport(FoundationXML) + import FoundationXML +#endif actor TAFXMLParser { From be4453f7fb5922541e46aabfdbf9717019089745 Mon Sep 17 00:00:00 2001 From: Tim Morgan Date: Mon, 6 Jul 2026 19:53:11 -0700 Subject: [PATCH 2/3] Silence SwiftLint unused-parameter warnings in the Linux localization shim Co-Authored-By: Claude Opus 4.8 (1M context) --- Sources/SwiftMETAR/LinuxLocalization.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftMETAR/LinuxLocalization.swift b/Sources/SwiftMETAR/LinuxLocalization.swift index d02c767..cc79785 100644 --- a/Sources/SwiftMETAR/LinuxLocalization.swift +++ b/Sources/SwiftMETAR/LinuxLocalization.swift @@ -10,10 +10,10 @@ extension String { init( localized key: String, - table: String? = nil, - bundle: Bundle? = nil, - locale: Locale? = nil, - comment: StaticString? = nil + table _: String? = nil, + bundle _: Bundle? = nil, + locale _: Locale? = nil, + comment _: StaticString? = nil ) { self = key } From 1222044add736c62e7202acbdc080c41d678e205 Mon Sep 17 00:00:00 2001 From: Tim Morgan Date: Mon, 6 Jul 2026 20:40:47 -0700 Subject: [PATCH 3/3] ci: move SwiftLint and docs build to Ubuntu runners Keep the Build-and-Test matrix on both macOS and Ubuntu. Move the SwiftLint job to ubuntu-latest via the ghcr.io/realm/swiftlint container (SwiftLint runs cleanly on Linux), and move the documentation build to ubuntu-latest. Periphery stays on macOS (needs an index-store build with no clean Linux install path). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 376723d..304b9af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,11 +52,10 @@ jobs: run: swift test -v lint: name: Run SwiftLint - runs-on: macos-latest + runs-on: ubuntu-latest + container: ghcr.io/realm/swiftlint:latest steps: - uses: actions/checkout@v6 - - name: Install SwiftLint - run: brew install swiftlint - name: Run SwiftLint run: swiftlint --strict swift-format: