flutter-tvos v1.1.0 — the porter release#11
Merged
Conversation
…S / macOS plugins (phase 1)
Adds `flutter-tvos plugin port <source-dir>` — a one-shot scaffolding
command that takes any existing iOS or macOS Flutter plugin and produces
a sibling `*_tvos` package conforming to the FlutterTV plugin standards.
The source is never modified; the porter writes a brand-new federated
sibling that depends on the same platform interface.
This commit is phase 1 of the plan documented in `docs/PLUGIN_PORTING.md`.
Phases 2–7 layer on top of this skeleton:
* phase 2 — copy native source into `tvos/Classes/` verbatim
* phase 3 — compatibility database + Swift transformer that comments
out and stubs iOS-only API call sites, generates
PORTING_REPORT.md
* phase 4 — Objective-C transformer
* phase 5 — `--include-example` to add `tvos/` to the source plugin's
example app
* phase 6 — `--from-pub` and `--from-git`
* phase 7 — docs, tab completion, real-plugin validation
What works today
- `flutter-tvos plugin port <path>` reads the source's pubspec, locates
the plugin class via `flutter.plugin.platforms.<ios|macos>`,
detects Swift vs Objective-C from `<platform>/Classes/`, finds the
federated platform interface package, and writes a complete package
skeleton:
pubspec.yaml (with flutter.plugin.platforms.tvos wired up)
README.md
CHANGELOG.md
LICENSE (copied from source if present)
analysis_options.yaml
.gitignore
lib/<plugin>_tvos.dart
test/<plugin>_tvos_test.dart
tvos/Classes/<PluginClass>.swift (stub)
tvos/Classes/<PluginClass>-Bridging-Header.h
tvos/<plugin>_tvos.podspec
- Flags: `--output`, `--base-platform={ios,macos}`, `--license-holder`,
`--force` (overwrite), `--dry-run` (preview without writing).
- Subcommand structure: `flutter-tvos plugin` is the umbrella; `port`
is its first subcommand. Future `lint` / `publish` subcommands slot
in alongside.
- Sensible refusals: pure-Dart plugins (no `*_tvos` needed — they
federate via the platform interface), plugins already targeting tvOS,
plugins with neither iOS nor macOS implementations, plugins missing
a `pluginClass` declaration.
Conventions enforced by the templates
- Output package name strips `_ios`, `_macos`, `_foundation`, `_darwin`
so `path_provider_foundation` → `path_provider_tvos` (not
`path_provider_foundation_tvos`).
- Generated podspec deliberately omits `s.dependency 'Flutter'` — the
Flutter pod doesn't declare tvOS support, so depending on it breaks
`pod install` for tvOS consumers. Flutter.framework is resolved via
FRAMEWORK_SEARCH_PATHS, populated by the host app's Podfile (same
mechanism `templates/app/swift/tvos.tmpl/Podfile` uses).
- Generated Dart entry declares `base class <X> extends <Interface>Platform`
— Dart 3 requires the `base` modifier when extending a `base class`
from a platform interface. This is the single most common breakage
when hand-porting plugins; the scaffold gets it right by default.
Tests (all 93 passing — was 81, +12 new)
- SourceAnalyzer covers pubspec parsing, name derivation across the
four platform suffixes, Swift vs ObjC detection, ios→macos fallback
with prefer=ios, and all four refusal paths.
- Scaffolder covers the full file set (pubspec contains the right
pluginClass / dartPluginClass, podspec has no `s.dependency 'Flutter'`,
Dart entry imports the platform interface, etc.), `--dry-run`,
`--force` overwrite semantics, and LICENSE copying.
Smoke-tested by running the command on a hand-built `url_launcher_ios`
fixture: produces a valid scaffold, `dart analyze lib test` clean
across the package after generation, `dart test test/` runs the
package's seed test successfully.
Documentation
- `docs/PLUGIN_PORTING.md` is the canonical design and phase plan;
treat it as the spec, fix the doc if the code disagrees.
- `flutter-tvos/CLAUDE.md` gets a `## Plugin Porting` section pointing
at the doc and explaining the file layout.
Files
- new: lib/commands/plugin.dart (umbrella)
- new: lib/commands/plugin_port.dart (port subcommand)
- new: lib/plugin_porting/source_analyzer.dart (pubspec/source detection)
- new: lib/plugin_porting/scaffolder.dart (file orchestration)
- new: lib/plugin_porting/templates.dart (rendered file bodies)
- new: test/general/plugin_port_test.dart (analyzer + scaffolder tests)
- new: docs/PLUGIN_PORTING.md (design + plan)
- modified: lib/executable.dart (registers TvosPluginCommand)
… plugin Phase 1 always wrote a stub Swift class with a single `result(FlutterMethodNotImplemented)` body, leaving the user to paste their iOS implementation in by hand. Phase 2 walks the source plugin's `<platform>/Classes/` directory and copies every Swift / Objective-C source file (`.swift` / `.h` / `.m` / `.mm`) verbatim into the output's `tvos/Classes/`, preserving subdirectory structure. Net effect: porting `url_launcher_ios → url_launcher_tvos` produces a package that already contains the real iOS implementation. The user's remaining work shrinks from "rewrite everything from scratch" to "strip the imports/calls that are not available on tvOS." Phase 3 will do most of that stripping automatically via the compatibility database. Behaviour - Native files under `<platform>/Classes/` (recursively) are copied to `tvos/Classes/`. Subdirectory structure is preserved — a helper at `ios/Classes/Helpers/UrlValidator.swift` lands at `tvos/Classes/Helpers/UrlValidator.swift`. - `<platform>/Resources/` is copied to `tvos/Resources/` if it exists. Filtering is by extension for sources (`.swift`, `.h`, `.m`, `.mm`) but unfiltered for resources, since plugins can ship anything there (xib, asset catalogs, .strings, plist, PNGs). - When the source has no native files at all (rare — e.g. plugins where Pigeon generates everything at build time, or the source's `Classes/` is empty), the scaffolder falls back to the Phase-1 stub Swift class plus its bridging header. So `flutter-tvos plugin port` always produces a buildable package, regardless of whether the source has copyable native code. - The bridging-header stub is only written in fallback mode — when the real Swift source is copied, the user's existing source already has whatever ObjC interop it needs. No more confusing `URLLauncherPlugin-Bridging-Header.h` showing up next to a real Swift source. Tests added (now 16 in plugin_port_test.dart, was 12; 97/97 across the package, was 93) - `copies Objective-C sources verbatim` — both .h and .m round-trip unchanged; no Swift stub appears alongside. - `preserves subdirectory structure under Classes/` — nested helper files land at the right depth in the output. - `copies <platform>/Resources/ when present` — including nested asset catalogs (`Assets.xcassets/Contents.json`) and `.strings` files. - `falls back to Swift stub when source has no native files` — guarantees the buildable-package promise even when the source's Classes/ is empty. - The existing "writes a complete federated package skeleton" test was updated to assert the source content is copied verbatim, not the Phase-1 stub. Smoke-tested by running the new command on a hand-built `url_launcher_ios` fixture with a realistic `URLLauncherPlugin.swift` and a `Resources/Localizable.strings`. Both files landed in the correct locations in the generated `url_launcher_tvos` package; no bridging header was emitted. Files - modified: lib/plugin_porting/scaffolder.dart (new helpers `_collectNativeCopies`, `_collectResourceCopies`, `_collectByExtension`, `_resolveResourcesDir`; new `_NativeCopy` type; the Phase-1 stub plan entries gated behind the empty-source fallback) - modified: test/general/plugin_port_test.dart (4 new tests + realistic Swift/ObjC fixture content)
Phase 3 work-in-progress so plugin-port branch is shippable. Still TODO: - report_emitter.dart (PORTING_REPORT.md generator) - wire SwiftPorter through Scaffolder - per-pattern + transformer + e2e tests Saved as WIP so main branch can be shared without phase-3 churn.
Bring plugin-port up to date with main (1.0.4): web-compatible FFI, RCU init refactors, doc/version bumps. No conflicts; plugin_porting sources and docs/PLUGIN_PORTING.md are unaffected.
Completes the Phase 3 deliverable from docs/PLUGIN_PORTING.md: - SwiftPorter now strips iOS-only `import`s independently of the API usage regex (the WIP code only stripped an import if the API regex also matched the import line, so `import WebKit` was never removed). - New report_emitter.dart renders PORTING_REPORT.md (§7): summary table, per-method ported/partial/stubbed breakdown, removed imports, manual-review list, checklist. - Scaffolder runs .swift sources through SwiftPorter (instead of a verbatim copy), collects findings, and writes the report. .h/.m/.mm stay verbatim for the Phase 4 ObjC transformer. - plugin_port gains --no-report and prints a strip/stub/review summary plus the "manual review required" banner. - SourceAnalyzer exposes the source version; SwiftPortingResult exposes detectedMethods for the report's "ported as-is" count. - Tests: per-pattern compatibility-db coverage, SwiftPorter unit tests, and an end-to-end url_launcher_ios port asserting WebKit is stripped and the web handlers are stubbed and reported. Header-comment regeneration (§6.1.6) remains deferred; noted in the doc status snapshot.
- Extract shared FindingAction/PortingFinding/PortingResult into
porting_result.dart so Swift and ObjC porters share one contract
(SwiftPortingResult -> PortingResult).
- New ObjcPorter: strips iOS-only `#import <Framework/...>` and
`@import Framework;` lines (banned frameworks derived from the same
compatibility-database stripImports), and stubs unsupported method
handlers found by brace-tracking `isEqualToString:@"x"` dispatch
chains. Closure is detected at the character level so `} else if
(...) {` chains are split correctly.
- Scaffolder routes .swift through SwiftPorter and .h/.m/.mm through
ObjcPorter; both feed the porting report.
- Tests: ObjcPorter unit tests + an end-to-end ObjC url_launcher port
asserting WebKit imports stripped, the web handler stubbed, and the
report naming it. Full plugin-port suite (51 tests) green;
`dart analyze lib/` exits 0.
New ExampleExtender wires the SOURCE plugin's example/ app for tvOS without ever touching the generated *_tvos package: - validates example/ (lib/main.dart + Flutter pubspec); skips with a reason instead of failing the port when absent/odd, - merges `dependency_overrides` (source *_ios and new *_tvos as local path deps) into the existing block or a new one, idempotently and without clobbering a user-provided override, - appends a one-line "run on tvOS" note to example/README.md. Generating example/tvos/ stays delegated to the existing `flutter-tvos create` template subsystem (on-disk templates + signing, not reproducible in-memory); the exact create/build commands are surfaced to the user via the new --include-example flag. Tests: 6 ExampleExtender unit tests (merge, idempotency, existing block, no-clobber, skip, dry-run). Full plugin-port suite 57 green; `dart analyze lib/` exits 0.
- SourceSpec: pure, validated planner for the three source forms (positional path / --from-pub / --from-git [--ref]); enforces mutual exclusion and that --ref requires --from-git; derives the checkout name and builds shallow `git clone` argv. - SourceFetcher: resolves a spec to a directory. localPath returns immediately; git shallow-clones into a temp dir; pub resolves the package via a throwaway probe project + package_config.json. - plugin_port: new --from-pub/--from-git/--ref options; fetched sources materialise under a system temp dir that is always cleaned up (try/finally), default output moves to the cwd, and --include-example is refused for fetched sources (its edits would not persist). - Tests: SourceSpec validation matrix + SourceFetcher git/pub/local paths driven by FakeProcessManager. Full plugin-port suite 70 green; `dart analyze lib/` exits 0.
User-facing documentation for `flutter-tvos plugin port`: local / --from-pub / --from-git usage, what the transformer does (Swift + ObjC import stripping and handler stubbing), the PORTING_REPORT.md review step, and the useful flags. No code changes; full plugin-port suite (70) still green, `dart analyze lib/` exits 0. Deferred (low value / not reproducible here): interactive Continue? prompt (--force/--dry-run already gate writes), shell tab completion, and the internal "port 3 real plugins" validation.
Dogfooding the porter against real pub.dev plugins (url_launcher_ios,
shared_preferences_foundation, path_provider_foundation) exposed three
modern-convention gaps; all fixed here:
- Source resolution now searches `<platform>/Classes`,
`<platform>/<pkg>/Sources/<pkg>` (Swift Package Manager),
`darwin/<pkg>/Sources/<pkg>` (sharedDarwinSource), any other
`Sources/<dir>`, then the platform root — `darwin/` first when
`sharedDarwinSource: true`. Previously only `<platform>/Classes`
then `<platform>/` were tried, so SPM/darwin plugins silently fell
back to an empty stub.
- `pluginClass` is no longer mandatory in the pubspec: when omitted it
is inferred from the sources (the `class X: … FlutterPlugin` /
`@interface X : … <FlutterPlugin>` declaration), falling back to a
derived name.
- Plugins with no native code AND no pluginClass (pure-Dart or
dart:ffi/package:objective_c, e.g. modern path_provider_foundation)
now exit with an advisory message ("no *_tvos package is needed")
instead of a hard error — these already work on tvOS via Dart.
- Scaffolder filters `Package.swift`/`Package.resolved` so SPM
manifests never land in the podspec's `Classes/**` glob.
Tests: 5 new SourceAnalyzer cases (SPM, sharedDarwinSource, inferred
pluginClass, advisory FFI exit, Package.swift exclusion). Full
plugin-port suite 75 green; `dart analyze lib/` exits 0.
Two real-world correctness fixes found while prepping the plugin repo: - _stripPlatformSuffix now also strips the federated Apple-impl suffixes `_avfoundation`, `_storekit`, `_apple` (longest-first so `_avfoundation` isn't shortened by `_foundation`). Without this, `video_player_avfoundation` → `video_player_avfoundation_tvos` etc. - The generated pubspec now copies the source's actual `*_platform_interface` version constraint instead of a hardcoded `^1.0.0` (which made `pub get` fail for every plugin whose interface is past 1.x); falls back to `any` when the source didn't pin one. Tests: suffix matrix (6 cases) + constraint carry/fallback. Full plugin-port suite 77 green; analyze clean.
- `flutter-tvos create --tvos-only` removes the non-tvOS platform folders (android/ios/macos/linux/windows/web) after scaffolding, so a tvOS plugin's example app targets only Apple TV. Compose it with the standard `flutter create --platforms=ios .` for a minimal scaffold (upstream Flutter has no `tvos` platform, so `tvos/` is still added by this command and the residual `ios/` is stripped). - Generated pubspec now quotes the carried platform-interface constraint when it contains YAML-significant chars (`>`, `<`, space) — an unquoted `>=2.4.0 <3.0.0` was a YAML block scalar and broke `pub get` (hit by sqflite_darwin). Tests: range-constraint quoting. Full plugin-port suite 78 green; analyze clean.
It's our CLI, so `tvos` should be a real platform. Upstream Flutter's `--platforms` enforces an `allowed:` whitelist at parse time (no `tvos`), and we don't patch Flutter — so the rewrite happens at our own argv entrypoint, the one seam we own: - `--platforms=tvos` → `--platforms=ios --tvos-only` (Flutter scaffolds minimal ios, TvosCreateCommand adds tvos/, the residual ios/ is stripped → a clean tvOS-only project) - `--platforms=tvos,ios,…` → `--platforms=ios,…` (tvos dropped; tvos/ still added; siblings kept, nothing stripped) - no tvos / no create → argv returned byte-identical Logic is a pure helper (`expandTvosPlatformArgs`) in its own library so it unit-tests with no I/O. 8 cases (forms, merge, passthrough, order). Full analyze clean; suites green.
--platforms=tvos no longer scaffolds an iOS app and deletes it. The new TvosAppScaffold writes the shared Dart app (pubspec, lib/main.dart, test, analysis_options, .gitignore, README) and TvosCreateCommand renders tvos/ on top — upstream `flutter create` is never invoked for the tvOS-only case, so the project is tvOS-only by construction. - `--tvos-only` is now an internal, hidden flag (the argv shim's signal); users only ever type `--platforms=tvos`. - `_renderTvosRunner` extracted and shared by the standard and tvOS-only paths; the strip-platforms code is gone. Smoke-tested: `create --platforms=tvos .` yields exactly lib/ test/ tvos/ + pubspec — no ios/android/macos/etc. analyze lib exits 0; suites green.
The generated `lib/<pkg>_tvos.dart` was a hand-written guess: it
invented the platform-interface class name and left abstract members
unimplemented, so it never compiled (every regenerated package failed
`dart analyze`).
The source plugin's own `lib/` is already a correct federated
implementation that extends the platform interface and talks to the
SAME method channel the native side registers — and the porter keeps
that channel name unchanged. So copy it verbatim, only:
* renaming the conventional entry `lib/<src>.dart` → `lib/<out>.dart`
(so Flutter's federated registrant resolves `dartPluginClass`), and
* rewriting `package:<src>/…` self-imports to the output package.
Falls back to the templated stub when the source ships no Dart `lib/`.
Tests: copy+rename+rewrite, and the no-lib stub fallback. Full
plugin-port suite green; `dart analyze lib/` exits 0.
A federated `*_tvos` package is never used directly; the app imports
the app-facing plugin. The old generated example (placeholder app
depending only on `<pkg>_tvos: path: ../`) demonstrated nothing.
Rework, mirroring flutter-tizen/plugins:
- ExamplePorter reuses the app-facing plugin's real example app
(its lib/, assets, deps), drops non-tvOS platform folders, and
rewrites the pubspec to the federated dual-dependency form:
dependencies:
<base>: ^<resolved version>
<base>_tvos: { path: ../ }
Existing managed-key entries are replaced idempotently; other deps
and the assets section are preserved.
- renderTvosRunner extracted from TvosCreateCommand into
commands/tvos_runner.dart so the porter can add tvos/ to the copied
example without re-running create.
- plugin_port --include-example now fetches the app-facing plugin via
pub, runs ExamplePorter, and renders tvos/ (temp dir always cleaned).
- Superseded ExampleExtender + its test removed.
Tests: ExamplePorter copy/strip/dual-dep + idempotency + skip. Full
plugin-port suite 85 green; `dart analyze lib/` exits 0.
The generated test stub imported the package but never referenced it, so every clean federated port still failed `dart analyze` with one `unused_import` warning. Now it references the federated `dartPluginClass` (verifying the package compiles and the class is reachable); when there is no Dart plugin class it emits a dependency-free smoke test instead of an unused import. Generated federated packages now analyze with zero issues.
Real tvOS-simulator build of shared_preferences_tvos failed at
xcodebuild:
messages.g.swift:14: error: Unsupported platform.
Pigeon (and many plugins) gate the Flutter module with
`#if os(iOS) import Flutter #elseif os(macOS) import FlutterMacOS
#else #error("Unsupported platform.") #endif`. tvOS fell into the
`#else`. The tvOS embedder ships the same `Flutter` module as iOS, so
the Swift porter now rewrites that specific guard's `#if os(iOS)` to
`#if os(iOS) || os(tvOS)`. Narrow: only an `#if os(iOS)` whose first
directive is `import Flutter` is touched; behaviour `#if os(iOS)`
blocks are left alone.
Tests: guard widened; non-guard block untouched. Suite green; analyze
clean.
Real tvOS-simulator build advanced past the import guard and then failed on the messenger branch: upstream splits `#if os(iOS) registrar.messenger() #else registrar.messenger` (macOS). tvOS fell into the macOS branch, but the tvOS embedder mirrors the iOS Flutter API, so `messenger` must be `messenger()`. Generalised the earlier narrow import-guard rule: in every `#if` / `#elseif` directive, widen each `os(iOS)` to `(os(iOS) || os(tvOS))` (parenthesised so `&&`/`!` precedence is preserved). tvOS now takes the iOS code paths everywhere; iOS-only APIs inside are still stubbed by the compatibility-database passes. Tests updated: import guard + behaviour/messenger branches widened, compound `&&` precedence kept, non-iOS directives untouched. Suite green; analyze clean.
Some plugins use APIs that simply do not exist on tvOS at type /
top-level scope (e.g. url_launcher: SFSafariViewController /
SFSafariViewControllerDelegate; WebKit). The porter can stub a
method-channel handler body, but it cannot invent a type the class
declaration depends on, so those packages fail only at Xcode time.
Now detected from the existing signal: a `taggedWithTodo` finding =
an `unsupported` API used outside a stubbable handler. When present:
- plugin port prints a loud warning ("NOT buildable on tvOS as-is —
uses <APIs> at type level …"), in both real and --dry-run output;
- PORTING_REPORT.md gets a top banner ("⚠️ This plugin is not
buildable on tvOS as-is") and the summary row becomes
"tvOS build outlook | ❌ will NOT compile" vs "✅ expected to
compile" (replacing the always-0 "Compile errors expected" row).
So a capability that's impossible on tvOS is reported clearly before
build, instead of surfacing as a cryptic xcodebuild error. Tests:
type-level unsupported → NOT-buildable banner; handler-only → expected
to compile. Suite green; analyze clean.
Empirically tested path_provider on the tvOS simulator: a tvOS-only
app depending on path_provider directly FAILS to build —
Target native_assets required define SdkRoot but it was not provided
The build failed.
So the prior advisory ("pure Dart-FFI … already works on tvOS, no
*_tvos package needed") was an unverified claim and is wrong for the
flutter-tvos toolchain: modern path_provider_foundation is a
dart:ffi / native-assets plugin (package:objective_c) and the tvOS
build pipeline does not feed SdkRoot to the native-assets hook.
Split the no-native-sources advisory into two honest cases:
- ffi/objective_c/build-hook present → "NOT supported by the
flutter-tvos build today (native-assets SdkRoot); a *_tvos package
won't help; needs engine native-assets support or a manual port."
- genuinely pure-Dart → federates via the platform interface; no
*_tvos package needed.
Tests updated to assert both branches. Suite green; analyze clean.
…rovider seed) dart:ffi / native-assets plugins (path_provider_foundation, …) can't be built for tvOS by the toolchain and we don't patch Flutter. Instead of a dead-end advisory, the analyzer now flags these (ffiNativeAssets) and the scaffolder emits a NATIVE federated *_tvos package — a Swift method-channel plugin + a Dart class extending the plugin's platform interface — the same model as flutter-tizen and as the verified shared_preferences_tvos. - NativeSkeleton: pure (path→content) generator; reuses the shared templates for pubspec/podspec/etc. - Seed registry: path_provider is fully implemented (NSTemporaryDirectory / NSSearchPathForDirectoriesInDomains for temp/docs/support/library/ cache; external/downloads throw UnsupportedError). Unseeded FFI plugins get a buildable skeleton (handlers return FlutterMethodNotImplemented, Dart inherits the interface's throwing defaults) + a PORTING_REPORT checklist. - This is the general, in-policy CLI mechanism for current and future FFI plugins: seeded → works; unseeded → buildable skeleton to finish. Tests: analyzer ffiNativeAssets flag + names; seeded path_provider package contents (Dart/Swift/pubspec/report); unseeded skeleton. Full plugin-port suite green; dart analyze lib/ exits 0.
Apps that depend (often transitively) on an FFI/native-assets plugin
— e.g. path_provider drags in the endorsed path_provider_foundation —
failed the tvOS build with "Target native_assets required define
SdkRoot". flutter_tools' code-asset path is iOS/macOS-only and we do
not patch it.
Two in-policy changes, entirely within flutter-tvos:
- TvosCopyFlutterBundle no longer pulls DartBuildForNative() /
InstallCodeAssets() into the tvOS build graph. tvOS cannot build
Dart code-assets, and federated tvOS plugins are plain Swift built
via CocoaPods — their Dart side is already routed by the existing
tvOS plugin-registrant override, so the native-assets step is
genuinely unnecessary on tvOS, not skipped as a hack.
- Since upstream CopyFlutterBundle still bundles native_assets.json as
NativeAssetsManifest.json, TvosCopyFlutterBundle.build() now writes
the canonical empty manifest ({"format-version":[1,0,0],
"native-assets":{}}) first so the bundle copy succeeds.
Verified on the tvOS simulator: an app using `path_provider` (with the
generated native federated path_provider_tvos) now builds AND runs,
returning real temp/documents/support directories. `dart analyze
lib/` exits 0; full plugin-port suite green.
…packages
A generated native federated *_tvos (FFI path, e.g. path_provider_tvos)
had no example/, so it couldn't be tried. NativeSkeleton now also emits
a tvOS-only example: example/pubspec.yaml depending on `<base>: any` +
`<base>_tvos: { path: ../ }`, example/lib/main.dart (path_provider's is
tailored to show temp/documents/support; unseeded gets a minimal app),
plus analysis_options/.gitignore/README. plugin_port renders the
example's tvOS runner for the FFI case (no fragile upstream-monorepo
example copy). Run with: cd <pkg>/example && flutter-tvos run.
Tests assert the example pubspec deps + main.dart. Suite green;
`dart analyze lib/` exits 0.
The native-skeleton generator had a hand-written path_provider implementation + a seed registry baked into the CLI. That couples a generic build tool to one plugin's domain logic, rots against platform-interface drift, and is the wrong layer/repo. NativeSkeleton now ALWAYS emits a generic, buildable native federated skeleton: federated pubspec/Dart (extends the platform interface, inherits its throwing defaults), a Swift method-channel stub returning FlutterMethodNotImplemented, a runnable tvOS-only example, and a PORTING_REPORT checklist that points implementers at the plugins repo. No `_seeds`, no per-plugin Swift/Dart, no special-casing anywhere in lib/. Tests use a synthetic fixture (no real plugin names) so the CLI repo carries zero plugin specificity. Finished, working `<plugin>_tvos` implementations (path_provider etc.) are authored and maintained in plugins/packages/, like flutter-tizen/plugins — never in the porter. dart analyze lib/ exits 0; plugin-port suite green.
Per the standing rule that the CLI repo carries zero plugin specificity (code AND tests), every real Flutter plugin name used as a test fixture is replaced with a synthetic one (gadget, widgetbox, prefsbox, dbbox, vidbox, audbox, …). Federated suffixes (_ios/_macos/_foundation/_darwin/_avfoundation/_storekit/_apple), *_platform_interface naming, and derived class names are preserved so the suffix-stripping / naming / FFI tests still exercise the same conventions — only the names changed, not the logic or assertions. grep for real plugin names across the plugin-port tests now returns nothing. Full suite: 89 tests green; `dart analyze test/` clean.
Physical-device (`appletvos`) builds failed at signing: "Automatic signing is disabled and unable to generate a profile … pass -allowProvisioningUpdates to xcodebuild." flutter-tvos already plumbs DEVELOPMENT_TEAM/CODE_SIGN_STYLE=Automatic but, unlike upstream `flutter build ios`, never told xcodebuild it may create/update the provisioning profile. Mirror upstream Flutter iOS: add `-allowProvisioningUpdates` to the device xcodebuild invocation (NativeTvosBundle). Simulator builds are unaffected (not code-signed). Still requires a signed-in Apple Developer team — this only lets automatic signing mint the profile when one exists. analyze lib/ exits 0.
Modern flutter/packages plugins split native code across several SwiftPM targets under one Sources/ dir (a Swift API target plus Objective-C <pkg>_objc / <pkg>_ios / <pkg>_macos targets). The analyzer only resolved the first target, silently dropping the rest, so such plugins failed to compile on tvOS. Now the porter detects the multi-target layout, copies every sibling target preserving structure (so cross-target relative imports keep resolving), drops the macOS-only target (tvOS uses the iOS sibling), widens ObjC TARGET_OS_IOS guards the same way Swift os(iOS) is widened, and emits a podspec that collapses the targets into one CocoaPods module exactly as the upstream package's own podspec does. Verified end-to-end: video_player_avfoundation now ports, builds, and renders actual video frames on the tvOS simulator.
Federated Apple plugins resolve flutter_assets via Bundle.main.path(forResource:ofType:) with a Bundle.main.bundleURL-relative fallback gated #if os(macOS). On tvOS the primary lookup fails the same way it does on macOS (nested flutter_assets/ paths don't resolve), so every Controller.asset(...) threw "Asset ... not found" — e.g. the official video_player example. SwiftPorter now widens that guard to also run on tvOS, scoped to the asset-fallback idiom (keyed on Bundle.main.bundleURL in the block) so `#if os(macOS) import FlutterMacOS` branches stay macOS-only — tvOS still takes the widened os(iOS) Flutter branch. Verified: regenerated video_player_tvos; the official upstream example loads and renders its bundled asset video on tvOS.
The prebuilt tvOS device engine's +[FlutterDartProject lookupKeyForAsset:] defaults to the iOS AOT path Frameworks/App.framework/flutter_assets/<asset>, but flutter-tvos copies flutter_assets to the app bundle root. Every plugin that resolves a bundled asset via lookupKeyForAsset: (e.g. VideoPlayerController.asset) therefore received a path that does not exist on a physical Apple TV (it worked on the simulator only because that engine returns a bundle-root key). Set the engine's documented override FLTAssetsPath=flutter_assets in the generated app Info.plist so the lookup key matches where the build actually places assets. Fixes asset loading generically for all asset-using plugins. Verified on a physical Apple TV: VideoPlayerController.asset now returns INIT_OK/RENDER_OK with only CLI-generated artifacts.
Upstream plugin examples are wired for the source monorepo: many
declare `resolution: workspace` (a pub-workspace member) and a
`dependency_overrides:` block that re-points packages at sibling
`path:`s of that monorepo. Copied verbatim into the generated
package those make the detached example fail `pub get`
("found no workspace root" / "path which doesn't exist"), so the
example could never build or run its integration tests.
ExamplePorter now strips the workspace directive and the entire
dependency_overrides block. The porter already pins the real
`<base>` from pub.dev and wires the local `<base>_tvos`, so the
overrides are both unnecessary and wrong here; the platform
interface now resolves from pub.dev normally.
Verified: wakelock_plus_tvos example integration tests go from
"Failed to update packages" to PASS; audioplayers / sqflite
examples now resolve and build (their remaining failures are
unrelated tvOS-API issues, not pub wiring).
dart analyze on the porter branch was clean (0 errors, 0 warnings). Of 114 info lints, 104 are the codebase's deliberate explicit-local- type house style (consistent with vendored flutter_tools — left as is). This clears the 10 genuine nits: - drop redundant porting_result import (re-exported via swift_porter) - fix 4 broken dartdoc [refs] -> code spans - rewrap WebKit note so the URL is one literal (no adjacent-string whitespace lint, URL intact) - $packageName interpolation, drop redundant dryRun: false arg, single-quote two test strings Analyzer now reports only the 104 house-style infos; 92 porter tests pass.
Previously a type-level tvOS-incompatible API made the command
refuse and write nothing. Many such plugins are mostly portable
(e.g. flutter_secure_storage: Keychain works on tvOS, only the
optional LAContext biometric path doesn't), so rejecting the whole
package was over-strict.
Now the porter never refuses. A type/top-level use the porter
can't stub behind a method channel has its enclosing declaration
(property, member, or whole type) wrapped in `#if !os(tvOS)` /
`#if !TARGET_OS_TV` (new `disabledOnTvos` finding). The package
still compiles on tvOS with that feature disabled; everything else
is ported normally. PORTING_REPORT.md gains a "Disabled on tvOS"
section + honest build outlook ("partial — N region(s) disabled")
so the developer knows exactly what to hand-port.
Removed the fail-fast gate and --allow-unbuildable flag. Exclusion
is best-effort and brace-shallow; a symbol referenced widely may
still need manual cleanup — flagged, not silently broken (per the
agreed contract). 92 porter tests pass; analyzer clean.
…ngle-target podspec From the graceful-partial sweep reports: - Compatibility DB: add AVAudioSessionOptions (.defaultToSpeaker / .allowBluetooth(A2DP)) and CoreTelephony (CTTelephonyNetworkInfo / CTCarrier / …) as unsupported, so the porter disables those regions on tvOS instead of build-failing (audioplayers, flutter_tts, permission_handler). - Modular-SwiftPM podspec now also applies to a SINGLE-target package that uses the SwiftPM `include/<module>/` public-headers convention (new PluginSource.spmModularHeaders), not just multi-target. Without it the `#import "include/<mod>/X.h"` paths break once CocoaPods flattens the framework headers. Fixes sqflite_darwin → it now builds GREEN on tvOS. Packages without an `include/` dir keep the legacy flat globs (no regression). 94 porter tests pass; analyzer clean. flutter_tts/permission_handler/ audioplayers now detect+disable+document the new APIs but still hit the accepted scoped-exclusion cascade (emit + flag in summary, per the agreed contract) — clean compile needs the hand-edits the PORTING_REPORT points to.
Upstream `_plus`-style packages (connectivity_plus, device_info_plus, wakelock_plus, package_info_plus, flutter_tts, …) bundle Dart implementations for Linux / Windows / Web / macOS / Android alongside the iOS one. The porter was copying all of them verbatim into the generated `*_tvos` package's `lib/`. None of those files are reachable at runtime on tvOS (the federated registrar loads only the tvOS plugin class) and their transitive imports (`package:web`, `flutter_web_plugins`, `win32`, `package:nm`, …) are not in the generated pubspec — shipping them inflates the package and breaks `pana` / `dart pub publish` analysis. Scaffolder now: - Drops `.dart` files whose path/name marks them as a non-Apple platform implementation (`_(linux|windows|web|android|macos|osx|io) (_plugin)?\.dart$` suffix, or `web/`/`web_impl/`/`windows/`/`linux/` /`android/` path segment). `_ios*` files and prefix-form data classes (e.g. `macos_device_info.dart`) are kept — UIKit/UIDevice exist on tvOS and data classes have no platform-specific imports. - Scrubs `import`/`export` directives in the remaining files that point at dropped paths — including the `if (dart.library.js_interop)` conditional form — replacing them with a `// (pruned …)` placeholder so line numbers stay stable. - Reports what was dropped via `ScaffoldResult.prunedDartFiles`, the CLI's "Pruned N cross-platform Dart file(s)" line, and a new "Cross-platform Dart pruned" section in `PORTING_REPORT.md`. Verified end-to-end against `connectivity_plus` from pub: 3 files dropped (`src/connectivity_plus_linux.dart`, `src/connectivity_plus_web.dart`, `src/web/dart_html_connectivity_plugin.dart`), conditional export in the entry file replaced with the pruner placeholder, output count drops from 16 → 13 files. Two new scaffolder tests cover the suffix/prefix distinction and the conditional-export scrub; the rest of the porter suite (53 tests) stays green.
`_generateXcconfigs` only honoured `--build-name` / `--build-number`
CLI flags. When neither was passed (the usual case) it baked the
hardcoded defaults `1.0.0` / `1` into the generated xcconfig, so the
app's `CFBundleShortVersionString` / `CFBundleVersion` ignored the
`version: 1.2.3+4` line in `pubspec.yaml`. That broke
`package_info_plus_tvos` integration tests and would surface
incorrect data to any tvOS app reading version metadata.
Resolution order now matches what iOS gets through
`xcode_backend.dart`'s build phase script:
1. CLI flag (`--build-name` / `--build-number`)
2. `project.manifest.buildName` / `buildNumber` (parsed from
`pubspec.yaml`'s `version:` field — e.g. `1.2.3+4`)
3. Canonical defaults (`1.0.0` / `1`)
Verified end-to-end against `package_info_plus_tvos`'s upstream
integration test: previously failed `buildNumber` expected `4` got `1`;
now passes 2/2 (`fromPlatform`, `example`).
`flutter-tvos plugin port` had a long inline section in the top-level
README and no entry in doc/. Now there's a proper user-facing guide
(doc/port-plugin.md) matching the existing doc style (quick start,
flag table, what it transforms, what it can't do, after-porting
checklist, troubleshooting) with the published `fluttertv/plugins`
repo as a worked-examples reference.
README:
- the dedicated "Add tvOS support to an existing plugin" section
is now a 4-line stub that links to the new doc and to
`fluttertv/plugins`;
- the platform-key section's outdated "FlutterTV-curated index
being assembled and will be published soon" line now links to
the published packages and the new doc;
- "Plugin development" docs list gains a third entry.
When `flutter-tvos build/run` prepares a project, any plugin in the
dep graph that has a FlutterTV-published `<name>_tvos` sibling the
user hasn't added is surfaced as a one-line warning:
audioplayers_tvos is available on pub.dev under the fluttertv.dev
verified publisher. Did you forget to add it to pubspec.yaml?
The known-plugins map (`_kKnownTvosPlugins`) is keyed only on the
user-facing aggregator name (`audioplayers`, not `audioplayers_darwin`
or `audioplayers_android`). Aggregator-vs-impl deduplication then
happens implicitly — when an app pulls in `audioplayers` the dep
graph lands `audioplayers`, `audioplayers_darwin`,
`audioplayers_android`, … but only the first matches a key, so the
warning fires exactly once.
The value is `List<String>` of acceptable alternative implementations
(e.g. one upstream plugin could in future have two competing `*_tvos`
ports). Empty list means the canonical `<name>_tvos` is the only fix.
Plugins outside the curated list are silently ignored — no hard-fail
and no auto-recommend-the-porter for every random plugin (that would
be presumptuous and noisy). The porter is one click away in
`doc/port-plugin.md`, linked from the README.
Implementation in `lib/tvos_plugins.dart`:
- new `_findAllPluginNames(project)` — names of every dep that
declares `flutter.plugin`, regardless of platform support;
- new public `recommendTvosPluginsToInstall(...)` — pure function
that turns the dep list into a list of warning lines (kept public
so unit tests don't have to fake a project tree);
- `ensureReadyForTvosTooling` calls them in sequence and prints each
message via `globals.logger.printWarning`.
Verified end-to-end against a synthetic `flutter-tvos create` project
with `audioplayers`, `connectivity_plus`, and `url_launcher`: three
matching `*_tvos` lines, no noise about Android/Linux/web siblings,
per-plugin lines disappear when the matching `_tvos` is added.
Tests: 6 cases in `tvos_plugins_test.dart` for the recommender
(empty dep list; single known; aggregator+impl dedupe; user already
installed; unknown silently ignored; multiple known plugins). All
pass.
Minor release — the "porter release". v1.0.1 had no
`lib/plugin_porting/` or `flutter-tvos plugin port` command at all;
v1.1.0 ships the entire 7-phase porter for scaffolding federated
`*_tvos` plugins from existing iOS / macOS plugins.
Highlights:
• New `flutter-tvos plugin port` command (positional path,
--from-pub, --from-git): Swift + Objective-C transformers
that widen iOS guards and @available to tvOS, a compatibility
database for tvOS-absent APIs (WebKit, SafariServices,
LocalAuthentication, CoreLocation, CaptiveNetwork, NEHotspot,
StoreKit code-redemption, AVAudioSession options,
CoreTelephony, …), graceful partial port via #if !os(tvOS),
multi-target SwiftPM collapsing, FFI-skeleton fallback,
cross-platform Dart pruning, and a PORTING_REPORT.md per port.
The 11 packages at github.com/fluttertv/plugins (pub.dev under
fluttertv.dev) were all produced this way.
• flutter-tvos create --platforms=tvos / --tvos-only — genuine
tvOS-only scaffold (no more create-iOS-then-strip).
• Build-time warning when an app pulls in a plugin that has a
FlutterTV-published <name>_tvos sibling the user hasn't added.
• Build pipeline fixes: FLTAssetsPath on device, xcodebuild
-allowProvisioningUpdates, Dart-native-assets skip for tvOS
(issue fluttertv#3), Generated.xcconfig now reads pubspec `version:`.
• New `doc/port-plugin.md` user guide; README's inline porter
section now a stub deferring to it; README updated to point
at the now-published fluttertv/plugins repo and verified
publisher.
Flutter SDK + tvOS engine artefact versions unchanged — pinned to
v1.0.1's `00b0c91f06` / `v1.0.0-flutter3.41.9`.
Release tag will be `v3.41.9-tvos.1.1.0` per the existing
`v<flutter-version>-tvos.<x.y.z>` scheme. CHANGELOG.md updated to
reflect the full scope.
Code-review follow-ups, no behaviour change.
• Extract `_walkPluginDependencies` — `_discoverTvosPlugins` and
`_findAllPluginNames` previously contained ~80 lines of identical
code reading `.flutter-plugins-dependencies` +
`.dart_tool/package_config.json` + each plugin's `pubspec.yaml`,
differing only in the final filter (`platforms.tvos`-only vs
"every `flutter.plugin`"). Both now reduce to a one-line
comprehension over a shared walker. Net: 235 → 168 lines in
the consumers, one source of truth for the parse/resolve logic
and its error handling.
• Rename `installed` → `depGraph` in `recommendTvosPluginsToInstall`.
The set holds every plugin in the dep graph (the membership
check then works correctly because the canonical `<name>_tvos`,
if added, IS in the dep graph), but the old name read as
"things the user has installed to fix things", which was
misleading.
• New `testUsingContext` integration test that exercises
`ensureReadyForTvosTooling` end-to-end against a MemoryFileSystem
project: writes `audioplayers` + `url_launcher` into the
dep-graph and package_config, runs the function, asserts the
BufferLogger captured the `audioplayers_tvos` recommendation
and stayed silent about `url_launcher` (not in curated list).
Guards against a future refactor silently dropping the call to
`recommendTvosPluginsToInstall` — the existing pure-function
unit tests wouldn't catch that.
192 / 192 tests pass, `dart analyze lib/` 0 errors / 0 warnings.
DenisovAV
reviewed
May 25, 2026
DenisovAV
reviewed
May 25, 2026
DenisovAV
reviewed
May 25, 2026
- scaffolder: extend directive-pruning regex to also cover `part` directives (previously only `import` and `export` were matched; a `part 'src/foo.dart'` pointing at a dropped path would survive unchanged and break the Dart build). `part of 'parent.dart'` is excluded by design — the `of` keyword sits between `part` and the opening quote so it does not match the regex. - swift_porter: add `dotAll: true` to `_availabilityClause` so multi-line `@available` attributes (e.g. Pigeon-generated code split across two lines) are matched and widened with a `tvOS` entry. Previously such attributes were silently skipped, producing an "unavailable" compile error on tvOS. - compatibility_database: rename `stripImports` → `stripSwiftImports` to make the Swift-only contract explicit at the type boundary. The ObjC porter derives framework names from these entries by stripping the `import ` prefix; an ObjC-syntax entry would silently produce the wrong key and never be commented out. The doc-comment is expanded to spell out the Swift-only requirement. (A `const` constructor precludes a runtime `assert` with a lambda, so an explicit name is the right enforcement mechanism here.)
DenisovAV
approved these changes
May 25, 2026
Contributor
DenisovAV
left a comment
There was a problem hiding this comment.
All three issues fixed — part directive scrubbing, multi-line @available via dotAll: true, and stripImports renamed to stripSwiftImports. LGTM.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Minor release — the "porter release".
v3.41.9-tvos.1.0.1had nolib/plugin_porting/orflutter-tvos plugin portcommand at all;this PR ships the entire 7-phase porter for scaffolding federated
*_tvosplugins from existing iOS / macOS plugins, plus first-classflutter-tvos create --platforms=tvos, a build-time nudge forplugins missing tvOS support, and a handful of build-pipeline fixes.
No Flutter SDK or engine-artefact change — pinned versions match
v1.0.1(Flutter3.41.9/00b0c91f06, tvOS engine artefactsv1.0.0-flutter3.41.9). Release tag will bev3.41.9-tvos.1.1.0per the existing
v<flutter-version>-tvos.<x.y.z>scheme.Highlights
Added
flutter-tvos plugin port— new command. Scaffolds afederated
<plugin>_tvossibling from any existing iOS or macOSFlutter plugin. Source loaders: positional local path,
--from-pub <name>, or--from-git <url> --ref <ref>. The 11packages at
fluttertv/plugins/ pub.dev under fluttertv.dev
were all produced this way. The transformer:
#if os(iOS)/#elseif os(macOS)and ObjC#if TARGET_OS_IOSso tvOS follows the iOS branch;@available iOS X, */#available/API_AVAILABLE(ios())to also listtvOS X;SafariServices, LocalAuthentication, CoreLocation,
CaptiveNetwork, NEHotspot, StoreKit code-redemption,
UIPasteboard, AVAudioSession Bluetooth / speaker options,CoreTelephony, GoogleSignIn SDK, …) and either strips the
import + stubs the enclosing method-channel handler, or wraps
type-level uses behind
#if !os(tvOS)(graceful partial port);CocoaPods module;
plugins the tvOS toolchain can't build for as-is;
lib/(drops_plus-style packages' Linux / Windows / Web / macOS /Android implementations + scrubs their imports).
Every transformation is recorded in a
PORTING_REPORT.mdwritten alongside the package.
flutter-tvos create --platforms=tvos/--tvos-only—genuine tvOS-only project scaffold (no more
create-iOS-then-strip).
Build-time warning for plugins missing tvOS support. Each
plugin in the app's dep graph that has a FlutterTV-published
<name>_tvossibling the user hasn't added is surfaced as aone-line warning during
flutter-tvos build/run. Keyed on theuser-facing aggregator name so
_darwin/_android/_linuxsiblings don't produce duplicate or noisy warnings.New
doc/port-plugin.md— proper user guide forplugin port(quick start, full flag reference, what the transformerdoes and doesn't do, after-porting workflow, troubleshooting).
Fixed
Generated.xcconfignow readsFLUTTER_BUILD_NAME/FLUTTER_BUILD_NUMBERfrom the app'spubspec.yamlversion:(
1.2.3+4→1.2.3/4). Previously hardcoded to1.0.0/1unless overridden via--build-name/--build-number,which broke
package_info_plusand any code readingCFBundleShortVersionString/CFBundleVersion.FLTAssetsPathis now baked into the generatedInfo.plistsoplugin asset lookup resolves on a real Apple TV (not just the
simulator).
-allowProvisioningUpdatesto xcodebuildso the first build on a fresh team / device can refresh
provisioning profiles itself.
transitively pull in an FFI / native-assets plugin (e.g.
path_provider_foundation,package_info_plus) no longer failwith
Target native_assets required define SdkRooton firstbuild (issue Target native_assets required define SdkRoot but it was not provided #3).
Documentation
doc/port-plugin.md; the platform-key paragraph points at thepublished
fluttertv/pluginsrepo and the
fluttertv.devverified publisher in place of "FlutterTV-curated index being
assembled and will be published soon".
doc/index gains "Porting an existing plugin" under "Plugindevelopment".
Verification
dart analyze lib/— 0 errors, 0 warningsdart test test/general/— 191 / 191 passingfluttertv/pluginsvia--from-pub, published them to pub.dev underfluttertv.dev,and exercised every one in a sample app on the Apple TV
simulator.
See
CHANGELOG.mdfor the full per-line entry.Closes #3.