Victoria Ashworth af8e9801a4
Add tooling to migrate to UIScene (#176427)
Migrates Flutter iOS apps to be compatible with UIScene lifecycle by
migrating the AppDelegate and Info.plist. If the AppDelegate does not
match Flutter's original template exactly, or if the Info.plist is not
found or can't be updated, then prompt the user to migrate manually.

This is hidden behind a feature flag so we can test it out first.

Fixes https://github.com/flutter/flutter/issues/170167.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-10-07 02:12:26 +00:00

404 lines
13 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// @docImport 'flutter_features.dart';
library;
import 'package:meta/meta.dart';
import 'base/context.dart';
/// The current [FeatureFlags] implementation.
FeatureFlags get featureFlags => context.get<FeatureFlags>()!;
/// The interface used to determine if a particular [Feature] is enabled.
///
/// This class is extended in google3. Whenever a new flag is added,
/// google3 must also be updated using a g3fix.
///
/// See also:
///
/// * [FlutterFeatureFlags], Flutter's implementation of this class.
/// * https://github.com/flutter/flutter/blob/main/docs/contributing/Feature-flags.md,
/// docs on feature flags and how to add or use them.
abstract class FeatureFlags {
/// const constructor so that subclasses can be const.
const FeatureFlags();
/// Whether flutter desktop for linux is enabled.
bool get isLinuxEnabled;
/// Whether flutter desktop for macOS is enabled.
bool get isMacOSEnabled;
/// Whether flutter web is enabled.
bool get isWebEnabled;
/// Whether flutter desktop for Windows is enabled.
bool get isWindowsEnabled;
/// Whether android is enabled.
bool get isAndroidEnabled;
/// Whether iOS is enabled.
bool get isIOSEnabled;
/// Whether fuchsia is enabled.
bool get isFuchsiaEnabled;
/// Whether custom devices are enabled.
bool get areCustomDevicesEnabled;
/// Whether animations are used in the command line interface.
bool get isCliAnimationEnabled;
/// Whether native assets compilation and bundling is enabled.
bool get isNativeAssetsEnabled;
/// Whether dart data assets building and bundling is enabled.
bool get isDartDataAssetsEnabled => false;
/// Whether Swift Package Manager dependency management is enabled.
bool get isSwiftPackageManagerEnabled;
/// Whether to stop writing the `{FLUTTER_ROOT}/version` file.
///
/// Tracking removal: <https://github.com/flutter/flutter/issues/171900>.
bool get isOmitLegacyVersionFileEnabled;
/// Whether desktop windowing is enabled.
bool get isWindowingEnabled;
/// Whether physical iOS devices are debugging with LLDB.
bool get isLLDBDebuggingEnabled;
/// Whether UIScene migration is enabled.
bool get isUISceneMigrationEnabled;
/// Whether a particular feature is enabled for the current channel.
///
/// Prefer using one of the specific getters above instead of this API.
bool isEnabled(Feature feature);
/// All current Flutter feature flags.
List<Feature> get allFeatures => const <Feature>[
flutterWebFeature,
flutterLinuxDesktopFeature,
flutterMacOSDesktopFeature,
flutterWindowsDesktopFeature,
flutterAndroidFeature,
flutterIOSFeature,
flutterFuchsiaFeature,
flutterCustomDevicesFeature,
cliAnimation,
nativeAssets,
dartDataAssets,
swiftPackageManager,
omitLegacyVersionFile,
windowingFeature,
lldbDebugging,
uiSceneMigration,
];
/// All current Flutter feature flags that can be configured.
///
/// [Feature.configSetting] is not `null`.
Iterable<Feature> get allConfigurableFeatures {
return allFeatures.where((Feature feature) => feature.configSetting != null);
}
/// All Flutter feature flags that are enabled.
// This member is overriden in google3.
Iterable<Feature> get allEnabledFeatures {
return allFeatures.where(isEnabled);
}
}
/// All current Flutter feature flags that can be configured.
///
/// [Feature.configSetting] is not `null`.
Iterable<Feature> get allConfigurableFeatures => featureFlags.allConfigurableFeatures;
/// The [Feature] for flutter web.
const flutterWebFeature = Feature.fullyEnabled(
name: 'Flutter for web',
configSetting: 'enable-web',
environmentOverride: 'FLUTTER_WEB',
);
/// The [Feature] for macOS desktop.
const flutterMacOSDesktopFeature = Feature.fullyEnabled(
name: 'support for desktop on macOS',
configSetting: 'enable-macos-desktop',
environmentOverride: 'FLUTTER_MACOS',
);
/// The [Feature] for Linux desktop.
const flutterLinuxDesktopFeature = Feature.fullyEnabled(
name: 'support for desktop on Linux',
configSetting: 'enable-linux-desktop',
environmentOverride: 'FLUTTER_LINUX',
);
/// The [Feature] for Windows desktop.
const flutterWindowsDesktopFeature = Feature.fullyEnabled(
name: 'support for desktop on Windows',
configSetting: 'enable-windows-desktop',
environmentOverride: 'FLUTTER_WINDOWS',
);
/// The [Feature] for Android devices.
const flutterAndroidFeature = Feature.fullyEnabled(
name: 'Flutter for Android',
configSetting: 'enable-android',
);
/// The [Feature] for iOS devices.
const flutterIOSFeature = Feature.fullyEnabled(
name: 'Flutter for iOS',
configSetting: 'enable-ios',
);
/// The [Feature] for Fuchsia support.
const flutterFuchsiaFeature = Feature(
name: 'Flutter for Fuchsia',
configSetting: 'enable-fuchsia',
environmentOverride: 'FLUTTER_FUCHSIA',
master: FeatureChannelSetting(available: true),
);
const flutterCustomDevicesFeature = Feature(
name: 'early support for custom device types',
configSetting: 'enable-custom-devices',
environmentOverride: 'FLUTTER_CUSTOM_DEVICES',
master: FeatureChannelSetting(available: true),
beta: FeatureChannelSetting(available: true),
stable: FeatureChannelSetting(available: true),
);
/// The [Feature] for CLI animations.
///
/// The TERM environment variable set to "dumb" turns this off.
const cliAnimation = Feature.fullyEnabled(
name: 'animations in the command line interface',
configSetting: 'cli-animations',
);
/// Enable native assets compilation and bundling.
const nativeAssets = Feature(
name: 'native assets compilation and bundling',
configSetting: 'enable-native-assets',
environmentOverride: 'FLUTTER_NATIVE_ASSETS',
master: FeatureChannelSetting(available: true, enabledByDefault: true),
beta: FeatureChannelSetting(available: true, enabledByDefault: true),
stable: FeatureChannelSetting(available: true, enabledByDefault: true),
);
/// Enable Dart data assets building and bundling.
const dartDataAssets = Feature(
name: 'Dart data assets building and bundling',
configSetting: 'enable-dart-data-assets',
environmentOverride: 'FLUTTER_DART_DATA_ASSETS',
master: FeatureChannelSetting(available: true),
);
/// Enable Swift Package Manager as a darwin dependency manager.
const swiftPackageManager = Feature(
name: 'support for Swift Package Manager for iOS and macOS',
configSetting: 'enable-swift-package-manager',
environmentOverride: 'FLUTTER_SWIFT_PACKAGE_MANAGER',
master: FeatureChannelSetting(available: true),
beta: FeatureChannelSetting(available: true),
stable: FeatureChannelSetting(available: true),
);
/// Whether to continue writing the `{FLUTTER_ROOT}/version` legacy file.
///
/// Tracking removal: <https://github.com/flutter/flutter/issues/171900>.
const omitLegacyVersionFile = Feature.fullyEnabled(
name: 'stops writing the legacy version file',
configSetting: 'omit-legacy-version-file',
extraHelpText:
'If set, the file {FLUTTER_ROOT}/version is no longer written as part of '
'the flutter tool execution; a newer file format has existed for some '
'time in {FLUTTER_ROOT}/bin/cache/flutter.version.json.',
);
/// Whether desktop windowing is enabled.
///
/// See: https://github.com/flutter/flutter/issues/30701.
const windowingFeature = Feature(
name: 'support for windowing on macOS, Linux, and Windows',
configSetting: 'enable-windowing',
environmentOverride: 'FLUTTER_WINDOWING',
runtimeId: 'windowing',
master: FeatureChannelSetting(available: true),
);
/// Enable LLDB debugging for physical iOS devices. When LLDB debugging is off,
/// Xcode debugging is used instead.
///
/// Requires iOS 17+ and Xcode 26+. If those requirements are not met, the previous
/// default debugging method is used instead.
const lldbDebugging = Feature(
name: 'support for debugging with LLDB for physical iOS devices',
extraHelpText:
'If LLDB debugging is off, Xcode debugging is used instead. '
'Only available for iOS 17 or newer devices. Requires Xcode 26 or greater.',
configSetting: 'enable-lldb-debugging',
environmentOverride: 'FLUTTER_LLDB_DEBUGGING',
master: FeatureChannelSetting(available: true, enabledByDefault: true),
beta: FeatureChannelSetting(available: true, enabledByDefault: true),
stable: FeatureChannelSetting(available: true, enabledByDefault: true),
);
/// Enable UIScene lifecycle migration for iOS apps. When enabled, if possible the tool will
/// attempt to auto-migrate the app. Otherwise, it will print a warning with instructions on how to
/// migrate manually.
const uiSceneMigration = Feature(
name: 'support for migrating to UIScene lifecycle',
extraHelpText:
'If enabled, Flutter will migrate your app to iOS UIScene lifecycle if possible or '
'otherwise instruct you to migrate manually.',
configSetting: 'enable-uiscene-migration',
environmentOverride: 'FLUTTER_UISCENE_MIGRATION',
master: FeatureChannelSetting(available: true),
beta: FeatureChannelSetting(available: true),
stable: FeatureChannelSetting(available: true),
);
/// A [Feature] is a process for conditionally enabling tool features.
///
/// All settings are optional, and if not provided will generally default to
/// a "safe" value, such as being off.
///
/// The top level feature settings can be provided to apply to all channels.
/// Otherwise, more specific settings take precedence over higher level
/// settings.
class Feature {
/// Creates a [Feature].
const Feature({
required this.name,
this.environmentOverride,
this.configSetting,
this.runtimeId,
this.extraHelpText,
this.master = const FeatureChannelSetting(),
this.beta = const FeatureChannelSetting(),
this.stable = const FeatureChannelSetting(),
});
/// Creates a [Feature] that is fully enabled across channels.
const Feature.fullyEnabled({
required this.name,
this.environmentOverride,
this.configSetting,
this.runtimeId,
this.extraHelpText,
}) : master = const FeatureChannelSetting(available: true, enabledByDefault: true),
beta = const FeatureChannelSetting(available: true, enabledByDefault: true),
stable = const FeatureChannelSetting(available: true, enabledByDefault: true);
/// The user visible name for this feature.
final String name;
/// The settings for the master branch and other unknown channels.
final FeatureChannelSetting master;
/// The settings for the beta branch.
final FeatureChannelSetting beta;
/// The settings for the stable branch.
final FeatureChannelSetting stable;
/// The name of an environment variable that can override the setting.
///
/// The environment variable needs to be set to the value 'true'. This is
/// only intended for usage by CI and not as an advertised method to enable
/// a feature.
///
/// If not provided, defaults to `null` meaning there is no override.
final String? environmentOverride;
/// The name of a setting that can be used to enable this feature.
///
/// If not provided, defaults to `null` meaning there is no config setting.
final String? configSetting;
/// The unique identifier for this feature at runtime.
///
/// If not `null`, the Flutter framework's enabled feature flags will
/// contain this value if this feature is enabled.
final String? runtimeId;
/// Additional text to add to the end of the help message.
///
/// If not provided, defaults to `null` meaning there is no additional text.
final String? extraHelpText;
/// A help message for the `flutter config` command, or null if unsupported.
String? generateHelpMessage() {
if (configSetting == null) {
return null;
}
final buffer = StringBuffer('Enable or disable $name.');
final channels = <String>[
if (master.available) 'master',
if (beta.available) 'beta',
if (stable.available) 'stable',
];
// Add channel info for settings only on some channels.
if (channels.length == 1) {
buffer.write('\nThis setting applies only to the ${channels.single} channel.');
} else if (channels.length == 2) {
buffer.write('\nThis setting applies only to the ${channels.join(' and ')} channels.');
}
if (extraHelpText != null) {
buffer.write(' $extraHelpText');
}
return buffer.toString();
}
/// Retrieve the correct setting for the provided `channel`.
FeatureChannelSetting getSettingForChannel(String channel) {
return switch (channel) {
'stable' => stable,
'beta' => beta,
'master' || _ => master,
};
}
}
/// A description of the conditions to enable a feature for a particular channel.
@immutable
final class FeatureChannelSetting {
const FeatureChannelSetting({this.available = false, this.enabledByDefault = false});
/// Whether the feature is available on this channel.
///
/// If not provided, defaults to `false`. This implies that the feature
/// cannot be enabled even by the settings below.
final bool available;
/// Whether the feature is enabled by default.
///
/// If not provided, defaults to `false`.
final bool enabledByDefault;
@override
bool operator ==(Object other) {
return other is FeatureChannelSetting &&
available == other.available &&
enabledByDefault == other.enabledByDefault;
}
@override
int get hashCode => Object.hash(available, enabledByDefault);
@override
String toString() {
return 'FeatureChannelSetting <available: $available, enabledByDefault: $enabledByDefault>';
}
}