diff --git a/CHANGELOG.md b/CHANGELOG.md index 57c3b9a..87f724d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,37 @@ All notable changes to flutter-tvos will be documented here. +## [1.3.2] — 2026-06-22 + +Refreshes the pinned engine to **Flutter 3.44.3** (`e1fd963c`, Dart +`d684a576`). + +### Changed +- Bumped `bin/internal/flutter.version` to `e1fd963c` (Flutter 3.44.3) and + `bin/internal/engine.version` to `v1.0.0-flutter3.44.3`. +- Rebuilt all six engine artifact variants from the Flutter 3.44.3 source tree. + This carries the 3.44.3 engine fixes — notably the APNG image decoder + (`lib/ui/painting/image_generator_apng`). Dart is unchanged (`d684a576`), so + the SDK hash is stable and AOT loads kernel without an `Invalid SDK hash` + mismatch. +- All 17 tvOS engine patches apply cleanly on the 3.44.3 tree. +- Picks up the Flutter 3.44.3 `flutter_tools` fixes that ship in the SDK + (asset hashing, `error_handling_io`, Swift Package Manager, Dart language + version), since the CLI wraps the pinned SDK unmodified. +- Bundled `flutter_tvos` plugin updated to 1.1.2 (README/docs refresh). +- **`flutter-tvos precache` no longer downloads other platforms' artifacts.** + With no platform flags it now fetches only the tvOS engine set plus the + universal artifacts (fonts, patched SDK, host USB-deploy tools); the Android, + iOS-engine, web, and macOS SDKs are skipped. The stock per-platform flags + (`--ios`, `--android`, `--all-platforms`, …) still work when passed + explicitly. The tvOS engine artifacts also now render under a **tvOS Engine** + header in the same nested tree style as the bundled Flutter SDK. + +### Fixed +- **`flutter-tvos create` with no output directory** now prints the usage + message and exits with code 2 (matching stock `flutter create`) instead of + crashing with `Bad state: No element`. + ## [1.3.1] — 2026-06-16 Refreshes the pinned engine to **Flutter 3.44.2** (`c9a6c484`, Dart diff --git a/README.md b/README.md index c40be45..63aeca7 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ A Flutter toolchain for building and running Flutter apps on **Apple TV (tvOS)** ## Current version -- flutter-tvos: `1.3.0` -- Flutter SDK: `3.44.1` (`924134a44c189315be2148659913dda1671cbe99`) -- tvOS engine artifacts: `v1.0.0-flutter3.44.1` +- flutter-tvos: `1.3.2` +- Flutter SDK: `3.44.3` (`e1fd963c6f6922bd32afde2e9698a363cd0406d2`) +- tvOS engine artifacts: `v1.0.0-flutter3.44.3` ## Installation diff --git a/bin/internal/engine.version b/bin/internal/engine.version index ef3fb1b..970e868 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -v1.0.0-flutter3.44.2 +v1.0.0-flutter3.44.3 diff --git a/bin/internal/flutter.version b/bin/internal/flutter.version index fd4cf32..912347e 100644 --- a/bin/internal/flutter.version +++ b/bin/internal/flutter.version @@ -1 +1 @@ -c9a6c484230f8b5e408ec57be1ef71dee1e77020 +e1fd963c6f6922bd32afde2e9698a363cd0406d2 diff --git a/lib/commands/create.dart b/lib/commands/create.dart index 72c4365..ff25f66 100644 --- a/lib/commands/create.dart +++ b/lib/commands/create.dart @@ -24,6 +24,12 @@ class TvosCreateCommand extends CreateCommand { @override Future runCommand() async { + // Mirror stock `flutter create`: print the friendly usage message and exit + // (code 2) when no output directory is given — or more than one — instead + // of crashing on `rest.first` ("Bad state: No element"). The tvos-only path + // below reads `rest.first` before delegating to `super.runCommand()`, so + // validate up front. + validateOutputDirectoryArg(); final String projectDirPath = argResults!.rest.first; final String name = stringArg('project-name') ?? globals.fs.path.basename(projectDirPath); diff --git a/lib/commands/precache.dart b/lib/commands/precache.dart index 7ee8630..586defc 100644 --- a/lib/commands/precache.dart +++ b/lib/commands/precache.dart @@ -4,8 +4,10 @@ import 'package:file/src/interface/directory.dart'; import 'package:flutter_tools/src/commands/precache.dart'; +import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/runner/flutter_command.dart'; +import 'package:meta/meta.dart'; import '../tvos_cache.dart'; @@ -20,6 +22,44 @@ class TvosPrecacheCommand extends PrecacheCommand { argParser.addFlag('tvos', defaultsTo: true, help: 'Precache artifacts for tvOS development.'); } + // The `--android` umbrella flag stands in for its three child artifacts. + static const Map _umbrellaForArtifact = { + 'android_gen_snapshot': 'android', + 'android_maven': 'android', + 'android_internal_build': 'android', + }; + + // Non-platform artifacts a tvOS build always needs (fonts, sky_engine, + // flutter_patched_sdk, font-subset, host USB-deploy tools, engine stamp). + static const Set _alwaysOn = {'universal', 'informative'}; + + /// The non-tvOS artifacts to fetch for the given flags. With no platform + /// flags this is only [_alwaysOn]; `--all-platforms` and explicit per-platform + /// flags add their artifacts, and a flag for an `--android` child works either + /// via `--android` or the child's own flag. Feature-gated platforms are + /// skipped when their feature is disabled. Pure (no I/O) so it is unit-tested + /// directly — see `test/general/tvos_precache_test.dart`. + @visibleForTesting + static Set selectRequiredArtifacts({ + required FeatureFlags featureFlags, + required bool allPlatforms, + required bool Function(String flagName) isFlagOn, + }) { + final requiredArtifacts = {}; + for (final DevelopmentArtifact artifact in DevelopmentArtifact.values) { + if (artifact.feature != null && !featureFlags.isEnabled(artifact.feature!)) { + continue; + } + final String? umbrella = _umbrellaForArtifact[artifact.name]; + final bool explicitlyRequested = + isFlagOn(artifact.name) || (umbrella != null && isFlagOn(umbrella)); + if (allPlatforms || _alwaysOn.contains(artifact.name) || explicitlyRequested) { + requiredArtifacts.add(artifact); + } + } + return requiredArtifacts; + } + @override Future runCommand() async { if (boolArg('tvos')) { @@ -31,6 +71,44 @@ class TvosPrecacheCommand extends PrecacheCommand { } await globals.cache.updateAll({TvosDevelopmentArtifact.tvos}); } - return super.runCommand(); + + // Stock `flutter precache` with no platform flags downloads *every* enabled + // platform's artifacts (Android, iOS, web, macOS). A tvOS embedder needs + // none of those — only the universal artifacts (fonts, sky_engine, + // flutter_patched_sdk, font-subset) and the engine stamp, on top of the + // tvOS engine set fetched above. So drive the cache ourselves instead of + // delegating to `super.runCommand()`, while still honouring the stock + // per-platform flags (`--ios`, `--android`, `--all-platforms`, …) for anyone + // who explicitly asks for them. + if (globals.platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true') { + await globals.cache.lock(); + } + if (boolArg('force')) { + globals.cache.clearStampFiles(); + } + final bool allPlatforms = boolArg('all-platforms'); + if (allPlatforms) { + globals.cache.includeAllPlatforms = true; + } + if (boolArg('use-unsigned-mac-binaries')) { + globals.cache.useUnsignedMacBinaries = true; + } + + final Set requiredArtifacts = selectRequiredArtifacts( + featureFlags: featureFlags, + allPlatforms: allPlatforms, + // `ArgResults.wasParsed` throws on an option the command never defined, so + // guard with `options.containsKey` — a future Flutter `DevelopmentArtifact` + // without a matching precache flag then can't crash us. + isFlagOn: (String name) => + argParser.options.containsKey(name) && argResults!.wasParsed(name) && boolArg(name), + ); + + // `updateAll` is idempotent — it checks each artifact's stamp and re-downloads + // only what is stale, so there is no need (and no reliable way, since the + // cache also tracks platforms we intentionally skip) to short-circuit on a + // global `isUpToDate()` check. + await globals.cache.updateAll(requiredArtifacts); + return FlutterCommandResult.success(); } } diff --git a/lib/plugin_porting/templates.dart b/lib/plugin_porting/templates.dart index 8bcff1f..377d766 100644 --- a/lib/plugin_porting/templates.dart +++ b/lib/plugin_porting/templates.dart @@ -457,6 +457,10 @@ tvos/.symlinks/ tvos/Flutter/Flutter.framework tvos/Flutter/Flutter.podspec +# Xcode / SwiftPM (per-user, generated when tvos/Package.swift is opened) +**/.swiftpm/ +**/xcuserdata/ + # IDE .idea/ .vscode/ diff --git a/lib/tvos_cache.dart b/lib/tvos_cache.dart index cce7756..f515a1f 100644 --- a/lib/tvos_cache.dart +++ b/lib/tvos_cache.dart @@ -111,6 +111,12 @@ class TvosEngineArtifacts extends EngineCachedArtifact { 'host_release.zip', ]; + // The artifact-group header printed by `Cache.updateAll` — defaults to the + // (stamp) `name`, which reads as a bare "engine". Label it "tvOS Engine" so + // the precache output names the toolchain it belongs to. + @override + String get displayName => 'tvOS Engine'; + @override Directory get location => tvosArtifactDirectory(globals.fs); @@ -194,14 +200,16 @@ class TvosEngineArtifacts extends EngineCachedArtifact { ); try { + var index = 0; for (final String zipName in _artifactZipNames) { + index++; final String url = artifactDownloadUrl(zipName); final File tempZip = tempDir.childFile(zipName); - // Format mirrors stock Flutter cache: - // `Downloading tools... 1,339ms` - // The Status object writes the elapsed time on stop(). + // Render as a child of the framework-printed `[i/N] engine` header, + // mirroring stock Flutter's nested artifact tree. The Status object + // writes the elapsed time on stop(). final Status status = _logger.startProgress( - 'Downloading ${_friendlyName(zipName)} tools...', + _treeLine(index, _artifactZipNames.length, _friendlyName(zipName)), ); try { final RunResult curlResult = await _processUtils.run([ @@ -264,9 +272,11 @@ class TvosEngineArtifacts extends EngineCachedArtifact { } location.createSync(recursive: true); + var index = 0; for (final zip in zips) { + index++; final Status status = _logger.startProgress( - 'Extracting ${_friendlyName(zip.basename)} tools...', + _treeLine(index, zips.length, _friendlyName(zip.basename)), ); try { final RunResult result = await _processUtils.run([ @@ -293,9 +303,19 @@ class TvosEngineArtifacts extends EngineCachedArtifact { _makeFilesExecutable(location, operatingSystemUtils); } + /// Formats one zip's progress line as a child of the framework-printed + /// `[i/N] engine` header, mirroring stock Flutter's nested artifact tree + /// (see `ArtifactUpdater.formatProgressMessage`): ` ├─ [1/6] tvos-debug-…`, + /// with `└─` on the final entry. The elapsed time is appended by the + /// [Status] on stop, exactly like the bundled "Flutter SDK" artifact. + String _treeLine(int index, int total, String name) { + final prefix = index == total ? '└─' : '├─'; + return ' $prefix [$index/$total] $name'; + } + /// Converts a zip filename to the human-readable label that goes into the - /// `Downloading … tools...` line. Mirrors stock Flutter's `/` - /// format using hyphens. + /// progress line. Mirrors stock Flutter's `/` format using + /// hyphens. /// /// Examples: /// tvos_debug_sim_arm64.zip → tvos-debug-sim-arm64 diff --git a/packages/flutter_tvos/CHANGELOG.md b/packages/flutter_tvos/CHANGELOG.md index da6116a..d0834fa 100644 --- a/packages/flutter_tvos/CHANGELOG.md +++ b/packages/flutter_tvos/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.1.2 + +- Documentation: list Siri Remote support in the README feature summary and + bump the install snippet to the current version. No functional or API + changes. + ## 1.1.1 - Fixed the remote-control `configure` handshake racing native plugin diff --git a/packages/flutter_tvos/README.md b/packages/flutter_tvos/README.md index b16885a..bf4007e 100644 --- a/packages/flutter_tvos/README.md +++ b/packages/flutter_tvos/README.md @@ -11,6 +11,9 @@ Part of the [flutter-tvos](https://fluttertv.dev) project — an open-source Flu - Check device capabilities: 4K, HDR, multi-user support - Get display resolution - **Synchronous API** — powered by dart:ffi, zero async overhead +- **Siri Remote support** — swipes and buttons drive Flutter's standard + focus system, with raw-touch and high-level swipe listeners for custom + handling (see [Remote Control](#remote-control-siri-remote)) ## Getting Started @@ -18,7 +21,7 @@ Add `flutter_tvos` to your `pubspec.yaml`: ```yaml dependencies: - flutter_tvos: ^1.0.4 + flutter_tvos: ^1.1.2 ``` ## Usage diff --git a/packages/flutter_tvos/example/pubspec.lock b/packages/flutter_tvos/example/pubspec.lock index f0384e8..373da55 100644 --- a/packages/flutter_tvos/example/pubspec.lock +++ b/packages/flutter_tvos/example/pubspec.lock @@ -94,7 +94,7 @@ packages: path: ".." relative: true source: path - version: "1.1.0" + version: "1.1.2" fuchsia_remote_debug_protocol: dependency: transitive description: flutter diff --git a/packages/flutter_tvos/pubspec.yaml b/packages/flutter_tvos/pubspec.yaml index c9afae9..bef5124 100644 --- a/packages/flutter_tvos/pubspec.yaml +++ b/packages/flutter_tvos/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_tvos description: Platform detection and utilities for Flutter apps running on Apple TV (tvOS). Provides runtime checks for tvOS, device info, and capability queries. -version: 1.1.1 +version: 1.1.2 homepage: https://fluttertv.dev repository: https://github.com/fluttertv/flutter-tvos issue_tracker: https://github.com/fluttertv/flutter-tvos/issues diff --git a/packages/flutter_tvos/tvos/.gitignore b/packages/flutter_tvos/tvos/.gitignore new file mode 100644 index 0000000..ae60f79 --- /dev/null +++ b/packages/flutter_tvos/tvos/.gitignore @@ -0,0 +1,4 @@ +# Per-user Xcode / SwiftPM local state, generated when the tvos/Package.swift +# is opened in Xcode. Not source — never commit. +.swiftpm/ +xcuserdata/ diff --git a/pubspec.yaml b/pubspec.yaml index 2b96e37..c10d059 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_tvos description: Flutter CLI tool for building and running Flutter apps on Apple TV (tvOS). A custom embedder wrapping Flutter SDK with tvOS-specific build targets, device management, and plugin support. -version: 1.3.1 +version: 1.3.2 homepage: https://fluttertv.dev repository: https://github.com/fluttertv/flutter-tvos publish_to: "none" diff --git a/test/general/tvos_precache_test.dart b/test/general/tvos_precache_test.dart new file mode 100644 index 0000000..c49a69b --- /dev/null +++ b/test/general/tvos_precache_test.dart @@ -0,0 +1,124 @@ +// Copyright 2026 The FlutterTV Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tvos/commands/precache.dart'; + +import '../src/common.dart'; +import '../src/fakes.dart'; + +void main() { + // Builds the `isFlagOn` predicate the command passes, from a set of "on" flags. + bool Function(String) flagsOn(Set on) => (String name) => on.contains(name); + + Set namesOf(Set artifacts) => + artifacts.map((DevelopmentArtifact a) => a.name).toSet(); + + group('TvosPrecacheCommand.selectRequiredArtifacts', () { + testWithoutContext('with no platform flags fetches only the universal/informative set — ' + 'no Android, iOS, web, or macOS', () { + final Set names = namesOf( + TvosPrecacheCommand.selectRequiredArtifacts( + featureFlags: TestFeatureFlags(), + allPlatforms: false, + isFlagOn: flagsOn({}), + ), + ); + expect(names, containsAll(['universal', 'informative'])); + expect(names, isNot(contains('ios'))); + expect(names, isNot(contains('android_gen_snapshot'))); + expect(names, isNot(contains('android_maven'))); + expect(names, isNot(contains('web'))); + expect(names, isNot(contains('macos'))); + }); + + testWithoutContext('--ios adds the iOS artifact (and keeps the universal set)', () { + final Set names = namesOf( + TvosPrecacheCommand.selectRequiredArtifacts( + featureFlags: TestFeatureFlags(), + allPlatforms: false, + isFlagOn: flagsOn({'ios'}), + ), + ); + expect(names, contains('ios')); + expect(names, containsAll(['universal', 'informative'])); + expect(names, isNot(contains('macos'))); + }); + + testWithoutContext('--android expands to all three android child artifacts', () { + final Set names = namesOf( + TvosPrecacheCommand.selectRequiredArtifacts( + featureFlags: TestFeatureFlags(), + allPlatforms: false, + isFlagOn: flagsOn({'android'}), + ), + ); + expect( + names, + containsAll(['android_gen_snapshot', 'android_maven', 'android_internal_build']), + ); + }); + + testWithoutContext('an individual android child flag is honored on its own, without ' + 'pulling in its siblings', () { + final Set names = namesOf( + TvosPrecacheCommand.selectRequiredArtifacts( + featureFlags: TestFeatureFlags(), + allPlatforms: false, + isFlagOn: flagsOn({'android_maven'}), + ), + ); + expect(names, contains('android_maven')); + expect(names, isNot(contains('android_gen_snapshot'))); + }); + + testWithoutContext('a feature-gated platform is skipped when its feature is disabled, ' + 'even if its flag is set', () { + final Set names = namesOf( + TvosPrecacheCommand.selectRequiredArtifacts( + featureFlags: TestFeatureFlags(), + allPlatforms: false, + isFlagOn: flagsOn({'web'}), + ), + ); + expect(names, isNot(contains('web'))); + }); + + testWithoutContext('a feature-enabled platform with its flag set is included', () { + final Set names = namesOf( + TvosPrecacheCommand.selectRequiredArtifacts( + featureFlags: TestFeatureFlags(isWebEnabled: true), + allPlatforms: false, + isFlagOn: flagsOn({'web'}), + ), + ); + expect(names, contains('web')); + }); + + testWithoutContext('--all-platforms includes every feature-enabled artifact', () { + final Set names = namesOf( + TvosPrecacheCommand.selectRequiredArtifacts( + featureFlags: TestFeatureFlags(isWebEnabled: true, isMacOSEnabled: true), + allPlatforms: true, + isFlagOn: flagsOn({}), + ), + ); + expect( + names, + containsAll(['ios', 'web', 'macos', 'universal', 'informative']), + ); + }); + + testWithoutContext('a disabled iOS feature excludes iOS even from the default set', () { + final Set names = namesOf( + TvosPrecacheCommand.selectRequiredArtifacts( + featureFlags: TestFeatureFlags(isIOSEnabled: false), + allPlatforms: false, + isFlagOn: flagsOn({'ios'}), + ), + ); + expect(names, isNot(contains('ios'))); + }); + }); +}