Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

All notable changes to flutter-tvos will be documented here.

## [Unreleased]

### Changed
- **`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
Expand Down
6 changes: 6 additions & 0 deletions lib/commands/create.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ class TvosCreateCommand extends CreateCommand {

@override
Future<FlutterCommandResult> 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);
Expand Down
52 changes: 51 additions & 1 deletion lib/commands/precache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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';

Expand Down Expand Up @@ -31,6 +32,55 @@ class TvosPrecacheCommand extends PrecacheCommand {
}
await globals.cache.updateAll(<DevelopmentArtifact>{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;
}

// The `--android` umbrella flag stands in for its three child artifacts.
const umbrellaForArtifact = <String, String>{
'android_gen_snapshot': 'android',
'android_maven': 'android',
'android_internal_build': 'android',
};
// Non-platform artifacts a tvOS build needs; always fetched.
const alwaysOn = <String>{'universal', 'informative'};

final requiredArtifacts = <DevelopmentArtifact>{};
for (final DevelopmentArtifact artifact in DevelopmentArtifact.values) {
if (artifact.feature != null && !featureFlags.isEnabled(artifact.feature!)) {
continue;
}
final String flagName = umbrellaForArtifact[artifact.name] ?? artifact.name;
final bool explicitlyRequested = argResults!.wasParsed(flagName) && boolArg(flagName);
if (allPlatforms || alwaysOn.contains(artifact.name) || explicitlyRequested) {
requiredArtifacts.add(artifact);
}
}

if (!await globals.cache.isUpToDate()) {
await globals.cache.updateAll(requiredArtifacts);
} else {
globals.logger.printStatus('Already up-to-date.');
}
return FlutterCommandResult.success();
}
}
34 changes: 27 additions & 7 deletions lib/tvos_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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 <name> 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(<String>[
Expand Down Expand Up @@ -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(<String>[
Expand All @@ -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 `<target>/<host>`
/// format using hyphens.
/// progress line. Mirrors stock Flutter's `<target>/<host>` format using
/// hyphens.
///
/// Examples:
/// tvos_debug_sim_arm64.zip → tvos-debug-sim-arm64
Expand Down
Loading