Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a6dcd4f
:heavy_plus_sign: add ReerKit dependency on Linux
techouse Sep 8, 2025
99abcf0
:children_crossing: add NSMapTable for Linux
techouse Sep 8, 2025
e08cdd0
:children_crossing: do not compile QsSwiftComparison on non-Apple pla…
techouse Sep 8, 2025
eea44ba
:see_no_evil: ignore cache
techouse Sep 8, 2025
1e9fc83
:hammer: do not keep history of cache
techouse Sep 8, 2025
2ab515d
:children_crossing: enhance NSNumber handling for Linux compatibility…
techouse Sep 8, 2025
6989252
:test_tube: add Linux compatibility for NSDictionary self-referential…
techouse Sep 16, 2025
f0b718e
:see_no_evil: add core directory to .gitignore for Linux compatibility
techouse Sep 16, 2025
c0729a5
:test_tube: enhance Linux compatibility for self-referential collecti…
techouse Sep 17, 2025
40ec7fa
:construction_worker: add Linux support for Swift tests in CI configu…
techouse Sep 17, 2025
7f4892d
:green_heart: update CI configuration to use 'ubuntu-latest' for Swif…
techouse Sep 17, 2025
9a716cc
:test_tube: add Linux-specific handling for Shift JIS encoding in Enc…
techouse Sep 17, 2025
5a31f31
:green_heart: optimize Swift test command for quieter output in CI co…
techouse Sep 17, 2025
2296ee0
:green_heart: refactor CI configuration to run coverage script instea…
techouse Sep 17, 2025
db493c1
:memo: update README to reflect experimental Linux support and caveats
techouse Sep 17, 2025
982b36d
:memo: update README to correct Swift version for experimental Linux …
techouse Sep 17, 2025
e849152
:green_heart: update CI configuration to use ubuntu-latest for Linux …
techouse Sep 17, 2025
d984d7f
:package: refactor Package.swift
techouse Sep 17, 2025
43fde42
:package: update Package.resolved
techouse Sep 17, 2025
b4fe284
:bulb: update error message for ReerKit dependency on Linux
techouse Sep 17, 2025
ec6f7be
:see_no_evil: update .gitignore to include trailing slashes for direc…
techouse Sep 17, 2025
9d2c72d
:memo: correct logo image URL in README.md
techouse Sep 17, 2025
7a4994e
:see_no_evil: update .gitignore to remove trailing slashes from .swif…
techouse Sep 17, 2025
ce3d233
:recycle: update conditional compilation for Linux and improve error …
techouse Sep 17, 2025
b92d047
:test_tube: improve Linux compatibility in tests and enhance type safety
techouse Sep 17, 2025
8f8df0b
:test_tube: fix encodeOrNil_cycle test to handle errors on Linux
techouse Sep 17, 2025
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
53 changes: 50 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
tests:
permissions:
contents: read
name: Swift tests + coverage
name: Swift tests + coverage (macOS, Xcode)
timeout-minutes: 45
runs-on: macos-15
needs: analyze
Expand Down Expand Up @@ -118,7 +118,7 @@ jobs:
objc-tests:
permissions:
contents: read
name: Objective-C E2E tests (Xcode)
name: Objective-C E2E tests (macOS, Xcode)
timeout-minutes: 45
runs-on: macos-15
needs: [ analyze, tests ]
Expand Down Expand Up @@ -250,6 +250,53 @@ jobs:
path: crashlogs-objc
if-no-files-found: ignore

linux-tests:
permissions:
contents: read
name: Swift tests (Linux)
runs-on: ubuntu-latest
timeout-minutes: 40
continue-on-error: true
needs: analyze
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Setup Swift ${{ env.SWIFT_VERSION }}
uses: swift-actions/setup-swift@v2
with:
swift-version: ${{ env.SWIFT_VERSION }}

- name: Show Swift & OS versions
run: |
swift --version
uname -a

- name: Cache SwiftPM artifacts (Linux)
uses: actions/cache@v4
with:
path: |
~/.swiftpm
.build
~/.cache/org.swift.swiftpm
key: spm-${{ runner.os }}-swift-${{ env.SWIFT_VERSION }}-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
spm-${{ runner.os }}-swift-${{ env.SWIFT_VERSION }}-

- name: Run tests (Linux)
env:
SKIP_EXPENSIVE_TESTS: 1
SWIFT_DETERMINISTIC_HASHING: 1
run: bash scripts/coverage.sh

- name: Kernel logs on failure
if: ${{ failure() }}
run: |
set -euxo pipefail
dmesg | tail -n 200 || true

ensure-compatibility:
name: Ensure compatibility with qs.js
needs: [ analyze, tests ]
Expand Down Expand Up @@ -292,7 +339,7 @@ jobs:

- name: Install qs.js
working-directory: Tools/QsSwiftComparison/js
run: npm ci --no-audit --no-fund
run: npm ci --no-audit --no-fund

- name: Show qs.js version
working-directory: Tools/QsSwiftComparison/js
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.swiftpm/cache
.netrc
.history
.idea
.cache
coverage/
docs/
core
core.*
vgcore.*
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"**/.swiftpm/**",
"**/.vscode/**",
"**/.idea/**",
"**/.cache/**",
"**/node_modules/**",
"**/bin/**",
"**/obj/**"
Expand Down
97 changes: 0 additions & 97 deletions LinuxSupport.md

This file was deleted.

2 changes: 1 addition & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 16 additions & 9 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@

import PackageDescription

var deps: [Package.Dependency] = [
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.2.1"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.2.1"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),
]
var targetDeps: [Target.Dependency] = [
.product(name: "Algorithms", package: "swift-algorithms"),
.product(name: "OrderedCollections", package: "swift-collections"),
]
#if os(Linux)
deps.append(.package(url: "https://github.com/reers/ReerKit.git", from: "1.1.9"))
targetDeps.append(.product(name: "ReerKit", package: "ReerKit"))
#endif

let package = Package(
name: "QsSwift",
platforms: [
Expand All @@ -12,18 +26,11 @@ let package = Package(
.library(name: "QsSwift", targets: ["QsSwift"]),
.library(name: "QsObjC", targets: ["QsObjC"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.2.1"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.2.1"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),
],
dependencies: deps,
targets: [
.target(
name: "QsSwift",
dependencies: [
.product(name: "Algorithms", package: "swift-algorithms"),
.product(name: "OrderedCollections", package: "swift-collections"),
],
dependencies: targetDeps,
path: "Sources/QsSwift"
),
.target(
Expand Down
27 changes: 17 additions & 10 deletions Package@swift-5.10.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@

import PackageDescription

var deps: [Package.Dependency] = [
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.2.1"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.2.1"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),
.package(url: "https://github.com/apple/swift-testing.git", from: "0.9.0"),
]
var targetDeps: [Target.Dependency] = [
.product(name: "Algorithms", package: "swift-algorithms"),
.product(name: "OrderedCollections", package: "swift-collections"),
]
#if os(Linux)
deps.append(.package(url: "https://github.com/reers/ReerKit.git", from: "1.1.9"))
targetDeps.append(.product(name: "ReerKit", package: "ReerKit"))
#endif

let package = Package(
name: "QsSwift",
platforms: [
Expand All @@ -12,19 +27,11 @@ let package = Package(
.library(name: "QsSwift", targets: ["QsSwift"]),
.library(name: "QsObjC", targets: ["QsObjC"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.2.1"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.2.1"),
.package(url: "https://github.com/apple/swift-testing.git", from: "0.9.0"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),
],
dependencies: deps,
targets: [
.target(
name: "QsSwift",
dependencies: [
.product(name: "Algorithms", package: "swift-algorithms"),
.product(name: "OrderedCollections", package: "swift-collections"),
],
dependencies: targetDeps,
path: "Sources/QsSwift",
swiftSettings: [
.unsafeFlags(["-strict-concurrency=complete"], .when(configuration: .debug)),
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# QsSwift

<p align="center">
<img src="https://github.com/techouse/qs-swift/raw/main/logo.png?raw=true?raw=true" width="256" alt="QsSwift" />
<img src="https://github.com/techouse/qs-swift/raw/main/logo.png?raw=true" width="256" alt="QsSwift" />
</p>

A fast, flexible query string **encoding/decoding** library for Swift and [Objective-C](#objective-c).
Expand Down Expand Up @@ -561,11 +561,17 @@ See the [QsObjC README](Sources/QsObjC/README.md) for installation, options, and

---

## Linux support status
## Linux support

Linux support is currently **not available**. The package relies on Objective-C-only Foundation types (e.g., `NSMapTable` / `NSHashTable`) and behavior used by the encoder’s cycle-detection side channel and the Objective-C bridge. These APIs are unavailable on Swift Foundation for Linux, and a safe, pure-Swift replacement is still being evaluated.
**Experimental** (Swift 6.0+)

See **[LinuxSupport.md](LinuxSupport.md)** for background, limitations, and the migration plan (including ideas for a pure-Swift side-channel and conditional compilation guards).
On non‑Apple platforms, QsSwift uses ReerKit’s `WeakMap` to emulate `NSMapTable.weakToWeakObjects()` (weak keys **and**
weak values) for the encoder’s cycle‑detection side‑channel. This works around CoreFoundation APIs that aren’t available
in swift‑corelibs‑foundation on Linux.

**Caveats**
- Some tests that construct *self‑referential* `NSArray`/`NSDictionary` graphs are wrapped in `withKnownIssue` because swift‑corelibs‑foundation can crash when creating those graphs. (Apple platforms are unaffected.)
- CI includes an **experimental Ubuntu** job and is marked `continue-on-error` while Linux behavior stabilizes.

---

Expand Down
42 changes: 42 additions & 0 deletions Sources/QsSwift/Internal/NSMapTable+Linux.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#if os(Linux)
import Foundation
#if canImport(ReerKit)
import ReerKit

/// Linux shim: adapt ReerKit's `WeakMap` (weak key + weak value) to the minimal
/// `NSMapTable` API surface that Qs uses (weakToWeakObjects, setObject, object(forKey:)).
///
/// This lets existing call sites keep using `NSMapTable<AnyObject, AnyObject>.weakToWeakObjects()`
/// without conditional code. On Apple platforms the real `NSMapTable` is used; on Linux this shim
/// is compiled instead.
final class NSMapTable<Key: AnyObject, Value: AnyObject> {
// ReerKit exposes WeakMap which can be configured for weak keys and weak values.
// We use NSObject as the storage types to interoperate with Foundation containers
// commonly used as keys/values during encoding.
private var map = WeakMap<WeakKeyValue, NSObject, NSObject>()

/// Matches Foundation's convenience: create a weak-to-weak map table.
static func weakToWeakObjects() -> NSMapTable<Key, Value> { NSMapTable<Key, Value>() }

/// Store or remove a value for a key. Passing `nil` removes the entry.
@inline(__always)
func setObject(_ obj: Value?, forKey key: Key) {
guard let keyNS = key as? NSObject else { return }
if let vNS = obj as? NSObject {
map[keyNS] = vNS
} else {
_ = map.removeValue(forKey: keyNS)
}
}

/// Retrieve a value for a key, or `nil` if none (or if the key/value was released).
@inline(__always)
func object(forKey key: Key) -> Value? {
guard let keyNS = key as? NSObject else { return nil }
return map[keyNS] as? Value
}
}
#else
#error("ReerKit is required on Linux. Add it as a conditional dependency for Linux in Package.swift.")
#endif
#endif
Loading