From 87a38fcd76e065c3fbfe6d7c8cc3be6db520526a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguye=CC=82=CC=83n=20Tua=CC=82=CC=81n=20Vie=CC=A3=CC=82t?= Date: Fri, 17 Jul 2026 10:11:08 +0700 Subject: [PATCH 1/2] =?UTF-8?q?fix(ios/spm):=20hyphenated=20library=20prod?= =?UTF-8?q?uct=20+=20explicit=20UIKit=20imports=20=E2=80=94=20release=201.?= =?UTF-8?q?4.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #52: Flutter's generated FlutterGeneratedPluginSwiftPackage references plugins by the HYPHENATED product name (plugin.name.replaceAll('_', '-') in flutter_tools swift_package_manager.dart — SwiftPM uses the product name as CFBundleIdentifier for dynamic linking, which forbids underscores). Our Package.swift exported the product as native_workmanager, so every SPM-enabled app failed resolution: "product 'native-workmanager' … not found in package 'native_workmanager'". Product renamed to native-workmanager; package and target names keep underscores. Verified end-to-end with a scratch Flutter app (SwiftPM enabled, plugin as a path dependency): reproduced the exact resolution error on 1.4.1, and the fix builds through to '✓ Built Runner.app'. That E2E run also surfaced two workers that never compiled under SPM — CryptoWorker and FileSystemWorker use UIApplication without an explicit 'import UIKit' (CocoaPods compiled them via transitive module re-export; SPM's module isolation does not). Imports added. The scratch-app SPM build is now a documented release step in CONTRIBUTING — both #49 and #52 escaped because nothing consumed the package the way Flutter actually does. Versions: 1.4.2 (pubspec, gen lockstep, podspec, doc snippets). --- CHANGELOG.md | 32 +++++++++++++++++++ CONTRIBUTING.md | 10 ++++++ README.md | 2 +- doc/ANDROID_SETUP.md | 2 +- doc/GETTING_STARTED.md | 2 +- doc/MIGRATION_GUIDE.md | 4 +-- doc/MIGRATION_TOOL_README.md | 2 +- example/pubspec.lock | 2 +- ios/native_workmanager.podspec | 2 +- ios/native_workmanager/Package.swift | 9 +++++- .../workers/CryptoWorker.swift | 4 +++ .../workers/FileSystemWorker.swift | 4 +++ native_workmanager_gen/CHANGELOG.md | 8 +++++ native_workmanager_gen/pubspec.yaml | 2 +- pubspec.yaml | 2 +- 15 files changed, 76 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bd9048..f3bdba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [1.4.2] - 2026-07-17 + +### Fixed + +- **iOS SwiftPM resolution failed one layer above the 1.4.1 fix — Issue #52.** + Flutter's generated `FlutterGeneratedPluginSwiftPackage` references every + plugin by the **hyphenated** library product name + (`plugin.name.replaceAll('_', '-')` in `flutter_tools` — SwiftPM uses the + product name as `CFBundleIdentifier` when linking dynamically, and bundle + identifiers cannot contain underscores). `Package.swift` exported the product + as `native_workmanager`, so SPM-enabled apps failed dependency resolution with + *"product 'native-workmanager' … not found in package 'native_workmanager'"*. + The library product is now `native-workmanager`; the package and target names + keep their underscores. Thanks @zaqwery for the precise diagnosis — again. + +- **iOS: two workers did not compile under SwiftPM.** `CryptoWorker` and + `FileSystemWorker` use `UIApplication` (background-task API) without an + explicit `import UIKit`; CocoaPods builds compiled anyway via transitive + module re-export, SwiftPM builds do not. Surfaced by the new end-to-end + verification below; explicit imports added. + +### Changed + +- **Release verification now includes a real SwiftPM app build.** Both #49 and + #52 escaped because the plugin package was only ever built in isolation or + consumed via CocoaPods. The SPM check now builds a scratch Flutter app with + `--enable-swift-package-manager` depending on the plugin, which exercises + Flutter's generated manifest (hyphenated product reference, platform minimums, + full plugin compile under SPM). + +--- + ## [1.4.1] - 2026-07-16 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c1e7b10..133b8f6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -410,6 +410,16 @@ number, even when a release has no codegen changes. 4. `flutter analyze` (0 issues) and the full test suite (`./scripts/run_all_tests.sh`, plus `flutter build ios --simulator` and an Android APK build as a smoke test) must be clean before tagging. + **Also build a scratch Flutter app with SwiftPM enabled** — both #49 and #52 escaped + because the plugin was only built in isolation or via CocoaPods: + `flutter create /tmp/spmtest && cd /tmp/spmtest` → add this plugin as a `path:` + dependency → raise the app's `IPHONEOS_DEPLOYMENT_TARGET` to 14.0 → + `flutter config --enable-swift-package-manager && flutter build ios --simulator --debug` + (restore with `flutter config --no-enable-swift-package-manager` after). This exercises + Flutter's generated SwiftPM manifest, which references the plugin by the **hyphenated** + library product name (`native-workmanager`) — that product name in `Package.swift` must + never be renamed back to underscores, and iOS sources must compile under SPM's stricter + module isolation (no transitive `import UIKit`). 5. Commit, tag `vX.Y.Z`, push. 6. Cut a GitHub release on the tag. If the iOS xcframework changed, attach `KMPWorkManager.xcframework.zip` as a release asset **named exactly that** — GitHub's diff --git a/README.md b/README.md index b9aaea8..aca2b8c 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ No boilerplate. No native code to write. No `AndroidManifest.xml` changes. Each ```yaml dependencies: - native_workmanager: ^1.4.1 + native_workmanager: ^1.4.2 ``` **2. Initialize once in `main()`:** diff --git a/doc/ANDROID_SETUP.md b/doc/ANDROID_SETUP.md index ddc818e..26dc8cc 100644 --- a/doc/ANDROID_SETUP.md +++ b/doc/ANDROID_SETUP.md @@ -81,7 +81,7 @@ Add to your `pubspec.yaml`: ```yaml dependencies: - native_workmanager: ^1.4.1 + native_workmanager: ^1.4.2 ``` Run: diff --git a/doc/GETTING_STARTED.md b/doc/GETTING_STARTED.md index a7aaf39..899f5c8 100644 --- a/doc/GETTING_STARTED.md +++ b/doc/GETTING_STARTED.md @@ -34,7 +34,7 @@ Or manually: ```yaml dependencies: - native_workmanager: ^1.4.1 + native_workmanager: ^1.4.2 ``` Then run: diff --git a/doc/MIGRATION_GUIDE.md b/doc/MIGRATION_GUIDE.md index 00ccd73..5b292d8 100644 --- a/doc/MIGRATION_GUIDE.md +++ b/doc/MIGRATION_GUIDE.md @@ -152,7 +152,7 @@ dependencies: **After:** ```yaml dependencies: - native_workmanager: ^1.4.1 + native_workmanager: ^1.4.2 ``` **Then run:** @@ -865,7 +865,7 @@ Use this checklist to track your migration progress: ```yaml dependencies: workmanager: ^0.5.0 - native_workmanager: ^1.4.1 + native_workmanager: ^1.4.2 ``` Migrate tasks one at a time, then remove workmanager when done. diff --git a/doc/MIGRATION_TOOL_README.md b/doc/MIGRATION_TOOL_README.md index dd8b663..b6aca01 100644 --- a/doc/MIGRATION_TOOL_README.md +++ b/doc/MIGRATION_TOOL_README.md @@ -142,7 +142,7 @@ Updated dependencies file: dependencies: flutter: sdk: flutter - native_workmanager: ^1.4.1 # Replaced workmanager + native_workmanager: ^1.4.2 # Replaced workmanager ``` **Usage:** diff --git a/example/pubspec.lock b/example/pubspec.lock index 74c71ca..f005ced 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -261,7 +261,7 @@ packages: path: ".." relative: true source: path - version: "1.4.1" + version: "1.4.2" objective_c: dependency: transitive description: diff --git a/ios/native_workmanager.podspec b/ios/native_workmanager.podspec index b5675b5..8cf5bb9 100644 --- a/ios/native_workmanager.podspec +++ b/ios/native_workmanager.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'native_workmanager' - s.version = '1.4.1' + s.version = '1.4.2' s.summary = 'Background task manager for Flutter using platform-native APIs.' s.description = <<-DESC Native WorkManager is a Flutter plugin that provides native background task scheduling diff --git a/ios/native_workmanager/Package.swift b/ios/native_workmanager/Package.swift index e68dcd6..3b6587a 100644 --- a/ios/native_workmanager/Package.swift +++ b/ios/native_workmanager/Package.swift @@ -10,7 +10,14 @@ let package = Package( .iOS("14.0"), ], products: [ - .library(name: "native_workmanager", targets: ["native_workmanager"]), + // The library PRODUCT must be the hyphenated plugin name (issue #52). + // Flutter's generated FlutterGeneratedPluginSwiftPackage references + // plugins as .product(name: plugin.name.replaceAll('_', '-'), + // package: plugin.name) — see flutter_tools swift_package_manager.dart. + // SPM uses the product name as CFBundleIdentifier when linked + // dynamically, and CFBundleIdentifier cannot contain underscores. + // Package name and target names keep their underscores. + .library(name: "native-workmanager", targets: ["native_workmanager"]), ], dependencies: [ // No third-party dependencies. Uses Apple Archive for ZIP operations. diff --git a/ios/native_workmanager/Sources/native_workmanager/workers/CryptoWorker.swift b/ios/native_workmanager/Sources/native_workmanager/workers/CryptoWorker.swift index 5aebe0f..8e840ba 100644 --- a/ios/native_workmanager/Sources/native_workmanager/workers/CryptoWorker.swift +++ b/ios/native_workmanager/Sources/native_workmanager/workers/CryptoWorker.swift @@ -2,6 +2,10 @@ import Foundation import KMPWorkManager import CryptoKit import CommonCrypto +// Explicit UIKit import: required for UIApplication (background task API). +// CocoaPods builds compiled without it because another module re-exported +// UIKit; SwiftPM builds do not (issue #52 E2E verification caught this). +import UIKit /// Native cryptographic operations worker for iOS. /// diff --git a/ios/native_workmanager/Sources/native_workmanager/workers/FileSystemWorker.swift b/ios/native_workmanager/Sources/native_workmanager/workers/FileSystemWorker.swift index 51c297b..a7a9e7e 100644 --- a/ios/native_workmanager/Sources/native_workmanager/workers/FileSystemWorker.swift +++ b/ios/native_workmanager/Sources/native_workmanager/workers/FileSystemWorker.swift @@ -1,5 +1,9 @@ import Foundation import KMPWorkManager +// Explicit UIKit import: required for UIApplication (background task API). +// CocoaPods builds compiled without it because another module re-exported +// UIKit; SwiftPM builds do not (issue #52 E2E verification caught this). +import UIKit /// Built-in worker: File system operations /// diff --git a/native_workmanager_gen/CHANGELOG.md b/native_workmanager_gen/CHANGELOG.md index 32d3894..165a9a2 100644 --- a/native_workmanager_gen/CHANGELOG.md +++ b/native_workmanager_gen/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). --- +## [1.4.2] - 2026-07-17 + +- Version bump synchronized with `native_workmanager` 1.4.2. No codegen changes — the + 1.4.2 fixes (iOS SwiftPM hyphenated library product #52, explicit UIKit imports) + are entirely in the main package's iOS build configuration and Swift sources. + +--- + ## [1.4.1] - 2026-07-16 - Version bump synchronized with `native_workmanager` 1.4.1. No codegen changes — the diff --git a/native_workmanager_gen/pubspec.yaml b/native_workmanager_gen/pubspec.yaml index ddffedf..de1f886 100644 --- a/native_workmanager_gen/pubspec.yaml +++ b/native_workmanager_gen/pubspec.yaml @@ -1,5 +1,5 @@ name: native_workmanager_gen -version: 1.4.1 +version: 1.4.2 description: > Code generator for native_workmanager. Generates type-safe DartWorker callback IDs and worker registry from diff --git a/pubspec.yaml b/pubspec.yaml index 6d955ec..5be6ea3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: native_workmanager description: "Background task scheduling for Flutter — 25+ native workers (HTTP, image, crypto, file), task chains, zero Flutter Engine overhead." -version: 1.4.1 +version: 1.4.2 homepage: https://github.com/brewkits/native_workmanager repository: https://github.com/brewkits/native_workmanager issue_tracker: https://github.com/brewkits/native_workmanager/issues From a915fe5da4a7a31a06d2b70b626ba35420e42d29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguye=CC=82=CC=83n=20Tua=CC=82=CC=81n=20Vie=CC=A3=CC=82t?= Date: Fri, 17 Jul 2026 10:11:21 +0700 Subject: [PATCH 2/2] style: dart format (example integration test + gen test) Format-only rewrap from Dart 3.11.5; both files sit outside the CI format gate (which checks lib/ and test/ of the plugin and gen's lib/ only). --- .../device_integration_test.dart | 14 ++++++++++--- .../test/worker_callback_generator_test.dart | 20 +++++++++++-------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/example/integration_test/device_integration_test.dart b/example/integration_test/device_integration_test.dart index 6480e25..3f1eead 100644 --- a/example/integration_test/device_integration_test.dart +++ b/example/integration_test/device_integration_test.dart @@ -204,7 +204,9 @@ Future _ditRetryCounter(Map? input) async { final path = input?['counterFile'] as String?; if (path != null) { final f = File(path); - final n = f.existsSync() ? (int.tryParse(f.readAsStringSync().trim()) ?? 0) : 0; + final n = f.existsSync() + ? (int.tryParse(f.readAsStringSync().trim()) ?? 0) + : 0; f.writeAsStringSync('${n + 1}'); print('[DartWorker] dit_retry_counter run #${n + 1}'); } @@ -2960,7 +2962,10 @@ void main() { } }); - final eventFuture = _waitEvent(id, timeout: const Duration(seconds: 45)); + final eventFuture = _waitEvent( + id, + timeout: const Duration(seconds: 45), + ); await NativeWorkManager.enqueue( taskId: id, @@ -2996,7 +3001,10 @@ void main() { 'issue_39: DartWorker status becomes terminal in allTasks() after success', (tester) async { final id = _id('issue39_status'); - final eventFuture = _waitEvent(id, timeout: const Duration(seconds: 45)); + final eventFuture = _waitEvent( + id, + timeout: const Duration(seconds: 45), + ); await NativeWorkManager.enqueue( taskId: id, diff --git a/native_workmanager_gen/test/worker_callback_generator_test.dart b/native_workmanager_gen/test/worker_callback_generator_test.dart index 9bfa7a5..2128a11 100644 --- a/native_workmanager_gen/test/worker_callback_generator_test.dart +++ b/native_workmanager_gen/test/worker_callback_generator_test.dart @@ -43,10 +43,10 @@ class WorkerCallback { /// Builds the asset map for a `lib/workers.dart` source, plus the virtual /// `native_workmanager` annotation package every fixture imports. Map assets(String workersSource) => { - 'native_workmanager|lib/src/worker_callback_generator_annotation.dart': - _annotationSource, - 'a|lib/workers.dart': '$_import\n\n$workersSource', - }; + 'native_workmanager|lib/src/worker_callback_generator_annotation.dart': + _annotationSource, + 'a|lib/workers.dart': '$_import\n\n$workersSource', + }; const outputAsset = 'a|lib/workers.worker_callback.g.part'; @@ -201,7 +201,8 @@ Future syncContacts(Map? input) async => true; expect( logs, anyElement(contains(messageContains)), - reason: 'Expected a SEVERE log containing "$messageContains", ' + reason: + 'Expected a SEVERE log containing "$messageContains", ' 'got: $logs', ); } @@ -250,12 +251,15 @@ Future twoParams(Map? input, String extra) async => true; ''', messageContains: 'has 2 parameters'); }); - test('rejects a parameter type that is not Map?', () async { - await expectRejected(''' + test( + 'rejects a parameter type that is not Map?', + () async { + await expectRejected(''' @WorkerCallback('bad') Future wrongParamType(String? input) async => true; ''', messageContains: 'Map?'); - }); + }, + ); test('rejects an empty id', () async { await expectRejected('''