diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..35b122c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,97 @@ +# NOTE: every job here fails on a clean checkout until PureSwift/AndroidBluetooth#4 is merged. +# AndroidBluetooth requests the `AndroidManifest` product from `Android`, but it moved to +# `swift-android-native` (PureSwift/Android#40), and SwiftPM validates the whole package graph even +# for dependencies that are conditional on `.android`: +# error: product 'AndroidManifest' required by package 'androidbluetooth' ... not found +# That is a one-line manifest fix upstream, not a problem with this repo. +name: CI + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test + runs-on: macos-15 + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + - name: Build + run: swift build --build-tests + - name: Test + run: swift test + + ios-archive: + name: iOS Archive + runs-on: macos-15 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + # WasmKit declares `.treatAllWarnings(as: .error)` for Apple platforms, which conflicts with + # the `-suppress-warnings` Xcode passes to package dependencies: + # error: Conflicting options '-warnings-as-errors' and '-suppress-warnings' + # The setting only reaches package targets from the command line, so it cannot live in the + # xcconfig or the project. + - name: Archive + working-directory: Darwin + run: | + xcodebuild archive \ + -project BluetoothExplorer.xcodeproj \ + -scheme "BluetoothExplorer App" \ + -destination 'generic/platform=iOS' \ + -archivePath "$RUNNER_TEMP/BluetoothExplorer.xcarchive" \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + SWIFT_SUPPRESS_WARNINGS=NO \ + CODE_SIGNING_ALLOWED=NO + + - name: Verify archive contents + run: | + APP="$RUNNER_TEMP/BluetoothExplorer.xcarchive/Products/Applications/BluetoothExplorer.app" + test -d "$APP" || { echo "::error::archive did not produce BluetoothExplorer.app"; exit 1; } + # The bundled WASM parser plugins must ship inside the app. + find "$APP" -name '*.wasm' -print | tee /tmp/wasm-list + test -s /tmp/wasm-list || { echo "::error::no bundled .wasm plugins in the archive"; exit 1; } + + - uses: actions/upload-artifact@v4 + with: + name: ios-archive + path: ${{ runner.temp }}/BluetoothExplorer.xcarchive + retention-days: 14 + + android-package: + name: Android Swift Package + runs-on: macos-15 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Install Swift Android SDK + run: | + brew install skiptools/skip/skip || (brew update && brew install skiptools/skip/skip) + skip android sdk install --version 6.3.3 + + # Only the plugin engine is built here. The full app does not yet cross-compile for Android: + # AndroidBluetooth's `@JavaImplementation` macro generates invalid code for its scan callback, + # and WasmKit's SystemExtras target does not compile against the Android sysroot + # (`st_mode` is UInt32 there while swift-system's CInterop.Mode is UInt16). Neither is + # reachable from this target. See Documentation/AndroidSwiftUIMigration.md. + - name: Build plugin engine for Android + run: swift build --swift-sdk swift-6.3.3-RELEASE_android --target BluetoothExplorerPluginEngine diff --git a/Darwin/BluetoothExplorer.xcconfig b/Darwin/BluetoothExplorer.xcconfig index cac0327..fea7034 100644 --- a/Darwin/BluetoothExplorer.xcconfig +++ b/Darwin/BluetoothExplorer.xcconfig @@ -1,12 +1,17 @@ -#include "../Skip.env" - -// Set the action that will be executed as part of the Xcode Run Script phase -// Setting to "launch" will build and run the app in the first open Android emulator or device -// Setting to "build" will just run gradle build, but will not launch the app -// Setting to "none" will completely disable the build and launch of the Android app -//SKIP_ACTION = launch -//SKIP_ACTION = build -SKIP_ACTION = none +// App identity. These previously lived in Skip.env, which was shared with the Android build; with +// Skip removed they are declared here and the Android project carries its own copy. + +// PRODUCT_NAME must match the app's Swift module name +PRODUCT_NAME = BluetoothExplorer + +// The unique id for the app +PRODUCT_BUNDLE_IDENTIFIER = org.pureswift.bluetoothexplorer + +// The semantic version of the app +MARKETING_VERSION = 1.0.0 + +// The build number specifying the internal app version +CURRENT_PROJECT_VERSION = 1 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor diff --git a/Documentation/AndroidSwiftUIMigration.md b/Documentation/AndroidSwiftUIMigration.md new file mode 100644 index 0000000..dc0a87c --- /dev/null +++ b/Documentation/AndroidSwiftUIMigration.md @@ -0,0 +1,126 @@ +# Migrating off Skip to AndroidSwiftUI + +The app previously used [Skip](https://skip.tools) to compile its SwiftUI codebase for Android. It +now uses the system SwiftUI on Apple platforms and [PureSwift/AndroidSwiftUI](https://github.com/PureSwift/AndroidSwiftUI) +on Android. + +## What Skip was doing + +Skip's footprint was smaller than it appeared — nine `import` lines and one `Logger` — but it also +owned the build: + +| Skip piece | Replacement | +|---|---| +| `SkipFuseUI` — SwiftUI for Android | `AndroidSwiftUI`, linked only `.when(platforms: [.android])` | +| `SkipModel` — Observation for Android | `import Observation` directly; the Swift Android SDK ships `Observation.swiftmodule` | +| `SkipFuse` — Foundation shims, `Logger` | `AppLogger` in `Sources/BluetoothExplorer/Logging.swift` (`os.Logger` on Apple, print-based elsewhere) | +| `skipstone` build plugin + three `skip.yml` | nothing — plain SwiftPM targets | +| `/* SKIP @bridge */` on the root view and app delegate | nothing — `Darwin/Sources/Main.swift` was already a real `@main` App | +| `ComposeView` / `#if SKIP` Compose interop in `ContentView` | removed with the rest of the Skip template scaffolding | +| `Skip.env` | removed | + +Views now import conditionally: + +```swift +#if canImport(SwiftUI) +import SwiftUI +#else +import AndroidSwiftUI +#endif +``` + +## The dependency-graph side effect + +This removed the whole Skip fork stack — `skip-fuse`, `skip-fuse-ui`, `skip-android-bridge`, +`skip-ui`, `skip-foundation`, `skip-model`, and their transitive Java/JNI packages. The package +dropped from ~30 resolved dependencies to 20, and **the graph now resolves from a clean checkout**: + +```sh +swift package resolve # exit 0, no mirrors, no SWIFTPM_ENABLE_MACROS=0 +``` + +Every blocker recorded in `Documentation/DependencyState.md` traced back to that stack: + +- the `swift-android-native` two-URL conflict needed `skip-android-bridge` as one of the two + claimants — with Skip gone there is only one claimant left; +- the `swift-java` / `swift-syntax` 603-vs-602 conflict came in through the same graph; +- `skip-fuse-ui` was the package that could not build against current `skip-ui`. + +Two `PureSwift` fixes are still required and are still open as PRs — `PureSwift/Android#43` and +`PureSwift/AndroidBluetooth#4`. Until they merge, local SwiftPM mirrors pointing at fixed clones are +needed; `AndroidBluetooth` otherwise fails with `product 'AndroidManifest' ... not found in package +'Android'`. + +## What AndroidSwiftUI needed + +The app's UI relies on SwiftUI that AndroidSwiftUI did not implement. These were added to +AndroidSwiftUI first (branch `feature/swiftui-containers`): + +- `NavigationStack` — reusing the existing `NavigationContext`, so `NavigationLink` push and + hardware-back pop are shared with `NavigationView` rather than duplicated +- `LazyVStack` / `LazyHStack` — eager, mapping onto the same LinearLayout path as `VStack`/`HStack` +- `TabView` with `.tabItem` and selection — tab items are read back by walking the modifier chain + for trait-writing modifiers; only the selected tab is mounted +- `.sheet(isPresented:content:)` — a full-screen overlay in the same view hierarchy rather than an + Android `Dialog`, because the fiber renderer mounts children into the parent's `ViewGroup` and has + no path to a `Dialog`'s separate decor view +- `.refreshable` — stores a `RefreshAction` in the environment as SwiftUI does, but no gesture + triggers it: binding `SwipeRefreshLayout` would need an androidx dependency the package lacks +- Swift **Observation** support — `.environment(object)` and `@Environment(Type.self)`, with + invalidation driven by `withObservationTracking` + +`.fileImporter` is not needed on Android: the plugin import button is already gated behind +`#if !os(Android)`. + +### Known gaps in those additions + +All four compile and were verified to build together for Android, but none has been exercised on a +device or emulator. Notable limitations, each documented in the source: + +- **Observation**: `@Bindable` is not implemented, and `@Environment(Store.self) var store: Store?` + (the optional form) is unsupported — a missing injection traps rather than yielding `nil`. +- **TabView**: unselected tabs are unmounted, so their `@State` resets; no `TabViewStyle`, paging or + badges. +- **Sheet**: no animation, detents or drag-to-dismiss, and no `@Environment(\.dismiss)` — content + must dismiss itself through the binding. +- **NavigationStack**: `NavigationStack(path:)` and value-based `navigationDestination(for:)` are + not implemented; `NavigationContext.path` holds type-erased views, not a `Hashable` data path. + +## Platform status + +- **Apple platforms** — the package resolves and every target builds with no Skip involvement. +- **Android** — the dependency wiring is in place, but the Android app build has not been verified + end to end. It additionally needs the two PureSwift PRs above, and the Kotlin JNI peers under + `Sources/BluetoothExplorer/Skip/` (`ScanCallback.kt`, `BluetoothGattCallback.kt`) rehomed into the + Android app project — they are peers for `AndroidBluetooth`, unrelated to Skip, and were only + living in that directory because skipstone collected them. + +## Building an Android app archive + +CI (`.github/workflows/ci.yml`) archives the iOS app but does **not** produce an Android `.aab`. +Three things block it, each verified by trying: + +1. **The app does not cross-compile for Android.** `swift build --swift-sdk … --target BluetoothExplorer` + fails inside `AndroidBluetooth`: its `@JavaImplementation` macro generates invalid Swift for + `LowEnergyScanCallback.swiftOnScanFailed(error:)` — the parameter named `error` is emitted into a + context where it parses as a keyword, producing + `declaration name '…' is not covered by macro 'JavaImplementation'`. This is an upstream + swift-java/AndroidBluetooth bug. +2. **A whole-package Android build additionally fails in WasmKit's `SystemExtras`**: it does + `st_mode & S_IFMT`, but `st_mode` is `UInt32` in the Android sysroot while swift-system's + `CInterop.Mode` is `UInt16`. This target is not reachable from the app, so building specific + targets avoids it; a plain `swift build` does not. +3. **The Android project is still Skip's.** `Android/settings.gradle.kts` shells out to + `skip plugin --prebuild`, and `Android/app/build.gradle.kts` applies `skip-build-plugin` with a + `skip { }` block that reads the now-deleted `Skip.env`. With Skip removed none of that resolves. + +Replacing (3) means writing a conventional Android app project, using +[AndroidSwiftUI's `Demo/`](https://github.com/PureSwift/AndroidSwiftUI/tree/master/Demo) as the +template: its `app/src/main/java/com/pureswift/swiftandroid/` Kotlin classes (`MainActivity`, +`Application`, `NativeLibrary`, `ListViewAdapter`, `Fragment`, …) are the peers AndroidSwiftUI's +`@JavaClass` bindings bind to, and `build-swift.sh` shows the packaging step — build the Swift +package for the target architecture, then copy the resulting `.so` into +`app/src/main/jniLibs//`. The Kotlin JNI peers currently under `Sources/BluetoothExplorer/Skip/` +(`ScanCallback.kt`, `BluetoothGattCallback.kt`) belong in that project too. + +None of this is worth doing until (1) is fixed upstream, since the `.so` cannot be produced. diff --git a/Package.swift b/Package.swift index 53f1e4b..7a00432 100644 --- a/Package.swift +++ b/Package.swift @@ -1,5 +1,4 @@ // swift-tools-version: 6.2 -// This is a Skip (https://skip.tools) package. import PackageDescription let package = Package( @@ -10,13 +9,12 @@ let package = Package( .library(name: "BluetoothExplorer", type: .dynamic, targets: ["BluetoothExplorer"]) ], dependencies: [ - .package(url: "https://source.skip.tools/skip.git", from: "1.7.1"), - .package(url: "https://source.skip.tools/skip-model.git", from: "1.0.0"), - .package(url: "https://github.com/MillerTechnologyPeru/skip-fuse-ui.git", branch: "feature/pureswift"), - .package(url: "https://github.com/MillerTechnologyPeru/skip-fuse.git", branch: "feature/pureswift"), .package(url: "https://github.com/PureSwift/GATT.git", branch: "master"), .package(url: "https://github.com/PureSwift/AndroidBluetooth.git", branch: "master"), .package(url: "https://github.com/PureSwift/Bluetooth.git", from: "7.2.0"), + // SwiftUI for Android. Apple platforms use the system SwiftUI instead, so this is only + // linked for .android — see the conditional target dependencies below. + .package(url: "https://github.com/PureSwift/AndroidSwiftUI.git", branch: "master"), .package(url: "https://github.com/swiftwasm/WasmKit.git", .upToNextMinor(from: "0.3.1")) ], targets: [ @@ -25,24 +23,25 @@ let package = Package( dependencies: [ "BluetoothExplorerUI", .product( - name: "SkipFuseUI", - package: "skip-fuse-ui" + name: "AndroidSwiftUI", + package: "AndroidSwiftUI", + condition: .when(platforms: [.android]) ) ], - resources: [.process("Resources")], - plugins: [.plugin(name: "skipstone", package: "skip")] + resources: [.process("Resources")] ), .target( name: "BluetoothExplorerUI", dependencies: [ "BluetoothExplorerModel", "BluetoothExplorerPluginEngine", - .product(name: "SkipModel", package: "skip-model"), - .product(name: "SkipFuse", package: "skip-fuse"), - .product(name: "SkipFuseUI", package: "skip-fuse-ui") + .product( + name: "AndroidSwiftUI", + package: "AndroidSwiftUI", + condition: .when(platforms: [.android]) + ) ], - resources: [.process("Resources")], - plugins: [.plugin(name: "skipstone", package: "skip")] + resources: [.process("Resources")] ), .target( name: "BluetoothExplorerPluginEngine", @@ -71,16 +70,12 @@ let package = Package( condition: .when(platforms: [.android]) ), .product( - name: "SkipFuse", - package: "skip-fuse" - ), - .product( - name: "SkipModel", - package: "skip-model" + name: "AndroidSwiftUI", + package: "AndroidSwiftUI", + condition: .when(platforms: [.android]) ) ], - resources: [.process("Resources")], - plugins: [.plugin(name: "skipstone", package: "skip")] + resources: [.process("Resources")] ), .testTarget( name: "BluetoothExplorerPluginEngineTests", diff --git a/Skip.env b/Skip.env deleted file mode 100644 index 600340f..0000000 --- a/Skip.env +++ /dev/null @@ -1,23 +0,0 @@ -// The configuration file for your Skip App (https://skip.tools). -// Properties specified here are shared between -// Darwin/BluetoothExplorer.xcconfig and Android/settings.gradle.kts -// and will be included in the app's metadata files -// Info.plist and AndroidManifest.xml - -// PRODUCT_NAME is the default title of the app, which must match the app's Swift module name -PRODUCT_NAME = BluetoothExplorer - -// PRODUCT_BUNDLE_IDENTIFIER is the unique id for both the iOS and Android app -PRODUCT_BUNDLE_IDENTIFIER = org.pureswift.bluetoothexplorer - -// The semantic version of the app -MARKETING_VERSION = 1.0.0 - -// The build number specifying the internal app version -CURRENT_PROJECT_VERSION = 1 - -// The package name for the Android entry point, referenced by the AndroidManifest.xml -ANDROID_PACKAGE_NAME = bluetooth.explorer - -// If your Android appId is different from the iOS Bundle Identifer, specify it here -// ANDROID_APPLICATION_ID = org.pureswift.bluetoothexplorer diff --git a/Sources/BluetoothExplorer/BluetoothExplorerApp.swift b/Sources/BluetoothExplorer/BluetoothExplorerApp.swift index 2d4a1b4..d0589bc 100644 --- a/Sources/BluetoothExplorer/BluetoothExplorerApp.swift +++ b/Sources/BluetoothExplorer/BluetoothExplorerApp.swift @@ -1,18 +1,21 @@ import Foundation -import SkipFuse +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import BluetoothExplorerModel import BluetoothExplorerUI /// A logger for the BluetoothExplorer module. -let logger: Logger = Logger(subsystem: "org.pureswift.bluetoothexplorer", category: "BluetoothExplorer") +let logger = AppLogger(subsystem: "org.pureswift.bluetoothexplorer", category: "BluetoothExplorer") /// The shared top-level view for the app, loaded from the platform-specific App delegates below. /// /// The default implementation merely loads the `ContentView` for the app and logs a message. -/* SKIP @bridge */public struct BluetoothExplorerRootView : View { - /* SKIP @bridge */public init() { +public struct BluetoothExplorerRootView : View { + public init() { } @State @@ -32,7 +35,7 @@ let logger: Logger = Logger(subsystem: "org.pureswift.bluetoothexplorer", catego } } .task { - logger.info("Skip app logs are viewable in the Xcode console for iOS; Android logs can be viewed in Studio or using adb logcat") + logger.info("BluetoothExplorer started") } } } @@ -40,37 +43,37 @@ let logger: Logger = Logger(subsystem: "org.pureswift.bluetoothexplorer", catego /// Global application delegate functions. /// /// These functions can update a shared observable object to communicate app state changes to interested views. -/* SKIP @bridge */public final class BluetoothExplorerAppDelegate : Sendable { - /* SKIP @bridge */public static let shared = BluetoothExplorerAppDelegate() +public final class BluetoothExplorerAppDelegate : Sendable { + public static let shared = BluetoothExplorerAppDelegate() private init() { } - /* SKIP @bridge */public func onInit() { + public func onInit() { logger.debug("onInit") } - /* SKIP @bridge */public func onLaunch() { + public func onLaunch() { logger.debug("onLaunch") } - /* SKIP @bridge */public func onResume() { + public func onResume() { logger.debug("onResume") } - /* SKIP @bridge */public func onPause() { + public func onPause() { logger.debug("onPause") } - /* SKIP @bridge */public func onStop() { + public func onStop() { logger.debug("onStop") } - /* SKIP @bridge */public func onDestroy() { + public func onDestroy() { logger.debug("onDestroy") } - /* SKIP @bridge */public func onLowMemory() { + public func onLowMemory() { logger.debug("onLowMemory") } } diff --git a/Sources/BluetoothExplorer/ContentView.swift b/Sources/BluetoothExplorer/ContentView.swift index 63bdec1..0291b2d 100644 --- a/Sources/BluetoothExplorer/ContentView.swift +++ b/Sources/BluetoothExplorer/ContentView.swift @@ -1,13 +1,16 @@ +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import BluetoothExplorerUI enum ContentTab: String, Hashable { - case welcome, home, plugins, settings + case devices, plugins, settings } struct ContentView: View { - @AppStorage("tab") var tab = ContentTab.welcome - @AppStorage("name") var welcomeName = "Skipper" + @AppStorage("tab") var tab = ContentTab.devices @AppStorage("appearance") var appearance = "" var body: some View { @@ -15,8 +18,8 @@ struct ContentView: View { NavigationStack { CentralList() } - .tabItem { Label("Welcome", systemImage: "heart.fill") } - .tag(ContentTab.welcome) + .tabItem { Label("Devices", systemImage: "dot.radiowaves.left.and.right") } + .tag(ContentTab.devices) NavigationStack { PluginsView() @@ -25,7 +28,7 @@ struct ContentView: View { .tag(ContentTab.plugins) NavigationStack { - SettingsView(appearance: $appearance, welcomeName: $welcomeName) + SettingsView(appearance: $appearance) .navigationTitle("Settings") } .tabItem { Label("Settings", systemImage: "gearshape.fill") } @@ -35,31 +38,11 @@ struct ContentView: View { } } -struct WelcomeView : View { - @State var heartBeating = false - @Binding var welcomeName: String - - var body: some View { - VStack(spacing: 0) { - Text("Hello [\(welcomeName)](https://skip.dev)!") - .padding() - Image(systemName: "heart.fill") - .foregroundStyle(.red) - .scaleEffect(heartBeating ? 1.5 : 1.0) - .animation(.easeInOut(duration: 1).repeatForever(), value: heartBeating) - .task { heartBeating = true } - } - .font(.largeTitle) - } -} - -struct SettingsView : View { +struct SettingsView: View { @Binding var appearance: String - @Binding var welcomeName: String var body: some View { Form { - TextField("Name", text: $welcomeName) Picker("Appearance", selection: $appearance) { Text("System").tag("") Text("Light").tag("light") @@ -69,32 +52,6 @@ struct SettingsView : View { let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { Text("Version \(version) (\(buildNumber))") } - HStack { - PlatformHeartView() - Text("Powered by [Skip](https://skip.tools)") - } } } } - -/// A view that shows a blue heart on iOS and a green heart on Android. -struct PlatformHeartView : View { - var body: some View { - #if os(Android) - ComposeView { - HeartComposer() - } - #else - Text(verbatim: "💙") - #endif - } -} - -#if SKIP -/// Use a ContentComposer to integrate Compose content. This code will be transpiled to Kotlin. -struct HeartComposer : ContentComposer { - @Composable func Compose(context: ComposeContext) { - androidx.compose.material3.Text("💚", modifier: context.modifier) - } -} -#endif diff --git a/Sources/BluetoothExplorer/Logging.swift b/Sources/BluetoothExplorer/Logging.swift new file mode 100644 index 0000000..90ebc80 --- /dev/null +++ b/Sources/BluetoothExplorer/Logging.swift @@ -0,0 +1,36 @@ +// +// Logging.swift +// BluetoothExplorer +// +// A minimal logger so the app does not depend on a cross-platform logging framework. +// Apple platforms use `os.Logger`; Android falls back to printing, which AndroidSwiftUI's +// runtime surfaces through logcat. +// + +#if canImport(os) +import os + +typealias AppLogger = os.Logger +#else +/// Stand-in for `os.Logger` on platforms without the unified logging system. +struct AppLogger { + + let subsystem: String + let category: String + + init(subsystem: String, category: String) { + self.subsystem = subsystem + self.category = category + } + + func debug(_ message: String) { emit("DEBUG", message) } + func info(_ message: String) { emit("INFO", message) } + func notice(_ message: String) { emit("NOTICE", message) } + func warning(_ message: String) { emit("WARNING", message) } + func error(_ message: String) { emit("ERROR", message) } + + private func emit(_ level: String, _ message: String) { + print("[\(level)] \(subsystem)/\(category): \(message)") + } +} +#endif diff --git a/Sources/BluetoothExplorer/Skip/skip.yml b/Sources/BluetoothExplorer/Skip/skip.yml deleted file mode 100644 index fdfcd15..0000000 --- a/Sources/BluetoothExplorer/Skip/skip.yml +++ /dev/null @@ -1,12 +0,0 @@ -# Configuration file for https://skip.tools project -# -# Kotlin dependencies and Gradle build options for this module can be configured here -#build: -# contents: -# - block: 'dependencies' -# contents: -# - 'implementation("androidx.compose.runtime:runtime")' - -# this is a natively-compiled Skip Fuse module -skip: - mode: 'native' diff --git a/Sources/BluetoothExplorerModel/Model/PluginManager.swift b/Sources/BluetoothExplorerModel/Model/PluginManager.swift index 59230dc..8aab2c2 100644 --- a/Sources/BluetoothExplorerModel/Model/PluginManager.swift +++ b/Sources/BluetoothExplorerModel/Model/PluginManager.swift @@ -8,7 +8,6 @@ import Foundation import Observation -import SkipFuse import BluetoothExplorerPluginEngine @MainActor diff --git a/Sources/BluetoothExplorerModel/Model/Store.swift b/Sources/BluetoothExplorerModel/Model/Store.swift index 0adeb13..5bb3fe7 100644 --- a/Sources/BluetoothExplorerModel/Model/Store.swift +++ b/Sources/BluetoothExplorerModel/Model/Store.swift @@ -8,8 +8,6 @@ import Foundation import Observation -import SkipFuse -import SkipModel import Bluetooth import GATT import BluetoothExplorerPluginEngine diff --git a/Sources/BluetoothExplorerModel/Skip/skip.yml b/Sources/BluetoothExplorerModel/Skip/skip.yml deleted file mode 100644 index fdfcd15..0000000 --- a/Sources/BluetoothExplorerModel/Skip/skip.yml +++ /dev/null @@ -1,12 +0,0 @@ -# Configuration file for https://skip.tools project -# -# Kotlin dependencies and Gradle build options for this module can be configured here -#build: -# contents: -# - block: 'dependencies' -# contents: -# - 'implementation("androidx.compose.runtime:runtime")' - -# this is a natively-compiled Skip Fuse module -skip: - mode: 'native' diff --git a/Sources/BluetoothExplorerModel/ViewModel/CentralListViewModel.swift b/Sources/BluetoothExplorerModel/ViewModel/CentralListViewModel.swift index 312b802..ec6e7e2 100644 --- a/Sources/BluetoothExplorerModel/ViewModel/CentralListViewModel.swift +++ b/Sources/BluetoothExplorerModel/ViewModel/CentralListViewModel.swift @@ -9,9 +9,6 @@ import Foundation import Observation import Bluetooth import GATT -import SkipFuse -import SkipUI -import SkipModel @MainActor @Observable diff --git a/Sources/BluetoothExplorerModel/ViewModel/PeripheralViewModel.swift b/Sources/BluetoothExplorerModel/ViewModel/PeripheralViewModel.swift index 55aab43..8a2bd36 100644 --- a/Sources/BluetoothExplorerModel/ViewModel/PeripheralViewModel.swift +++ b/Sources/BluetoothExplorerModel/ViewModel/PeripheralViewModel.swift @@ -9,8 +9,6 @@ import Foundation import Observation import Bluetooth import GATT -import SkipFuse -import SkipModel @MainActor @Observable diff --git a/Sources/BluetoothExplorerPluginEngine/Wasm/WasmParserPlugin.swift b/Sources/BluetoothExplorerPluginEngine/Wasm/WasmParserPlugin.swift index 7b0ccf3..ff6723f 100644 --- a/Sources/BluetoothExplorerPluginEngine/Wasm/WasmParserPlugin.swift +++ b/Sources/BluetoothExplorerPluginEngine/Wasm/WasmParserPlugin.swift @@ -23,7 +23,7 @@ public struct WasmParserPlugin: ParserPlugin { public init( manifest: PluginManifest, moduleBytes: [UInt8], - deadline: Duration = .milliseconds(50), + deadline: Duration = .milliseconds(250), warmupDeadline: Duration = .seconds(5) ) throws { try manifest.validate() diff --git a/Sources/BluetoothExplorerPluginEngine/Wasm/WasmPluginRunner.swift b/Sources/BluetoothExplorerPluginEngine/Wasm/WasmPluginRunner.swift index a3ad6fa..139536f 100644 --- a/Sources/BluetoothExplorerPluginEngine/Wasm/WasmPluginRunner.swift +++ b/Sources/BluetoothExplorerPluginEngine/Wasm/WasmPluginRunner.swift @@ -66,7 +66,7 @@ final class WasmPluginRunner: @unchecked Sendable { init( manifest: PluginManifest, moduleBytes: [UInt8], - deadline: Duration = .milliseconds(50), + deadline: Duration = .milliseconds(250), warmupDeadline: Duration = .seconds(5) ) { self.manifest = manifest diff --git a/Sources/BluetoothExplorerUI/AsyncButton.swift b/Sources/BluetoothExplorerUI/AsyncButton.swift index d8d2943..62f5f2f 100644 --- a/Sources/BluetoothExplorerUI/AsyncButton.swift +++ b/Sources/BluetoothExplorerUI/AsyncButton.swift @@ -5,7 +5,11 @@ // Created by Alsey Coleman Miller on 23/12/21. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif enum ActionOption: CaseIterable { case disableButton diff --git a/Sources/BluetoothExplorerUI/AttributeCell.swift b/Sources/BluetoothExplorerUI/AttributeCell.swift index efbb3fb..ae20305 100644 --- a/Sources/BluetoothExplorerUI/AttributeCell.swift +++ b/Sources/BluetoothExplorerUI/AttributeCell.swift @@ -5,7 +5,11 @@ // Created by Alsey Coleman Miller on 19/12/21. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import Bluetooth import GATT diff --git a/Sources/BluetoothExplorerUI/AttributeValueCell.swift b/Sources/BluetoothExplorerUI/AttributeValueCell.swift index ccae711..ce3f299 100644 --- a/Sources/BluetoothExplorerUI/AttributeValueCell.swift +++ b/Sources/BluetoothExplorerUI/AttributeValueCell.swift @@ -7,7 +7,11 @@ import Foundation import Bluetooth +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import BluetoothExplorerModel import BluetoothExplorerPluginEngine diff --git a/Sources/BluetoothExplorerUI/AttributeValuesSection.swift b/Sources/BluetoothExplorerUI/AttributeValuesSection.swift index f57ab90..14bd7b3 100644 --- a/Sources/BluetoothExplorerUI/AttributeValuesSection.swift +++ b/Sources/BluetoothExplorerUI/AttributeValuesSection.swift @@ -7,7 +7,11 @@ import Foundation import Bluetooth +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import BluetoothExplorerModel import BluetoothExplorerPluginEngine diff --git a/Sources/BluetoothExplorerUI/CentralCell.swift b/Sources/BluetoothExplorerUI/CentralCell.swift index 74a5120..7351bdc 100644 --- a/Sources/BluetoothExplorerUI/CentralCell.swift +++ b/Sources/BluetoothExplorerUI/CentralCell.swift @@ -5,7 +5,11 @@ // Created by Alsey Coleman Miller on 18/12/21. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import BluetoothExplorerModel public struct CentralCell: View { diff --git a/Sources/BluetoothExplorerUI/CentralList.swift b/Sources/BluetoothExplorerUI/CentralList.swift index 4dd520b..dad5a21 100644 --- a/Sources/BluetoothExplorerUI/CentralList.swift +++ b/Sources/BluetoothExplorerUI/CentralList.swift @@ -6,7 +6,11 @@ // Copyright © 2019 Alsey Coleman Miller. All rights reserved. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import BluetoothExplorerModel public struct CentralList: View { diff --git a/Sources/BluetoothExplorerUI/CharacteristicView.swift b/Sources/BluetoothExplorerUI/CharacteristicView.swift index 7b9fd86..1d61515 100644 --- a/Sources/BluetoothExplorerUI/CharacteristicView.swift +++ b/Sources/BluetoothExplorerUI/CharacteristicView.swift @@ -5,7 +5,11 @@ // Created by Alsey Coleman Miller on 22/12/21. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import Bluetooth import GATT import BluetoothExplorerModel diff --git a/Sources/BluetoothExplorerUI/DecodedFieldsView.swift b/Sources/BluetoothExplorerUI/DecodedFieldsView.swift index 8e36877..35c32bd 100644 --- a/Sources/BluetoothExplorerUI/DecodedFieldsView.swift +++ b/Sources/BluetoothExplorerUI/DecodedFieldsView.swift @@ -6,7 +6,11 @@ // subset (no ByteCountFormatter; Text(verbatim:) throughout). // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import BluetoothExplorerModel import BluetoothExplorerPluginEngine diff --git a/Sources/BluetoothExplorerUI/DescriptorView.swift b/Sources/BluetoothExplorerUI/DescriptorView.swift index 617335e..cd715d5 100644 --- a/Sources/BluetoothExplorerUI/DescriptorView.swift +++ b/Sources/BluetoothExplorerUI/DescriptorView.swift @@ -5,7 +5,11 @@ // Created by Alsey Coleman Miller on 23/12/21. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import Bluetooth import GATT import BluetoothExplorerModel diff --git a/Sources/BluetoothExplorerUI/PeripheralView.swift b/Sources/BluetoothExplorerUI/PeripheralView.swift index da55c86..b96abde 100644 --- a/Sources/BluetoothExplorerUI/PeripheralView.swift +++ b/Sources/BluetoothExplorerUI/PeripheralView.swift @@ -6,7 +6,11 @@ // Copyright © 2021 Alsey Coleman Miller. All rights reserved. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import BluetoothExplorerModel import BluetoothExplorerPluginEngine diff --git a/Sources/BluetoothExplorerUI/PluginsView.swift b/Sources/BluetoothExplorerUI/PluginsView.swift index 4fd80a6..3aca61e 100644 --- a/Sources/BluetoothExplorerUI/PluginsView.swift +++ b/Sources/BluetoothExplorerUI/PluginsView.swift @@ -7,7 +7,11 @@ // listed here lives in one place the user can inspect. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif #if canImport(UniformTypeIdentifiers) import UniformTypeIdentifiers #endif diff --git a/Sources/BluetoothExplorerUI/ServiceView.swift b/Sources/BluetoothExplorerUI/ServiceView.swift index 03c7719..49e87aa 100644 --- a/Sources/BluetoothExplorerUI/ServiceView.swift +++ b/Sources/BluetoothExplorerUI/ServiceView.swift @@ -5,7 +5,11 @@ // Created by Alsey Coleman Miller on 22/12/21. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import Bluetooth import GATT import BluetoothExplorerModel diff --git a/Sources/BluetoothExplorerUI/Skip/skip.yml b/Sources/BluetoothExplorerUI/Skip/skip.yml deleted file mode 100644 index fdfcd15..0000000 --- a/Sources/BluetoothExplorerUI/Skip/skip.yml +++ /dev/null @@ -1,12 +0,0 @@ -# Configuration file for https://skip.tools project -# -# Kotlin dependencies and Gradle build options for this module can be configured here -#build: -# contents: -# - block: 'dependencies' -# contents: -# - 'implementation("androidx.compose.runtime:runtime")' - -# this is a natively-compiled Skip Fuse module -skip: - mode: 'native' diff --git a/Sources/BluetoothExplorerUI/WriteAttributeView.swift b/Sources/BluetoothExplorerUI/WriteAttributeView.swift index 2a4cb7c..8ee313d 100644 --- a/Sources/BluetoothExplorerUI/WriteAttributeView.swift +++ b/Sources/BluetoothExplorerUI/WriteAttributeView.swift @@ -6,7 +6,11 @@ // import Foundation +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import Bluetooth import BluetoothExplorerModel diff --git a/Tests/BluetoothExplorerPluginEngineTests/GATTPluginTests.swift b/Tests/BluetoothExplorerPluginEngineTests/GATTPluginTests.swift index 2b25bcc..2c1debb 100644 --- a/Tests/BluetoothExplorerPluginEngineTests/GATTPluginTests.swift +++ b/Tests/BluetoothExplorerPluginEngineTests/GATTPluginTests.swift @@ -96,7 +96,11 @@ private let knownDivergences: [UInt16: String] = [:] // MARK: - Tests -@Suite("GATT characteristic plugins") +// Serialized: each test loads all 13 bundled plugins, and every plugin holds its own warm +// interpreter instance on its own thread. Running these tests concurrently multiplies that into +// dozens of contending interpreters, which is a property of the test harness rather than of the +// engine, and makes per-call latency wildly variable. +@Suite("GATT characteristic plugins", .serialized) struct GATTPluginTests { private func loadAll() throws -> [LoadedPlugin] {