diff --git a/bin/internal/engine.version b/bin/internal/engine.version index 8adb3670e1487..145557766b515 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -14eb83a7d0bdec5ae6e617b5cbeeb64678d5e7ee +ddc5bb3302544f218599b863586052c0de974ebc diff --git a/bin/internal/fuchsia-linux.version b/bin/internal/fuchsia-linux.version index 1d08bd3ce631c..5904cf6dbfb3b 100644 --- a/bin/internal/fuchsia-linux.version +++ b/bin/internal/fuchsia-linux.version @@ -1 +1 @@ -2t3v0NB99aaHnV-JpXzIpT51setcpDQBFD0bQfGDX0cC +ej74uu6Dtncz7eKatdlEBu3i7wwMvcsDesvWsvbwkD0C diff --git a/bin/internal/fuchsia-mac.version b/bin/internal/fuchsia-mac.version index 191376d214a27..c43422ec6fded 100644 --- a/bin/internal/fuchsia-mac.version +++ b/bin/internal/fuchsia-mac.version @@ -1 +1 @@ -Xbz_BflVsu1gMTrKHzA4IagsrzzbBB8MwMUIuYgG4lUC +_eyzEmI9ppaXY6q-WkhtHU___ZmInvC-T8EOPCAM064C diff --git a/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart b/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart index c25318efede58..706ae3fd51119 100644 --- a/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart +++ b/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart @@ -189,6 +189,23 @@ Future _testBuildIosFramework(Directory projectDir, { bool isModule = fals 'vm_snapshot_data', )); + final String appFrameworkDsymPath = path.join( + outputPath, + mode, + 'App.xcframework', + 'ios-arm64', + 'dSYMs', + 'App.framework.dSYM' + ); + checkDirectoryExists(appFrameworkDsymPath); + await _checkDsym(path.join( + appFrameworkDsymPath, + 'Contents', + 'Resources', + 'DWARF', + 'App', + )); + checkFileExists(path.join( outputPath, mode, @@ -404,6 +421,25 @@ Future _testBuildIosFramework(Directory projectDir, { bool isModule = fals 'App', )); + if (mode != 'Debug') { + final String appFrameworkDsymPath = path.join( + cocoapodsOutputPath, + mode, + 'App.xcframework', + 'ios-arm64', + 'dSYMs', + 'App.framework.dSYM' + ); + checkDirectoryExists(appFrameworkDsymPath); + await _checkDsym(path.join( + appFrameworkDsymPath, + 'Contents', + 'Resources', + 'DWARF', + 'App', + )); + } + if (Directory(path.join( cocoapodsOutputPath, mode, @@ -582,6 +618,23 @@ Future _testBuildMacOSFramework(Directory projectDir) async { 'Resources', 'Info.plist', )); + + final String appFrameworkDsymPath = path.join( + outputPath, + mode, + 'App.xcframework', + 'macos-arm64_x86_64', + 'dSYMs', + 'App.framework.dSYM' + ); + checkDirectoryExists(appFrameworkDsymPath); + await _checkDsym(path.join( + appFrameworkDsymPath, + 'Contents', + 'Resources', + 'DWARF', + 'App', + )); } section("Check all modes' engine dylib"); @@ -712,6 +765,25 @@ Future _testBuildMacOSFramework(Directory projectDir) async { 'App', )); + if (mode != 'Debug') { + final String appFrameworkDsymPath = path.join( + cocoapodsOutputPath, + mode, + 'App.xcframework', + 'macos-arm64_x86_64', + 'dSYMs', + 'App.framework.dSYM' + ); + checkDirectoryExists(appFrameworkDsymPath); + await _checkDsym(path.join( + appFrameworkDsymPath, + 'Contents', + 'Resources', + 'DWARF', + 'App', + )); + } + await _checkStatic(path.join( cocoapodsOutputPath, mode, @@ -750,6 +822,13 @@ Future _checkDylib(String pathToLibrary) async { } } +Future _checkDsym(String pathToSymbolFile) async { + final String binaryFileType = await fileType(pathToSymbolFile); + if (!binaryFileType.contains('dSYM companion file')) { + throw TaskResult.failure('$pathToSymbolFile is not a dSYM, found: $binaryFileType'); + } +} + Future _checkStatic(String pathToLibrary) async { final String binaryFileType = await fileType(pathToLibrary); if (!binaryFileType.contains('current ar archive random library')) { diff --git a/dev/devicelab/bin/tasks/module_test_ios.dart b/dev/devicelab/bin/tasks/module_test_ios.dart index dc230fd01c8c9..54d883d4d0918 100644 --- a/dev/devicelab/bin/tasks/module_test_ios.dart +++ b/dev/devicelab/bin/tasks/module_test_ios.dart @@ -440,6 +440,16 @@ end 'Frameworks', 'url_launcher_ios.framework', )); + + checkFileExists(path.join( + '${objectiveCBuildArchiveDirectory.path}.xcarchive', + 'dSYMs', + 'App.framework.dSYM', + 'Contents', + 'Resources', + 'DWARF', + 'App' + )); }); section('Run platform unit tests'); diff --git a/dev/tools/gen_defaults/bin/gen_defaults.dart b/dev/tools/gen_defaults/bin/gen_defaults.dart index 71868770684c5..659cec641f918 100644 --- a/dev/tools/gen_defaults/bin/gen_defaults.dart +++ b/dev/tools/gen_defaults/bin/gen_defaults.dart @@ -17,16 +17,17 @@ import 'dart:convert'; import 'dart:io'; +import 'package:gen_defaults/action_chip_template.dart'; import 'package:gen_defaults/app_bar_template.dart'; +import 'package:gen_defaults/banner_template.dart'; import 'package:gen_defaults/button_template.dart'; import 'package:gen_defaults/card_template.dart'; import 'package:gen_defaults/checkbox_template.dart'; -import 'package:gen_defaults/chip_action_template.dart'; -import 'package:gen_defaults/chip_filter_template.dart'; -import 'package:gen_defaults/chip_input_template.dart'; import 'package:gen_defaults/dialog_template.dart'; import 'package:gen_defaults/fab_template.dart'; +import 'package:gen_defaults/filter_chip_template.dart'; import 'package:gen_defaults/icon_button_template.dart'; +import 'package:gen_defaults/input_chip_template.dart'; import 'package:gen_defaults/input_decorator_template.dart'; import 'package:gen_defaults/navigation_bar_template.dart'; import 'package:gen_defaults/navigation_rail_template.dart'; @@ -103,6 +104,7 @@ Future main(List args) async { tokens['colorsDark'] = _readTokenFile('color_dark.json'); AppBarTemplate('AppBar', '$materialLib/app_bar.dart', tokens).updateFile(); + BannerTemplate('Banner', '$materialLib/banner.dart', tokens).updateFile(); ButtonTemplate('md.comp.elevated-button', 'ElevatedButton', '$materialLib/elevated_button.dart', tokens).updateFile(); ButtonTemplate('md.comp.filled-button', 'FilledButton', '$materialLib/filled_button.dart', tokens).updateFile(); ButtonTemplate('md.comp.filled-tonal-button', 'FilledTonalButton', '$materialLib/filled_button.dart', tokens).updateFile(); @@ -110,10 +112,10 @@ Future main(List args) async { ButtonTemplate('md.comp.text-button', 'TextButton', '$materialLib/text_button.dart', tokens).updateFile(); CardTemplate('Card', '$materialLib/card.dart', tokens).updateFile(); CheckboxTemplate('Checkbox', '$materialLib/checkbox.dart', tokens).updateFile(); - ChipActionTemplate('ActionChip', '$materialLib/chip_action.dart', tokens).updateFile(); - ChipFilterTemplate('FilterChip', '$materialLib/chip_filter.dart', tokens).updateFile(); - ChipFilterTemplate('FilterChip', '$materialLib/chip_choice.dart', tokens).updateFile(); - ChipInputTemplate('InputChip', '$materialLib/chip_input.dart', tokens).updateFile(); + ChipActionTemplate('ActionChip', '$materialLib/action_chip.dart', tokens).updateFile(); + ChipFilterTemplate('FilterChip', '$materialLib/filter_chip.dart', tokens).updateFile(); + ChipFilterTemplate('FilterChip', '$materialLib/choice_chip.dart', tokens).updateFile(); + ChipInputTemplate('InputChip', '$materialLib/input_chip.dart', tokens).updateFile(); DialogTemplate('Dialog', '$materialLib/dialog.dart', tokens).updateFile(); FABTemplate('FAB', '$materialLib/floating_action_button.dart', tokens).updateFile(); IconButtonTemplate('IconButton', '$materialLib/icon_button.dart', tokens).updateFile(); diff --git a/dev/tools/gen_defaults/lib/chip_action_template.dart b/dev/tools/gen_defaults/lib/action_chip_template.dart similarity index 100% rename from dev/tools/gen_defaults/lib/chip_action_template.dart rename to dev/tools/gen_defaults/lib/action_chip_template.dart diff --git a/dev/tools/gen_defaults/lib/banner_template.dart b/dev/tools/gen_defaults/lib/banner_template.dart new file mode 100644 index 0000000000000..123c8ec103431 --- /dev/null +++ b/dev/tools/gen_defaults/lib/banner_template.dart @@ -0,0 +1,31 @@ +// 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. + +import 'template.dart'; + +class BannerTemplate extends TokenTemplate { + const BannerTemplate(super.blockName, super.fileName, super.tokens); + + @override + String generate() => ''' +class _${blockName}DefaultsM3 extends MaterialBannerThemeData { + const _${blockName}DefaultsM3(this.context) + : super(elevation: ${elevation("md.comp.banner.container")}); + + final BuildContext context; + + @override + Color? get backgroundColor => ${componentColor("md.comp.banner.container")}; + + @override + Color? get surfaceTintColor => ${color("md.comp.banner.container.surface-tint-layer.color")}; + + @override + Color? get dividerColor => ${color("md.comp.banner.divider.color")}; + + @override + TextStyle? get contentTextStyle => ${textStyle("md.comp.banner.supporting-text")}; +} +'''; +} diff --git a/dev/tools/gen_defaults/lib/chip_filter_template.dart b/dev/tools/gen_defaults/lib/filter_chip_template.dart similarity index 100% rename from dev/tools/gen_defaults/lib/chip_filter_template.dart rename to dev/tools/gen_defaults/lib/filter_chip_template.dart diff --git a/dev/tools/gen_defaults/lib/chip_input_template.dart b/dev/tools/gen_defaults/lib/input_chip_template.dart similarity index 100% rename from dev/tools/gen_defaults/lib/chip_input_template.dart rename to dev/tools/gen_defaults/lib/input_chip_template.dart diff --git a/packages/flutter/lib/src/material/banner.dart b/packages/flutter/lib/src/material/banner.dart index 6eede2df7cb1a..83849d048901d 100644 --- a/packages/flutter/lib/src/material/banner.dart +++ b/packages/flutter/lib/src/material/banner.dart @@ -102,12 +102,15 @@ class MaterialBanner extends StatefulWidget { this.elevation, this.leading, this.backgroundColor, + this.surfaceTintColor, + this.shadowColor, + this.dividerColor, this.padding, this.leadingPadding, this.forceActionsBelow = false, this.overflowAlignment = OverflowBarAlignment.end, this.animation, - this.onVisible + this.onVisible, }) : assert(elevation == null || elevation >= 0.0), assert(content != null), assert(actions != null), @@ -153,6 +156,29 @@ class MaterialBanner extends StatefulWidget { /// also `null`, [ColorScheme.surface] of [ThemeData.colorScheme] is used. final Color? backgroundColor; + /// The color used as an overlay on [backgroundColor] to indicate elevation. + /// + /// If null, [MaterialBannerThemeData.surfaceTintColor] is used. If that + /// is also null, the default value is [ColorScheme.surfaceTint]. + /// + /// See [Material.surfaceTintColor] for more details on how this + /// overlay is applied. + final Color? surfaceTintColor; + + /// The color of the shadow below the [MaterialBanner]. + /// + /// If this property is null, then [MaterialBannerThemeData.shadowColor] of + /// [ThemeData.bannerTheme] is used. If that is also null, the default value + /// is null. + final Color? shadowColor; + + /// The color of the divider. + /// + /// If this property is null, then [MaterialBannerThemeData.dividerColor] of + /// [ThemeData.bannerTheme] is used. If that is also null, the default value + /// is [ColorScheme.surfaceVariant]. + final Color? dividerColor; + /// The amount of space by which to inset the [content]. /// /// If the [actions] are below the [content], this defaults to @@ -273,6 +299,7 @@ class _MaterialBannerState extends State { final ThemeData theme = Theme.of(context); final MaterialBannerThemeData bannerTheme = MaterialBannerTheme.of(context); + final MaterialBannerThemeData defaults = theme.useMaterial3 ? _BannerDefaultsM3(context) : _BannerDefaultsM2(context); final bool isSingleRow = widget.actions.length == 1 && !widget.forceActionsBelow; final EdgeInsetsGeometry padding = widget.padding ?? bannerTheme.padding ?? (isSingleRow @@ -296,16 +323,26 @@ class _MaterialBannerState extends State { final double elevation = widget.elevation ?? bannerTheme.elevation ?? 0.0; final Color backgroundColor = widget.backgroundColor ?? bannerTheme.backgroundColor - ?? theme.colorScheme.surface; + ?? defaults.backgroundColor!; + final Color? surfaceTintColor = widget.surfaceTintColor + ?? bannerTheme.surfaceTintColor + ?? defaults.surfaceTintColor; + final Color? shadowColor = widget.shadowColor + ?? bannerTheme.shadowColor; + final Color? dividerColor = widget.dividerColor + ?? bannerTheme.dividerColor + ?? defaults.dividerColor; final TextStyle? textStyle = widget.contentTextStyle ?? bannerTheme.contentTextStyle - ?? theme.textTheme.bodyMedium; + ?? defaults.contentTextStyle; Widget materialBanner = Container( margin: EdgeInsets.only(bottom: elevation > 0 ? 10.0 : 0.0), child: Material( elevation: elevation, color: backgroundColor, + surfaceTintColor: surfaceTintColor, + shadowColor: shadowColor, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -331,9 +368,8 @@ class _MaterialBannerState extends State { ), if (!isSingleRow) buttonBar, - if (elevation == 0) - const Divider(height: 0), + Divider(height: 0, color: dividerColor), ], ), ), @@ -394,3 +430,51 @@ class _MaterialBannerState extends State { ); } } + +class _BannerDefaultsM2 extends MaterialBannerThemeData { + _BannerDefaultsM2(this.context) + : _theme = Theme.of(context), + super(elevation: 0.0); + + final BuildContext context; + final ThemeData _theme; + + @override + Color? get backgroundColor => _theme.colorScheme.surface; + + @override + Color? get dividerColor => Theme.of(context).colorScheme.surfaceVariant; + + @override + TextStyle? get contentTextStyle => _theme.textTheme.bodyText2; +} + +// BEGIN GENERATED TOKEN PROPERTIES - Banner + +// Do not edit by hand. The code between the "BEGIN GENERATED" and +// "END GENERATED" comments are generated from data in the Material +// Design token database by the script: +// dev/tools/gen_defaults/bin/gen_defaults.dart. + +// Token database version: v0_101 + +class _BannerDefaultsM3 extends MaterialBannerThemeData { + const _BannerDefaultsM3(this.context) + : super(elevation: 1.0); + + final BuildContext context; + + @override + Color? get backgroundColor => Theme.of(context).colorScheme.surface; + + @override + Color? get surfaceTintColor => Theme.of(context).colorScheme.surfaceTint; + + @override + Color? get dividerColor => Theme.of(context).colorScheme.surfaceVariant; + + @override + TextStyle? get contentTextStyle => Theme.of(context).textTheme.bodyMedium; +} + +// END GENERATED TOKEN PROPERTIES - Banner diff --git a/packages/flutter/lib/src/material/banner_theme.dart b/packages/flutter/lib/src/material/banner_theme.dart index 5081f8825e783..8b0b183c6cd7f 100644 --- a/packages/flutter/lib/src/material/banner_theme.dart +++ b/packages/flutter/lib/src/material/banner_theme.dart @@ -35,6 +35,9 @@ class MaterialBannerThemeData with Diagnosticable { /// [ThemeData.bannerTheme]. const MaterialBannerThemeData({ this.backgroundColor, + this.surfaceTintColor, + this.shadowColor, + this.dividerColor, this.contentTextStyle, this.elevation, this.padding, @@ -44,6 +47,15 @@ class MaterialBannerThemeData with Diagnosticable { /// The background color of a [MaterialBanner]. final Color? backgroundColor; + /// Overrides the default value of [MaterialBanner.surfaceTintColor]. + final Color? surfaceTintColor; + + /// Overrides the default value of [MaterialBanner.shadowColor]. + final Color? shadowColor; + + /// Overrides the default value of [MaterialBanner.dividerColor]. + final Color? dividerColor; + /// Used to configure the [DefaultTextStyle] for the [MaterialBanner.content] /// widget. final TextStyle? contentTextStyle; @@ -63,6 +75,9 @@ class MaterialBannerThemeData with Diagnosticable { /// new values. MaterialBannerThemeData copyWith({ Color? backgroundColor, + Color? surfaceTintColor, + Color? shadowColor, + Color? dividerColor, TextStyle? contentTextStyle, double? elevation, EdgeInsetsGeometry? padding, @@ -70,6 +85,9 @@ class MaterialBannerThemeData with Diagnosticable { }) { return MaterialBannerThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, + surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor, + shadowColor: shadowColor ?? this.shadowColor, + dividerColor: dividerColor ?? this.dividerColor, contentTextStyle: contentTextStyle ?? this.contentTextStyle, elevation: elevation ?? this.elevation, padding: padding ?? this.padding, @@ -86,6 +104,9 @@ class MaterialBannerThemeData with Diagnosticable { assert(t != null); return MaterialBannerThemeData( backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t), + surfaceTintColor: Color.lerp(a?.surfaceTintColor, b?.surfaceTintColor, t), + shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t), + dividerColor: Color.lerp(a?.dividerColor, b?.dividerColor, t), contentTextStyle: TextStyle.lerp(a?.contentTextStyle, b?.contentTextStyle, t), elevation: lerpDouble(a?.elevation, b?.elevation, t), padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t), @@ -96,6 +117,9 @@ class MaterialBannerThemeData with Diagnosticable { @override int get hashCode => Object.hash( backgroundColor, + surfaceTintColor, + shadowColor, + dividerColor, contentTextStyle, elevation, padding, @@ -111,19 +135,25 @@ class MaterialBannerThemeData with Diagnosticable { return false; } return other is MaterialBannerThemeData - && other.backgroundColor == backgroundColor - && other.contentTextStyle == contentTextStyle - && other.elevation == elevation - && other.padding == padding - && other.leadingPadding == leadingPadding; + && other.backgroundColor == backgroundColor + && other.surfaceTintColor == surfaceTintColor + && other.shadowColor == shadowColor + && other.dividerColor == dividerColor + && other.contentTextStyle == contentTextStyle + && other.elevation == elevation + && other.padding == padding + && other.leadingPadding == leadingPadding; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: null)); + properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null)); + properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null)); + properties.add(ColorProperty('dividerColor', dividerColor, defaultValue: null)); properties.add(DiagnosticsProperty('contentTextStyle', contentTextStyle, defaultValue: null)); - properties.add(DiagnosticsProperty('elevation', elevation, defaultValue: null)); + properties.add(DoubleProperty('elevation', elevation, defaultValue: null)); properties.add(DiagnosticsProperty('padding', padding, defaultValue: null)); properties.add(DiagnosticsProperty('leadingPadding', leadingPadding, defaultValue: null)); } diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index 9ae0df7d38c4b..cc5ec41436899 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -593,7 +593,13 @@ class ListTile extends StatelessWidget { return selectedColor ?? tileTheme.selectedColor ?? theme.listTileTheme.selectedColor ?? theme.colorScheme.primary; } - final Color? color = iconColor ?? tileTheme.iconColor ?? theme.listTileTheme.iconColor; + final Color? color = iconColor + ?? tileTheme.iconColor + ?? theme.listTileTheme.iconColor + // If [ThemeData.useMaterial3] is set to true the disabled icon color + // will be set to Theme.colorScheme.onSurface(0.38), if false, defaults to null, + // as described in: https://m3.material.io/components/icon-buttons/specs. + ?? (theme.useMaterial3 ? theme.colorScheme.onSurface.withOpacity(0.38) : null); if (color != null) { return color; } diff --git a/packages/flutter/test/material/banner_theme_test.dart b/packages/flutter/test/material/banner_theme_test.dart index 258966ad4fcc4..db351f9ad9699 100644 --- a/packages/flutter/test/material/banner_theme_test.dart +++ b/packages/flutter/test/material/banner_theme_test.dart @@ -15,7 +15,13 @@ void main() { test('MaterialBannerThemeData null fields by default', () { const MaterialBannerThemeData bannerTheme = MaterialBannerThemeData(); expect(bannerTheme.backgroundColor, null); + expect(bannerTheme.surfaceTintColor, null); + expect(bannerTheme.shadowColor, null); + expect(bannerTheme.dividerColor, null); expect(bannerTheme.contentTextStyle, null); + expect(bannerTheme.elevation, null); + expect(bannerTheme.padding, null); + expect(bannerTheme.leadingPadding, null); }); testWidgets('Default MaterialBannerThemeData debugFillProperties', (WidgetTester tester) async { @@ -23,9 +29,9 @@ void main() { const MaterialBannerThemeData().debugFillProperties(builder); final List description = builder.properties - .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) - .map((DiagnosticsNode node) => node.toString()) - .toList(); + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); expect(description, []); }); @@ -33,27 +39,43 @@ void main() { testWidgets('MaterialBannerThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const MaterialBannerThemeData( - backgroundColor: Color(0xFFFFFFFF), - contentTextStyle: TextStyle(color: Color(0xFFFFFFFF)), + backgroundColor: Color(0xfffffff0), + surfaceTintColor: Color(0xfffffff1), + shadowColor: Color(0xfffffff2), + dividerColor: Color(0xfffffff3), + contentTextStyle: TextStyle(color: Color(0xfffffff4)), + elevation: 4.0, + padding: EdgeInsets.all(20.0), + leadingPadding: EdgeInsets.only(left: 8.0), ).debugFillProperties(builder); final List description = builder.properties - .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) - .map((DiagnosticsNode node) => node.toString()) - .toList(); + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); expect(description, [ - 'backgroundColor: Color(0xffffffff)', - 'contentTextStyle: TextStyle(inherit: true, color: Color(0xffffffff))', + 'backgroundColor: Color(0xfffffff0)', + 'surfaceTintColor: Color(0xfffffff1)', + 'shadowColor: Color(0xfffffff2)', + 'dividerColor: Color(0xfffffff3)', + 'contentTextStyle: TextStyle(inherit: true, color: Color(0xfffffff4))', + 'elevation: 4.0', + 'padding: EdgeInsets.all(20.0)', + 'leadingPadding: EdgeInsets(8.0, 0.0, 0.0, 0.0)', ]); }); testWidgets('Passing no MaterialBannerThemeData returns defaults', (WidgetTester tester) async { + final ThemeData theme = ThemeData(); const String contentText = 'Content'; + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true), home: Scaffold( body: MaterialBanner( content: const Text(contentText), + leading: const Icon(Icons.umbrella), actions: [ TextButton( child: const Text('Action'), @@ -65,16 +87,39 @@ void main() { )); final Material material = _getMaterialFromText(tester, contentText); - final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); expect(material.color, const Color(0xffffffff)); - // Default value for ThemeData.typography is Typography.material2014() - expect(content.text.style, Typography.material2014().englishLike.bodyMedium!.merge(Typography.material2014().black.bodyMedium)); + expect(material.surfaceTintColor, theme.colorScheme.surfaceTint); + expect(material.shadowColor, null); + expect(material.elevation, 0.0); + + final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); + // Default value for ThemeData.typography is Typography.material2021() + expect( + content.text.style, + Typography.material2021().englishLike.bodyMedium!.merge( + Typography.material2021().black.bodyMedium, + ), + ); + + final Offset rowTopLeft = tester.getTopLeft(find.byType(Row)); + final Offset materialTopLeft = tester.getTopLeft(_materialFinder()); + final Offset leadingTopLeft = tester.getTopLeft(find.byIcon(Icons.umbrella)); + expect(rowTopLeft.dy - materialTopLeft.dy, 2.0); // Default single line top padding. + expect(rowTopLeft.dx - materialTopLeft.dx, 16.0); // Default single line start padding. + expect(leadingTopLeft.dy - materialTopLeft.dy, 16); // Default leading padding. + expect(leadingTopLeft.dx - materialTopLeft.dx, 16); // Default leading padding. + + final Divider divider = tester.widget(find.byType(Divider)); + expect(divider.color, theme.colorScheme.surfaceVariant); }); testWidgets('Passing no MaterialBannerThemeData returns defaults when presented by ScaffoldMessenger', (WidgetTester tester) async { + final ThemeData theme = ThemeData(); const String contentText = 'Content'; const Key tapTarget = Key('tap-target'); + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true), home: Scaffold( body: Builder( builder: (BuildContext context) { @@ -83,6 +128,7 @@ void main() { onTap: () { ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner( content: const Text(contentText), + leading: const Icon(Icons.umbrella), actions: [ TextButton( child: const Text('Action'), @@ -105,10 +151,30 @@ void main() { await tester.pumpAndSettle(); final Material material = _getMaterialFromText(tester, contentText); - final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); expect(material.color, const Color(0xffffffff)); - // Default value for ThemeData.typography is Typography.material2014() - expect(content.text.style, Typography.material2014().englishLike.bodyMedium!.merge(Typography.material2014().black.bodyMedium)); + expect(material.surfaceTintColor, theme.colorScheme.surfaceTint); + expect(material.shadowColor, null); + expect(material.elevation, 0.0); + + final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); + // Default value for ThemeData.typography is Typography.material2021() + expect( + content.text.style, + Typography.material2021().englishLike.bodyMedium!.merge( + Typography.material2021().black.bodyMedium, + ), + ); + + final Offset rowTopLeft = tester.getTopLeft(find.byType(Row)); + final Offset materialTopLeft = tester.getTopLeft(_materialFinder()); + final Offset leadingTopLeft = tester.getTopLeft(find.byIcon(Icons.umbrella)); + expect(rowTopLeft.dy - materialTopLeft.dy, 2.0); // Default single line top padding. + expect(rowTopLeft.dx - materialTopLeft.dx, 16.0); // Default single line start padding. + expect(leadingTopLeft.dy - materialTopLeft.dy, 16); // Default leading padding. + expect(leadingTopLeft.dx - materialTopLeft.dx, 16); // Default leading padding. + + final Divider divider = tester.widget(find.byType(Divider)); + expect(divider.color, theme.colorScheme.surfaceVariant); }); testWidgets('MaterialBanner uses values from MaterialBannerThemeData', (WidgetTester tester) async { @@ -131,8 +197,12 @@ void main() { )); final Material material = _getMaterialFromText(tester, contentText); - final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); expect(material.color, bannerTheme.backgroundColor); + expect(material.surfaceTintColor, bannerTheme.surfaceTintColor); + expect(material.shadowColor, bannerTheme.shadowColor); + expect(material.elevation, bannerTheme.elevation); + + final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); expect(content.text.style, bannerTheme.contentTextStyle); final Offset contentTopLeft = tester.getTopLeft(_textFinder(contentText)); @@ -142,6 +212,8 @@ void main() { expect(contentTopLeft.dx - materialTopLeft.dx, 41); expect(leadingTopLeft.dy - materialTopLeft.dy, 19); expect(leadingTopLeft.dx - materialTopLeft.dx, 11); + + expect(find.byType(Divider), findsNothing); }); testWidgets('MaterialBanner uses values from MaterialBannerThemeData when presented by ScaffoldMessenger', (WidgetTester tester) async { @@ -181,8 +253,12 @@ void main() { await tester.pumpAndSettle(); final Material material = _getMaterialFromText(tester, contentText); - final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); expect(material.color, bannerTheme.backgroundColor); + expect(material.surfaceTintColor, bannerTheme.surfaceTintColor); + expect(material.shadowColor, bannerTheme.shadowColor); + expect(material.elevation, bannerTheme.elevation); + + final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); expect(content.text.style, bannerTheme.contentTextStyle); final Offset contentTopLeft = tester.getTopLeft(_textFinder(contentText)); @@ -192,18 +268,26 @@ void main() { expect(contentTopLeft.dx - materialTopLeft.dx, 41); expect(leadingTopLeft.dy - materialTopLeft.dy, 19); expect(leadingTopLeft.dx - materialTopLeft.dx, 11); + + expect(find.byType(Divider), findsNothing); }); testWidgets('MaterialBanner widget properties take priority over theme', (WidgetTester tester) async { const Color backgroundColor = Colors.purple; + const Color surfaceTintColor = Colors.red; + const Color shadowColor = Colors.orange; const TextStyle textStyle = TextStyle(color: Colors.green); final MaterialBannerThemeData bannerTheme = _bannerTheme(); const String contentText = 'Content'; + await tester.pumpWidget(MaterialApp( theme: ThemeData(bannerTheme: bannerTheme), home: Scaffold( body: MaterialBanner( backgroundColor: backgroundColor, + surfaceTintColor: surfaceTintColor, + shadowColor: shadowColor, + elevation: 6.0, leading: const Icon(Icons.ac_unit), contentTextStyle: textStyle, content: const Text(contentText), @@ -220,8 +304,12 @@ void main() { )); final Material material = _getMaterialFromText(tester, contentText); - final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); expect(material.color, backgroundColor); + expect(material.surfaceTintColor, surfaceTintColor); + expect(material.shadowColor, shadowColor); + expect(material.elevation, 6.0); + + final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); expect(content.text.style, textStyle); final Offset contentTopLeft = tester.getTopLeft(_textFinder(contentText)); @@ -231,14 +319,18 @@ void main() { expect(contentTopLeft.dx - materialTopLeft.dx, 58); expect(leadingTopLeft.dy - materialTopLeft.dy, 24); expect(leadingTopLeft.dx - materialTopLeft.dx, 22); + + expect(find.byType(Divider), findsNothing); }); testWidgets('MaterialBanner widget properties take priority over theme when presented by ScaffoldMessenger', (WidgetTester tester) async { const Color backgroundColor = Colors.purple; + const double elevation = 6.0; const TextStyle textStyle = TextStyle(color: Colors.green); final MaterialBannerThemeData bannerTheme = _bannerTheme(); const String contentText = 'Content'; const Key tapTarget = Key('tap-target'); + await tester.pumpWidget(MaterialApp( theme: ThemeData(bannerTheme: bannerTheme), home: Scaffold( @@ -249,6 +341,7 @@ void main() { onTap: () { ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner( backgroundColor: backgroundColor, + elevation: elevation, leading: const Icon(Icons.ac_unit), contentTextStyle: textStyle, content: const Text(contentText), @@ -276,8 +369,10 @@ void main() { await tester.pumpAndSettle(); final Material material = _getMaterialFromText(tester, contentText); - final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); expect(material.color, backgroundColor); + expect(material.elevation, elevation); + + final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); expect(content.text.style, textStyle); final Offset contentTopLeft = tester.getTopLeft(_textFinder(contentText)); @@ -287,6 +382,8 @@ void main() { expect(contentTopLeft.dx - materialTopLeft.dx, 58); expect(leadingTopLeft.dy - materialTopLeft.dy, 24); expect(leadingTopLeft.dx - materialTopLeft.dx, 22); + + expect(find.byType(Divider), findsNothing); }); testWidgets('MaterialBanner uses color scheme when necessary', (WidgetTester tester) async { @@ -349,12 +446,130 @@ void main() { final Material material = _getMaterialFromText(tester, contentText); expect(material.color, colorScheme.surface); }); + + group('Material 2', () { + // Tests that are only relevant for Material 2. Once ThemeData.useMaterial3 + // is turned on by default, these tests can be removed. + + testWidgets('Passing no MaterialBannerThemeData returns defaults', (WidgetTester tester) async { + final ThemeData theme = ThemeData(); + const String contentText = 'Content'; + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: MaterialBanner( + content: const Text(contentText), + leading: const Icon(Icons.umbrella), + actions: [ + TextButton( + child: const Text('Action'), + onPressed: () { }, + ), + ], + ), + ), + )); + + final Material material = _getMaterialFromText(tester, contentText); + expect(material.color, const Color(0xffffffff)); + expect(material.surfaceTintColor, null); + expect(material.shadowColor, null); + expect(material.elevation, 0.0); + + final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); + // Default value for ThemeData.typography is Typography.material2014() + expect( + content.text.style, + Typography.material2014().englishLike.bodyText2!.merge( + Typography.material2014().black.bodyText2, + ), + ); + + final Offset rowTopLeft = tester.getTopLeft(find.byType(Row)); + final Offset materialTopLeft = tester.getTopLeft(_materialFinder()); + final Offset leadingTopLeft = tester.getTopLeft(find.byIcon(Icons.umbrella)); + expect(rowTopLeft.dy - materialTopLeft.dy, 2.0); // Default single line top padding. + expect(rowTopLeft.dx - materialTopLeft.dx, 16.0); // Default single line start padding. + expect(leadingTopLeft.dy - materialTopLeft.dy, 16); // Default leading padding. + expect(leadingTopLeft.dx - materialTopLeft.dx, 16); // Default leading padding. + + final Divider divider = tester.widget(find.byType(Divider)); + expect(divider.color, theme.colorScheme.surfaceVariant); + }); + + testWidgets('Passing no MaterialBannerThemeData returns defaults when presented by ScaffoldMessenger', (WidgetTester tester) async { + final ThemeData theme = ThemeData(); + const String contentText = 'Content'; + const Key tapTarget = Key('tap-target'); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + key: tapTarget, + onTap: () { + ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner( + content: const Text(contentText), + leading: const Icon(Icons.umbrella), + actions: [ + TextButton( + child: const Text('Action'), + onPressed: () { }, + ), + ], + )); + }, + behavior: HitTestBehavior.opaque, + child: const SizedBox( + height: 100.0, + width: 100.0, + ), + ); + }, + ), + ), + )); + await tester.tap(find.byKey(tapTarget)); + await tester.pumpAndSettle(); + + final Material material = _getMaterialFromText(tester, contentText); + expect(material.color, const Color(0xffffffff)); + expect(material.surfaceTintColor, null); + expect(material.shadowColor, null); + expect(material.elevation, 0.0); + + final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); + // Default value for ThemeData.typography is Typography.material2014() + expect( + content.text.style, + Typography.material2014().englishLike.bodyText2!.merge( + Typography.material2014().black.bodyText2, + ), + ); + + final Offset rowTopLeft = tester.getTopLeft(find.byType(Row)); + final Offset materialTopLeft = tester.getTopLeft(_materialFinder()); + final Offset leadingTopLeft = tester.getTopLeft(find.byIcon(Icons.umbrella)); + expect(rowTopLeft.dy - materialTopLeft.dy, 2.0); // Default single line top padding. + expect(rowTopLeft.dx - materialTopLeft.dx, 16.0); // Default single line start padding. + expect(leadingTopLeft.dy - materialTopLeft.dy, 16); // Default leading padding. + expect(leadingTopLeft.dx - materialTopLeft.dx, 16); // Default leading padding. + + final Divider divider = tester.widget(find.byType(Divider)); + expect(divider.color, theme.colorScheme.surfaceVariant); + }); + }); } MaterialBannerThemeData _bannerTheme() { return const MaterialBannerThemeData( backgroundColor: Colors.orange, + surfaceTintColor: Colors.yellow, + shadowColor: Colors.red, + dividerColor: Colors.green, contentTextStyle: TextStyle(color: Colors.pink), + elevation: 4.0, padding: EdgeInsets.all(5), leadingPadding: EdgeInsets.all(6), ); diff --git a/packages/flutter/test/material/list_tile_test.dart b/packages/flutter/test/material/list_tile_test.dart index d9a0790ec3a2d..2066f784d825b 100644 --- a/packages/flutter/test/material/list_tile_test.dart +++ b/packages/flutter/test/material/list_tile_test.dart @@ -2037,8 +2037,6 @@ void main() { }); testWidgets('selected, enabled ListTile default icon color, light and dark themes', (WidgetTester tester) async { - // Regression test for https://github.com/flutter/flutter/pull/77004 - const ColorScheme lightColorScheme = ColorScheme.light(); const ColorScheme darkColorScheme = ColorScheme.dark(); final Key leadingKey = UniqueKey(); @@ -2048,8 +2046,8 @@ void main() { Widget buildFrame({ required Brightness brightness, required bool selected }) { final ThemeData theme = brightness == Brightness.light - ? ThemeData.from(colorScheme: const ColorScheme.light()) - : ThemeData.from(colorScheme: const ColorScheme.dark()); + ? ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true) + : ThemeData.from(colorScheme: const ColorScheme.dark(), useMaterial3: true); return MaterialApp( theme: theme, home: Material( @@ -2075,10 +2073,10 @@ void main() { expect(iconColor(trailingKey), lightColorScheme.primary); await tester.pumpWidget(buildFrame(brightness: Brightness.light, selected: false)); - expect(iconColor(leadingKey), Colors.black45); - expect(iconColor(titleKey), Colors.black45); - expect(iconColor(subtitleKey), Colors.black45); - expect(iconColor(trailingKey), Colors.black45); + expect(iconColor(leadingKey), lightColorScheme.onSurface.withOpacity(0.38)); + expect(iconColor(titleKey), lightColorScheme.onSurface.withOpacity(0.38)); + expect(iconColor(subtitleKey), lightColorScheme.onSurface.withOpacity(0.38)); + expect(iconColor(trailingKey), lightColorScheme.onSurface.withOpacity(0.38)); await tester.pumpWidget(buildFrame(brightness: Brightness.dark, selected: true)); await tester.pumpAndSettle(); // Animated theme change @@ -2090,10 +2088,10 @@ void main() { // For this configuration, ListTile defers to the default IconTheme. // The default dark theme's IconTheme has color:white await tester.pumpWidget(buildFrame(brightness: Brightness.dark, selected: false)); - expect(iconColor(leadingKey), Colors.white); - expect(iconColor(titleKey), Colors.white); - expect(iconColor(subtitleKey), Colors.white); - expect(iconColor(trailingKey), Colors.white); + expect(iconColor(leadingKey), darkColorScheme.onSurface.withOpacity(0.38)); + expect(iconColor(titleKey), darkColorScheme.onSurface.withOpacity(0.38)); + expect(iconColor(subtitleKey), darkColorScheme.onSurface.withOpacity(0.38)); + expect(iconColor(trailingKey), darkColorScheme.onSurface.withOpacity(0.38)); }); testWidgets('ListTile font size', (WidgetTester tester) async { @@ -2442,6 +2440,66 @@ void main() { trailing = _getTextRenderObject(tester, 'trailing'); expect(trailing.text.style!.color, theme.textTheme.bodyMedium!.color); }); + + testWidgets('selected, enabled ListTile default icon color, light and dark themes', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/pull/77004 + + const ColorScheme lightColorScheme = ColorScheme.light(); + const ColorScheme darkColorScheme = ColorScheme.dark(); + final Key leadingKey = UniqueKey(); + final Key titleKey = UniqueKey(); + final Key subtitleKey = UniqueKey(); + final Key trailingKey = UniqueKey(); + + Widget buildFrame({ required Brightness brightness, required bool selected }) { + final ThemeData theme = brightness == Brightness.light + ? ThemeData.from(colorScheme: const ColorScheme.light()) + : ThemeData.from(colorScheme: const ColorScheme.dark()); + return MaterialApp( + theme: theme, + home: Material( + child: Center( + child: ListTile( + selected: selected, + leading: TestIcon(key: leadingKey), + title: TestIcon(key: titleKey), + subtitle: TestIcon(key: subtitleKey), + trailing: TestIcon(key: trailingKey), + ), + ), + ), + ); + } + + Color iconColor(Key key) => tester.state(find.byKey(key)).iconTheme.color!; + + await tester.pumpWidget(buildFrame(brightness: Brightness.light, selected: true)); + expect(iconColor(leadingKey), lightColorScheme.primary); + expect(iconColor(titleKey), lightColorScheme.primary); + expect(iconColor(subtitleKey), lightColorScheme.primary); + expect(iconColor(trailingKey), lightColorScheme.primary); + + await tester.pumpWidget(buildFrame(brightness: Brightness.light, selected: false)); + expect(iconColor(leadingKey), Colors.black45); + expect(iconColor(titleKey), Colors.black45); + expect(iconColor(subtitleKey), Colors.black45); + expect(iconColor(trailingKey), Colors.black45); + + await tester.pumpWidget(buildFrame(brightness: Brightness.dark, selected: true)); + await tester.pumpAndSettle(); // Animated theme change + expect(iconColor(leadingKey), darkColorScheme.primary); + expect(iconColor(titleKey), darkColorScheme.primary); + expect(iconColor(subtitleKey), darkColorScheme.primary); + expect(iconColor(trailingKey), darkColorScheme.primary); + + // For this configuration, ListTile defers to the default IconTheme. + // The default dark theme's IconTheme has color:white + await tester.pumpWidget(buildFrame(brightness: Brightness.dark, selected: false)); + expect(iconColor(leadingKey), Colors.white); + expect(iconColor(titleKey), Colors.white); + expect(iconColor(subtitleKey), Colors.white); + expect(iconColor(trailingKey), Colors.white); + }); }); } diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart index 96600b4889303..08d326848e304 100644 --- a/packages/flutter_tools/lib/src/base/build.dart +++ b/packages/flutter_tools/lib/src/base/build.dart @@ -139,10 +139,17 @@ class AOTSnapshotter { '--deterministic', ]; + final bool targetingApplePlatform = + platform == TargetPlatform.ios || platform == TargetPlatform.darwin; + _logger.printTrace('targetingApplePlatform = $targetingApplePlatform'); + + final bool extractAppleDebugSymbols = + buildMode == BuildMode.profile || buildMode == BuildMode.release; + _logger.printTrace('extractAppleDebugSymbols = $extractAppleDebugSymbols'); + // We strip snapshot by default, but allow to suppress this behavior // by supplying --no-strip in extraGenSnapshotOptions. bool shouldStrip = true; - if (extraGenSnapshotOptions != null && extraGenSnapshotOptions.isNotEmpty) { _logger.printTrace('Extra gen_snapshot options: $extraGenSnapshotOptions'); for (final String option in extraGenSnapshotOptions) { @@ -168,8 +175,20 @@ class AOTSnapshotter { ]); } - if (shouldStrip) { - genSnapshotArgs.add('--strip'); + // When buiding for iOS and splitting out debug info, we want to strip + // manually after the dSYM export, instead of in the `gen_snapshot`. + final bool stripAfterBuild; + if (targetingApplePlatform) { + stripAfterBuild = shouldStrip; + if (stripAfterBuild) { + _logger.printTrace('Will strip AOT snapshot manual after build and dSYM generation.'); + } + } else { + stripAfterBuild = false; + if (shouldStrip) { + genSnapshotArgs.add('--strip'); + _logger.printTrace('Will strip AOT snapshot during build.'); + } } if (platform == TargetPlatform.android_arm) { @@ -218,8 +237,8 @@ class AOTSnapshotter { // On iOS and macOS, we use Xcode to compile the snapshot into a dynamic library that the // end-developer can link into their app. - if (platform == TargetPlatform.ios || platform == TargetPlatform.darwin) { - final RunResult result = await _buildFramework( + if (targetingApplePlatform) { + return _buildFramework( appleArch: darwinArch!, isIOS: platform == TargetPlatform.ios, sdkRoot: sdkRoot, @@ -227,24 +246,26 @@ class AOTSnapshotter { outputPath: outputDir.path, bitcode: bitcode, quiet: quiet, + stripAfterBuild: stripAfterBuild, + extractAppleDebugSymbols: extractAppleDebugSymbols ); - if (result.exitCode != 0) { - return result.exitCode; - } + } else { + return 0; } - return 0; } /// Builds an iOS or macOS framework at [outputPath]/App.framework from the assembly /// source at [assemblyPath]. - Future _buildFramework({ + Future _buildFramework({ required DarwinArch appleArch, required bool isIOS, String? sdkRoot, required String assemblyPath, required String outputPath, required bool bitcode, - required bool quiet + required bool quiet, + required bool stripAfterBuild, + required bool extractAppleDebugSymbols }) async { final String targetArch = getNameForDarwinArch(appleArch); if (!quiet) { @@ -278,7 +299,7 @@ class AOTSnapshotter { ]); if (compileResult.exitCode != 0) { _logger.printError('Failed to compile AOT snapshot. Compiler terminated with exit code ${compileResult.exitCode}'); - return compileResult; + return compileResult.exitCode; } final String frameworkDir = _fileSystem.path.join(outputPath, 'App.framework'); @@ -294,11 +315,33 @@ class AOTSnapshotter { '-o', appLib, assemblyO, ]; + final RunResult linkResult = await _xcode.clang(linkArgs); if (linkResult.exitCode != 0) { - _logger.printError('Failed to link AOT snapshot. Linker terminated with exit code ${compileResult.exitCode}'); + _logger.printError('Failed to link AOT snapshot. Linker terminated with exit code ${linkResult.exitCode}'); + return linkResult.exitCode; + } + + if (extractAppleDebugSymbols) { + final RunResult dsymResult = await _xcode.dsymutil(['-o', '$frameworkDir.dSYM', appLib]); + if (dsymResult.exitCode != 0) { + _logger.printError('Failed to generate dSYM - dsymutil terminated with exit code ${dsymResult.exitCode}'); + return dsymResult.exitCode; + } + + if (stripAfterBuild) { + // See https://www.unix.com/man-page/osx/1/strip/ for arguments + final RunResult stripResult = await _xcode.strip(['-S', appLib, '-o', appLib]); + if (stripResult.exitCode != 0) { + _logger.printError('Failed to strip debugging symbols from the generated AOT snapshot - strip terminated with exit code ${stripResult.exitCode}'); + return stripResult.exitCode; + } + } + } else { + assert(stripAfterBuild == false); } - return linkResult; + + return 0; } bool _isValidAotPlatform(TargetPlatform platform, BuildMode buildMode) { diff --git a/packages/flutter_tools/lib/src/build_system/targets/common.dart b/packages/flutter_tools/lib/src/build_system/targets/common.dart index 656f1943dfb6f..753fe683e5ba5 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/common.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/common.dart @@ -7,6 +7,7 @@ import 'package:package_config/package_config.dart'; import '../../artifacts.dart'; import '../../base/build.dart'; import '../../base/file_system.dart'; +import '../../base/io.dart'; import '../../build_info.dart'; import '../../compile.dart'; import '../../dart/package_map.dart'; @@ -394,3 +395,48 @@ abstract class CopyFlutterAotBundle extends Target { environment.buildDir.childFile('app.so').copySync(outputFile.path); } } + +/// Lipo CLI tool wrapper shared by iOS and macOS builds. +class Lipo { + /// Static only. + Lipo._(); + + /// Create a "fat" binary by combining multiple architecture-specific ones. + /// `skipMissingInputs` can be changed to `true` to first check whether + /// the expected input paths exist and ignore the command if they don't. + /// Otherwise, `lipo` would fail if the given paths didn't exist. + static Future create( + Environment environment, + List darwinArchs, { + required String relativePath, + required String inputDir, + bool skipMissingInputs = false, + }) async { + + final String resultPath = environment.fileSystem.path.join(environment.buildDir.path, relativePath); + environment.fileSystem.directory(resultPath).parent.createSync(recursive: true); + + Iterable inputPaths = darwinArchs.map( + (DarwinArch iosArch) => environment.fileSystem.path.join(inputDir, getNameForDarwinArch(iosArch), relativePath) + ); + if (skipMissingInputs) { + inputPaths = inputPaths.where(environment.fileSystem.isFileSync); + if (inputPaths.isEmpty) { + return; + } + } + + final List lipoArgs = [ + 'lipo', + ...inputPaths, + '-create', + '-output', + resultPath, + ]; + + final ProcessResult result = await environment.processManager.run(lipoArgs); + if (result.exitCode != 0) { + throw Exception('lipo exited with code ${result.exitCode}.\n${result.stderr}'); + } + } +} diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart index 2eab6847a5f25..2bcb7668cd58f 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -112,19 +112,24 @@ abstract class AotAssemblyBase extends Target { if (results.any((int result) => result != 0)) { throw Exception('AOT snapshotter exited with code ${results.join()}'); } - final String resultPath = environment.fileSystem.path.join(environment.buildDir.path, 'App.framework', 'App'); - environment.fileSystem.directory(resultPath).parent.createSync(recursive: true); - final ProcessResult result = await environment.processManager.run([ - 'lipo', - ...darwinArchs.map((DarwinArch iosArch) => - environment.fileSystem.path.join(buildOutputPath, getNameForDarwinArch(iosArch), 'App.framework', 'App')), - '-create', - '-output', - resultPath, - ]); - if (result.exitCode != 0) { - throw Exception('lipo exited with code ${result.exitCode}.\n${result.stderr}'); - } + + // Combine the app lib into a fat framework. + await Lipo.create( + environment, + darwinArchs, + relativePath: 'App.framework/App', + inputDir: buildOutputPath, + ); + + // And combine the dSYM for each architecture too, if it was created. + await Lipo.create( + environment, + darwinArchs, + relativePath: 'App.framework.dSYM/Contents/Resources/DWARF/App', + inputDir: buildOutputPath, + // Don't fail if the dSYM wasn't created (i.e. during a debug build). + skipMissingInputs: true, + ); } } @@ -489,6 +494,26 @@ abstract class IosAssetBundle extends Target { .copySync(frameworkBinaryPath); } + // Copy the dSYM + if (environment.buildDir.childDirectory('App.framework.dSYM').existsSync()) { + final File dsymOutputBinary = environment + .outputDir + .childDirectory('App.framework.dSYM') + .childDirectory('Contents') + .childDirectory('Resources') + .childDirectory('DWARF') + .childFile('App'); + dsymOutputBinary.parent.createSync(recursive: true); + environment + .buildDir + .childDirectory('App.framework.dSYM') + .childDirectory('Contents') + .childDirectory('Resources') + .childDirectory('DWARF') + .childFile('App') + .copySync(dsymOutputBinary.path); + } + // Copy the assets. final Depfile assetDepfile = await copyAssets( environment, @@ -547,8 +572,25 @@ class DebugIosApplicationBundle extends IosAssetBundle { ]; } +/// IosAssetBundle with debug symbols, used for Profile and Release builds. +abstract class _IosAssetBundleWithDSYM extends IosAssetBundle { + const _IosAssetBundleWithDSYM(); + + @override + List get inputs => [ + ...super.inputs, + const Source.pattern('{BUILD_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'), + ]; + + @override + List get outputs => [ + ...super.outputs, + const Source.pattern('{OUTPUT_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'), + ]; +} + /// Build a profile iOS application bundle. -class ProfileIosApplicationBundle extends IosAssetBundle { +class ProfileIosApplicationBundle extends _IosAssetBundleWithDSYM { const ProfileIosApplicationBundle(); @override @@ -561,7 +603,7 @@ class ProfileIosApplicationBundle extends IosAssetBundle { } /// Build a release iOS application bundle. -class ReleaseIosApplicationBundle extends IosAssetBundle { +class ReleaseIosApplicationBundle extends _IosAssetBundleWithDSYM { const ReleaseIosApplicationBundle(); @override diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart index af08b070ea2f7..e88f883242d36 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -302,19 +302,23 @@ class CompileMacOSFramework extends Target { throw Exception('AOT snapshotter exited with code ${results.join()}'); } - final String resultPath = environment.fileSystem.path.join(environment.buildDir.path, 'App.framework', 'App'); - environment.fileSystem.directory(resultPath).parent.createSync(recursive: true); - final ProcessResult result = await environment.processManager.run([ - 'lipo', - ...darwinArchs.map((DarwinArch iosArch) => - environment.fileSystem.path.join(buildOutputPath, getNameForDarwinArch(iosArch), 'App.framework', 'App')), - '-create', - '-output', - resultPath, - ]); - if (result.exitCode != 0) { - throw Exception('lipo exited with code ${result.exitCode}.\n${result.stderr}'); - } + // Combine the app lib into a fat framework. + await Lipo.create( + environment, + darwinArchs, + relativePath: 'App.framework/App', + inputDir: buildOutputPath, + ); + + // And combine the dSYM for each architecture too, if it was created. + await Lipo.create( + environment, + darwinArchs, + relativePath: 'App.framework.dSYM/Contents/Resources/DWARF/App', + inputDir: buildOutputPath, + // Don't fail if the dSYM wasn't created (i.e. during a debug build). + skipMissingInputs: true, + ); } @override @@ -332,6 +336,7 @@ class CompileMacOSFramework extends Target { @override List get outputs => const [ Source.pattern('{BUILD_DIR}/App.framework/App'), + Source.pattern('{BUILD_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'), ]; } @@ -382,6 +387,26 @@ abstract class MacOSBundleFlutterAssets extends Target { .childFile('App') .copySync(outputDirectory.childFile('App').path); + // Copy the dSYM + if (environment.buildDir.childDirectory('App.framework.dSYM').existsSync()) { + final File dsymOutputBinary = environment + .outputDir + .childDirectory('App.framework.dSYM') + .childDirectory('Contents') + .childDirectory('Resources') + .childDirectory('DWARF') + .childFile('App'); + dsymOutputBinary.parent.createSync(recursive: true); + environment + .buildDir + .childDirectory('App.framework.dSYM') + .childDirectory('Contents') + .childDirectory('Resources') + .childDirectory('DWARF') + .childFile('App') + .copySync(dsymOutputBinary.path); + } + // Copy assets into asset directory. final Directory assetDirectory = outputDirectory .childDirectory('Resources') @@ -530,6 +555,18 @@ class ProfileMacOSBundleFlutterAssets extends MacOSBundleFlutterAssets { CompileMacOSFramework(), ProfileUnpackMacOS(), ]; + + @override + List get inputs => [ + ...super.inputs, + const Source.pattern('{BUILD_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'), + ]; + + @override + List get outputs => [ + ...super.outputs, + const Source.pattern('{OUTPUT_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'), + ]; } @@ -546,6 +583,18 @@ class ReleaseMacOSBundleFlutterAssets extends MacOSBundleFlutterAssets { ReleaseUnpackMacOS(), ]; + @override + List get inputs => [ + ...super.inputs, + const Source.pattern('{BUILD_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'), + ]; + + @override + List get outputs => [ + ...super.outputs, + const Source.pattern('{OUTPUT_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'), + ]; + @override Future build(Environment environment) async { bool buildSuccess = true; diff --git a/packages/flutter_tools/lib/src/commands/symbolize.dart b/packages/flutter_tools/lib/src/commands/symbolize.dart index a4edd6a25bc11..d8393f15d96a8 100644 --- a/packages/flutter_tools/lib/src/commands/symbolize.dart +++ b/packages/flutter_tools/lib/src/commands/symbolize.dart @@ -68,8 +68,11 @@ class SymbolizeCommand extends FlutterCommand { if (argResults?.wasParsed('debug-info') != true) { throwToolExit('"--debug-info" is required to symbolize stack traces.'); } - if (!_fileSystem.isFileSync(stringArgDeprecated('debug-info')!)) { - throwToolExit('${stringArgDeprecated('debug-info')} does not exist.'); + final String debugInfoPath = stringArgDeprecated('debug-info')!; + if (debugInfoPath.endsWith('.dSYM') + ? !_fileSystem.isDirectorySync(debugInfoPath) + : !_fileSystem.isFileSync(debugInfoPath)) { + throwToolExit('$debugInfoPath does not exist.'); } if ((argResults?.wasParsed('input') ?? false) && !_fileSystem.isFileSync(stringArgDeprecated('input')!)) { throwToolExit('${stringArgDeprecated('input')} does not exist.'); @@ -105,7 +108,25 @@ class SymbolizeCommand extends FlutterCommand { input = _stdio.stdin; } - final Uint8List symbols = _fileSystem.file(stringArgDeprecated('debug-info')).readAsBytesSync(); + String debugInfoPath = stringArgDeprecated('debug-info')!; + + // If it's a dSYM container, expand the path to the actual DWARF. + if (debugInfoPath.endsWith('.dSYM')) { + final Directory debugInfoDir = _fileSystem + .directory(debugInfoPath) + .childDirectory('Contents') + .childDirectory('Resources') + .childDirectory('DWARF'); + + final List dwarfFiles = debugInfoDir.listSync().whereType().toList(); + if (dwarfFiles.length == 1) { + debugInfoPath = dwarfFiles.first.path; + } else { + throwToolExit('Expected a single DWARF file in a dSYM container.'); + } + } + + final Uint8List symbols = _fileSystem.file(debugInfoPath).readAsBytesSync(); await _dwarfSymbolizationService.decode( input: input, output: output, diff --git a/packages/flutter_tools/lib/src/macos/xcode.dart b/packages/flutter_tools/lib/src/macos/xcode.dart index fa7212f705920..86c11e49fa8d2 100644 --- a/packages/flutter_tools/lib/src/macos/xcode.dart +++ b/packages/flutter_tools/lib/src/macos/xcode.dart @@ -165,16 +165,17 @@ class Xcode { /// See [XcodeProjectInterpreter.xcrunCommand]. List xcrunCommand() => _xcodeProjectInterpreter.xcrunCommand(); - Future cc(List args) { - return _processUtils.run( - [...xcrunCommand(), 'cc', ...args], - throwOnError: true, - ); - } + Future cc(List args) => _run('cc', args); + + Future clang(List args) => _run('clang', args); + + Future dsymutil(List args) => _run('dsymutil', args); + + Future strip(List args) => _run('strip', args); - Future clang(List args) { + Future _run(String command, List args) { return _processUtils.run( - [...xcrunCommand(), 'clang', ...args], + [...xcrunCommand(), command, ...args], throwOnError: true, ); } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/symbolize_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/symbolize_test.dart index c58c14aab74f8..bd601705c2f2d 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/symbolize_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/symbolize_test.dart @@ -69,7 +69,7 @@ void main() { OutputPreferences: () => OutputPreferences.test(), }); - testUsingContext('symbolize exits when --debug-info file is missing', () async { + testUsingContext('symbolize exits when --debug-info dwarf file is missing', () async { final SymbolizeCommand command = SymbolizeCommand( stdio: stdio, fileSystem: fileSystem, @@ -83,6 +83,20 @@ void main() { OutputPreferences: () => OutputPreferences.test(), }); + testUsingContext('symbolize exits when --debug-info dSYM is missing', () async { + final SymbolizeCommand command = SymbolizeCommand( + stdio: stdio, + fileSystem: fileSystem, + dwarfSymbolizationService: DwarfSymbolizationService.test(), + ); + final Future result = createTestCommandRunner(command) + .run(const ['symbolize', '--debug-info=app.dSYM']); + + expect(result, throwsToolExit(message: 'app.dSYM does not exist.')); + }, overrides: { + OutputPreferences: () => OutputPreferences.test(), + }); + testUsingContext('symbolize exits when --input file is missing', () async { final SymbolizeCommand command = SymbolizeCommand( stdio: stdio, diff --git a/packages/flutter_tools/test/general.shard/base/build_test.dart b/packages/flutter_tools/test/general.shard/base/build_test.dart index 39161065f8d81..06e33e19a21a2 100644 --- a/packages/flutter_tools/test/general.shard/base/build_test.dart +++ b/packages/flutter_tools/test/general.shard/base/build_test.dart @@ -210,7 +210,6 @@ void main() { '--deterministic', '--snapshot_kind=app-aot-assembly', '--assembly=$assembly', - '--strip', 'main.dill', ]), kWhichSysctlCommand, @@ -253,6 +252,21 @@ void main() { 'build/foo/App.framework/App', 'build/foo/snapshot_assembly.o', ]), + const FakeCommand(command: [ + 'xcrun', + 'dsymutil', + '-o', + 'build/foo/App.framework.dSYM', + 'build/foo/App.framework/App', + ]), + const FakeCommand(command: [ + 'xcrun', + 'strip', + '-S', + 'build/foo/App.framework/App', + '-o', + 'build/foo/App.framework/App', + ]), ]); final int genSnapshotExitCode = await snapshotter.build( @@ -285,7 +299,6 @@ void main() { '--deterministic', '--snapshot_kind=app-aot-assembly', '--assembly=$assembly', - '--strip', '--dwarf-stack-traces', '--save-debugging-info=$debugPath', 'main.dill', @@ -312,6 +325,21 @@ void main() { 'arm64', ...kDefaultClang, ]), + const FakeCommand(command: [ + 'xcrun', + 'dsymutil', + '-o', + 'build/foo/App.framework.dSYM', + 'build/foo/App.framework/App', + ]), + const FakeCommand(command: [ + 'xcrun', + 'strip', + '-S', + 'build/foo/App.framework/App', + '-o', + 'build/foo/App.framework/App', + ]), ]); final int genSnapshotExitCode = await snapshotter.build( @@ -344,7 +372,6 @@ void main() { '--deterministic', '--snapshot_kind=app-aot-assembly', '--assembly=$assembly', - '--strip', '--obfuscate', 'main.dill', ]), @@ -370,6 +397,21 @@ void main() { 'arm64', ...kDefaultClang, ]), + const FakeCommand(command: [ + 'xcrun', + 'dsymutil', + '-o', + 'build/foo/App.framework.dSYM', + 'build/foo/App.framework/App', + ]), + const FakeCommand(command: [ + 'xcrun', + 'strip', + '-S', + 'build/foo/App.framework/App', + '-o', + 'build/foo/App.framework/App', + ]), ]); final int genSnapshotExitCode = await snapshotter.build( @@ -400,7 +442,6 @@ void main() { '--deterministic', '--snapshot_kind=app-aot-assembly', '--assembly=${fileSystem.path.join(outputPath, 'snapshot_assembly.S')}', - '--strip', 'main.dill', ]), kWhichSysctlCommand, @@ -425,6 +466,21 @@ void main() { 'arm64', ...kDefaultClang, ]), + const FakeCommand(command: [ + 'xcrun', + 'dsymutil', + '-o', + 'build/foo/App.framework.dSYM', + 'build/foo/App.framework/App', + ]), + const FakeCommand(command: [ + 'xcrun', + 'strip', + '-S', + 'build/foo/App.framework/App', + '-o', + 'build/foo/App.framework/App', + ]), ]); final int genSnapshotExitCode = await snapshotter.build( diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart index 03c198496a2d4..422890aff3e1a 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart @@ -477,7 +477,6 @@ void main() { '--deterministic', kAssemblyAot, '--assembly=$build/arm64/snapshot_assembly.S', - '--strip', '$build/app.dill', ]), FakeCommand(command: [ @@ -520,6 +519,21 @@ void main() { '$build/arm64/App.framework/App', '$build/arm64/snapshot_assembly.o', ]), + FakeCommand(command: [ + 'xcrun', + 'dsymutil', + '-o', + '$build/arm64/App.framework.dSYM', + '$build/arm64/App.framework/App', + ]), + FakeCommand(command: [ + 'xcrun', + 'strip', + '-S', + '$build/arm64/App.framework/App', + '-o', + '$build/arm64/App.framework/App', + ]), FakeCommand(command: [ 'lipo', '$build/arm64/App.framework/App', @@ -553,7 +567,6 @@ void main() { '--trace-precompiler-to=code_size_1/trace.arm64.json', kAssemblyAot, '--assembly=$build/arm64/snapshot_assembly.S', - '--strip', '$build/app.dill', ]), FakeCommand(command: [ @@ -596,6 +609,21 @@ void main() { '$build/arm64/App.framework/App', '$build/arm64/snapshot_assembly.o', ]), + FakeCommand(command: [ + 'xcrun', + 'dsymutil', + '-o', + '$build/arm64/App.framework.dSYM', + '$build/arm64/App.framework/App', + ]), + FakeCommand(command: [ + 'xcrun', + 'strip', + '-S', + '$build/arm64/App.framework/App', + '-o', + '$build/arm64/App.framework/App', + ]), FakeCommand(command: [ 'lipo', '$build/arm64/App.framework/App', diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart index b9412d436ab69..cd2d81a3cad84 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart @@ -239,6 +239,14 @@ void main() { .childFile('App') .createSync(recursive: true); + // Input dSYM + environment.buildDir + .childDirectory('App.framework.dSYM') + .childDirectory('Contents') + .childDirectory('Resources') + .childDirectory('DWARF') + .childFile('App') + .createSync(recursive: true); final Directory frameworkDirectory = environment.outputDir.childDirectory('App.framework'); final File frameworkDirectoryBinary = frameworkDirectory.childFile('App'); @@ -257,6 +265,12 @@ void main() { expect(frameworkDirectoryBinary, exists); expect(frameworkDirectory.childFile('Info.plist'), exists); + expect(environment.outputDir + .childDirectory('App.framework.dSYM') + .childDirectory('Contents') + .childDirectory('Resources') + .childDirectory('DWARF') + .childFile('App'), exists); final Directory assetDirectory = frameworkDirectory.childDirectory('flutter_assets'); expect(assetDirectory.childFile('kernel_blob.bin'), isNot(exists)); diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart index 909f5fe27ab67..c43454d5b15e0 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart @@ -295,6 +295,28 @@ void main() { ProcessManager: () => processManager, }); + testUsingContext('release macOS application creates App.framework.dSYM', () async { + fileSystem.file('bin/cache/artifacts/engine/darwin-x64/vm_isolate_snapshot.bin') + .createSync(recursive: true); + fileSystem.file('bin/cache/artifacts/engine/darwin-x64/isolate_snapshot.bin') + .createSync(recursive: true); + fileSystem.file('${environment.buildDir.path}/App.framework/App') + .createSync(recursive: true); + fileSystem.file('${environment.buildDir.path}/App.framework.dSYM/Contents/Resources/DWARF/App') + .createSync(recursive: true); + + await const ReleaseMacOSBundleFlutterAssets() + .build(environment..defines[kBuildMode] = 'release'); + + expect(fileSystem.file( + 'App.framework.dSYM/Contents/Resources/DWARF/App'), + exists, + ); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }); + testUsingContext('release/profile macOS application updates when App.framework updates', () async { fileSystem.file('bin/cache/artifacts/engine/darwin-x64/vm_isolate_snapshot.bin') .createSync(recursive: true); @@ -415,13 +437,20 @@ void main() { environment.defines[kDarwinArchs] = 'arm64 x86_64'; environment.defines[kBuildMode] = 'release'; + // Input dSYMs need to exist for `lipo` to combine them + environment.buildDir + .childFile('arm64/App.framework.dSYM/Contents/Resources/DWARF/App') + .createSync(recursive: true); + environment.buildDir + .childFile('x86_64/App.framework.dSYM/Contents/Resources/DWARF/App') + .createSync(recursive: true); + processManager.addCommands([ FakeCommand(command: [ 'Artifact.genSnapshot.TargetPlatform.darwin.release_arm64', '--deterministic', '--snapshot_kind=app-aot-assembly', '--assembly=${environment.buildDir.childFile('arm64/snapshot_assembly.S').path}', - '--strip', environment.buildDir.childFile('app.dill').path, ]), FakeCommand(command: [ @@ -429,7 +458,6 @@ void main() { '--deterministic', '--snapshot_kind=app-aot-assembly', '--assembly=${environment.buildDir.childFile('x86_64/snapshot_assembly.S').path}', - '--strip', environment.buildDir.childFile('app.dill').path, ]), FakeCommand(command: [ @@ -458,6 +486,36 @@ void main() { '-o', environment.buildDir.childFile('x86_64/App.framework/App').path, environment.buildDir.childFile('x86_64/snapshot_assembly.o').path, ]), + FakeCommand(command: [ + 'xcrun', + 'dsymutil', + '-o', + environment.buildDir.childFile('arm64/App.framework.dSYM').path, + environment.buildDir.childFile('arm64/App.framework/App').path, + ]), + FakeCommand(command: [ + 'xcrun', + 'dsymutil', + '-o', + environment.buildDir.childFile('x86_64/App.framework.dSYM').path, + environment.buildDir.childFile('x86_64/App.framework/App').path, + ]), + FakeCommand(command: [ + 'xcrun', + 'strip', + '-S', + environment.buildDir.childFile('arm64/App.framework/App').path, + '-o', + environment.buildDir.childFile('arm64/App.framework/App').path, + ]), + FakeCommand(command: [ + 'xcrun', + 'strip', + '-S', + environment.buildDir.childFile('x86_64/App.framework/App').path, + '-o', + environment.buildDir.childFile('x86_64/App.framework/App').path, + ]), FakeCommand(command: [ 'lipo', environment.buildDir.childFile('arm64/App.framework/App').path, @@ -466,6 +524,14 @@ void main() { '-output', environment.buildDir.childFile('App.framework/App').path, ]), + FakeCommand(command: [ + 'lipo', + environment.buildDir.childFile('arm64/App.framework.dSYM/Contents/Resources/DWARF/App').path, + environment.buildDir.childFile('x86_64/App.framework.dSYM/Contents/Resources/DWARF/App').path, + '-create', + '-output', + environment.buildDir.childFile('App.framework.dSYM/Contents/Resources/DWARF/App').path, + ]), ]); await const CompileMacOSFramework().build(environment); diff --git a/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart b/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart index de08721d7d453..c2e03191edd91 100644 --- a/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart +++ b/packages/flutter_tools/test/host_cross_arch.shard/ios_content_validation_test.dart @@ -5,6 +5,7 @@ import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/utils.dart'; import 'package:flutter_tools/src/build_info.dart'; import '../integration.shard/test_utils.dart'; @@ -75,7 +76,7 @@ void main() { for (final BuildMode buildMode in [BuildMode.debug, BuildMode.release]) { group('build in ${buildMode.name} mode', () { - late Directory buildPath; + late Directory outputPath; late Directory outputApp; late Directory frameworkDirectory; late Directory outputFlutterFramework; @@ -83,6 +84,9 @@ void main() { late Directory outputAppFramework; late File outputAppFrameworkBinary; late File outputPluginFrameworkBinary; + late Directory buildPath; + late Directory buildAppFrameworkDsym; + late File buildAppFrameworkDsymBinary; late ProcessResult buildResult; setUpAll(() { @@ -98,14 +102,14 @@ void main() { '--split-debug-info=foo debug info/', ], workingDirectory: projectRoot); - buildPath = fileSystem.directory(fileSystem.path.join( + outputPath = fileSystem.directory(fileSystem.path.join( projectRoot, 'build', 'ios', 'iphoneos', )); - outputApp = buildPath.childDirectory('Runner.app'); + outputApp = outputPath.childDirectory('Runner.app'); frameworkDirectory = outputApp.childDirectory('Frameworks'); outputFlutterFramework = frameworkDirectory.childDirectory('Flutter.framework'); @@ -115,6 +119,16 @@ void main() { outputAppFrameworkBinary = outputAppFramework.childFile('App'); outputPluginFrameworkBinary = frameworkDirectory.childDirectory('hello.framework').childFile('hello'); + + buildPath = fileSystem.directory(fileSystem.path.join( + projectRoot, + 'build', + 'ios', + '${sentenceCase(buildMode.name)}-iphoneos', + )); + + buildAppFrameworkDsym = buildPath.childDirectory('App.framework.dSYM'); + buildAppFrameworkDsymBinary = buildAppFrameworkDsym.childFile('Contents/Resources/DWARF/App'); }); testWithoutContext('flutter build ios builds a valid app', () { @@ -128,6 +142,8 @@ void main() { expect(outputAppFrameworkBinary, exists); expect(outputAppFramework.childFile('Info.plist'), exists); + expect(buildAppFrameworkDsymBinary.existsSync(), buildMode != BuildMode.debug); + final File vmSnapshot = fileSystem.file(fileSystem.path.join( outputAppFramework.path, 'flutter_assets', @@ -190,6 +206,25 @@ void main() { expect(aotSymbolsFound, buildMode != BuildMode.debug); }); + // dSYM is not created for a debug build so nothing to check. + if (buildMode != BuildMode.debug) { + testWithoutContext('check symbols in dSYM', () { + final ProcessResult nm = processManager.runSync( + [ + 'nm', + '--debug-syms', + '--defined-only', + '--just-symbol-name', + buildAppFrameworkDsymBinary.path, + '-arch', + 'arm64', + ], + ); + final List symbols = (nm.stdout as String).split('\n'); + expect(symbols, contains('_kDartVmSnapshotInstructions')); + }); + } + testWithoutContext('xcode_backend embed_and_thin', () { outputFlutterFramework.deleteSync(recursive: true); outputAppFramework.deleteSync(recursive: true); @@ -219,7 +254,7 @@ void main() { 'ios', 'Release-iphoneos', ), - 'TARGET_BUILD_DIR': buildPath.path, + 'TARGET_BUILD_DIR': outputPath.path, 'FRAMEWORKS_FOLDER_PATH': 'Runner.app/Frameworks', 'VERBOSE_SCRIPT_LOGGING': '1', 'FLUTTER_BUILD_MODE': 'release', diff --git a/packages/flutter_tools/test/host_cross_arch.shard/macos_content_validation_test.dart b/packages/flutter_tools/test/host_cross_arch.shard/macos_content_validation_test.dart index 19f21e4965249..4502f380f5a3d 100644 --- a/packages/flutter_tools/test/host_cross_arch.shard/macos_content_validation_test.dart +++ b/packages/flutter_tools/test/host_cross_arch.shard/macos_content_validation_test.dart @@ -67,18 +67,18 @@ void main() { expect(result.exitCode, 0); expect(result.stdout, contains('Running pod install')); + expect(podfile.lastModifiedSync().isBefore(podfileLock.lastModifiedSync()), isTrue); - final Directory outputApp = fileSystem.directory(fileSystem.path.join( + final Directory buildPath = fileSystem.directory(fileSystem.path.join( workingDirectory, 'build', 'macos', 'Build', 'Products', buildMode, - 'flutter_gallery.app', )); - expect(podfile.lastModifiedSync().isBefore(podfileLock.lastModifiedSync()), isTrue); + final Directory outputApp = buildPath.childDirectory('flutter_gallery.app'); final Directory outputAppFramework = fileSystem.directory(fileSystem.path.join( outputApp.path, @@ -87,19 +87,19 @@ void main() { 'App.framework', )); - final File outputAppFrameworkBinary = outputAppFramework.childFile('App'); - final String archs = processManager.runSync( - ['file', outputAppFrameworkBinary.path], - ).stdout as String; - - final bool containsX64 = archs.contains('Mach-O 64-bit dynamically linked shared library x86_64'); - final bool containsArm = archs.contains('Mach-O 64-bit dynamically linked shared library arm64'); - if (buildModeLower == 'debug') { - // Only build the architecture matching the machine running this test, not both. - expect(containsX64 ^ containsArm, isTrue, reason: 'Unexpected architecture $archs'); - } else { - expect(containsX64, isTrue, reason: 'Unexpected architecture $archs'); - expect(containsArm, isTrue, reason: 'Unexpected architecture $archs'); + _checkFatBinary( + outputAppFramework.childFile('App'), + buildModeLower, + 'dynamically linked shared library', + ); + + // dSYM is not created for a debug build so nothing to check. + if (buildMode != 'Debug') { + _checkFatBinary( + buildPath.childFile('App.framework.dSYM/Contents/Resources/DWARF/App'), + buildModeLower, + 'dSYM companion file', + ); } expect(outputAppFramework.childLink('Resources'), exists); @@ -172,3 +172,19 @@ void main() { }, skip: !platform.isMacOS); // [intended] only makes sense for macos platform. } } + +void _checkFatBinary(File file, String buildModeLower, String expectedType) { + final String archs = processManager.runSync( + ['file', file.path], + ).stdout as String; + + final bool containsX64 = archs.contains('Mach-O 64-bit $expectedType x86_64'); + final bool containsArm = archs.contains('Mach-O 64-bit $expectedType arm64'); + if (buildModeLower == 'debug') { + // Only build the architecture matching the machine running this test, not both. + expect(containsX64 ^ containsArm, isTrue, reason: 'Unexpected architecture $archs'); + } else { + expect(containsX64, isTrue, reason: 'Unexpected architecture $archs'); + expect(containsArm, isTrue, reason: 'Unexpected architecture $archs'); + } +}