From 353bec25c998e08ddb181234a546c138d7809686 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Thu, 8 Aug 2024 09:28:52 -0700 Subject: [PATCH 01/14] chore: squash all commits on 3.29.0 --- .github/workflows/ci.yml | 43 ++ .github/workflows/shorebird_ci.yml | 57 +++ .../flutter_tools/gradle/build.gradle.kts | 1 + .../gradle/src/main/groovy/flutter.groovy | 37 ++ .../flutter_tools/lib/src/base/build.dart | 13 +- .../lib/src/build_system/targets/assets.dart | 15 + .../lib/src/build_system/targets/ios.dart | 16 + .../lib/src/build_system/targets/macos.dart | 16 + packages/flutter_tools/lib/src/cache.dart | 5 + .../lib/src/ios/application_package.dart | 12 + packages/flutter_tools/lib/src/ios/mac.dart | 4 +- .../lib/src/shorebird/shorebird_yaml.dart | 64 +++ .../lib/src/windows/build_windows.dart | 1 + .../hermetic/build_macos_test.dart | 41 ++ .../hermetic/build_windows_test.dart | 164 +++++-- .../build_system/targets/macos_test.dart | 212 ++++++-- .../test/general.shard/cache_test.dart | 459 ++++++++++++------ .../shorebird/shorebird_yaml_test.dart | 101 ++++ packages/shorebird_tests/.gitignore | 3 + packages/shorebird_tests/README.md | 2 + .../shorebird_tests/analysis_options.yaml | 2 + packages/shorebird_tests/pubspec.yaml | 16 + .../shorebird_tests/test/android_test.dart | 92 ++++ packages/shorebird_tests/test/base_test.dart | 15 + packages/shorebird_tests/test/ios_test.dart | 91 ++++ .../shorebird_tests/test/shorebird_tests.dart | 269 ++++++++++ 26 files changed, 1505 insertions(+), 246 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/shorebird_ci.yml create mode 100644 packages/flutter_tools/lib/src/shorebird/shorebird_yaml.dart create mode 100644 packages/flutter_tools/test/general.shard/shorebird/shorebird_yaml_test.dart create mode 100644 packages/shorebird_tests/.gitignore create mode 100644 packages/shorebird_tests/README.md create mode 100644 packages/shorebird_tests/analysis_options.yaml create mode 100644 packages/shorebird_tests/pubspec.yaml create mode 100644 packages/shorebird_tests/test/android_test.dart create mode 100644 packages/shorebird_tests/test/base_test.dart create mode 100644 packages/shorebird_tests/test/ios_test.dart create mode 100644 packages/shorebird_tests/test/shorebird_tests.dart diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000000..ade159ddabcb9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: ci + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + runs-on: ${{ matrix.os }} + + name: ๐Ÿงช Test + + env: + FLUTTER_STORAGE_BASE_URL: https://download.shorebird.dev + + steps: + - name: ๐Ÿ“š Git Checkout + uses: actions/checkout@v4 + with: + # Fetch all branches and tags to ensure that Flutter can determine its version + fetch-depth: 0 + + - name: ๐ŸŽฏ Setup Dart + uses: dart-lang/setup-dart@v1 + + - name: ๐Ÿ“ฆ Install Dependencies + run: | + dart pub get -C ./dev/bots + dart pub get -C ./dev/tools + + - name: ๐Ÿงช Run Tests + run: dart ./dev/bots/test.dart diff --git a/.github/workflows/shorebird_ci.yml b/.github/workflows/shorebird_ci.yml new file mode 100644 index 0000000000000..69397c4cad0d6 --- /dev/null +++ b/.github/workflows/shorebird_ci.yml @@ -0,0 +1,57 @@ +name: shorebird_ci + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + push: + branches: + - shorebird/dev + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + runs-on: ${{ matrix.os }} + + name: ๐Ÿฆ Shorebird Test + + # TODO(eseidel): This is also set inside shorebird_tests, unclear if + # if it's needed here as well. + env: + FLUTTER_STORAGE_BASE_URL: https://download.shorebird.dev + + steps: + - name: ๐Ÿ“š Git Checkout + uses: actions/checkout@v4 + with: + # Fetch all branches and tags to ensure that Flutter can determine its version + fetch-depth: 0 + + # TODO(eseidel): shorebird_tests seems to assume flutter is available + # yet it doesn't seem to set it up here? + - name: ๐ŸŽฏ Setup Dart + uses: dart-lang/setup-dart@v1 + + - uses: actions/setup-java@v4 + with: + distribution: "zulu" + java-version: "17" + + - name: ๐Ÿฆ Run Flutter Tools Tests + # TODO(eseidel): Find a nice way to run this on windows. + if: matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' + # TODO(eseidel): We can't run all flutter_tools tests until we make + # our changes not throw exceptions on missing shorebird.yaml. + # https://github.com/shorebirdtech/shorebird/issues/2392 + run: ../../bin/flutter test test/general.shard/shorebird + working-directory: packages/flutter_tools + + - name: ๐Ÿฆ Run Shorebird Tests + run: dart test + working-directory: packages/shorebird_tests diff --git a/packages/flutter_tools/gradle/build.gradle.kts b/packages/flutter_tools/gradle/build.gradle.kts index 56d4629b9b2fd..80ff9a77029a8 100644 --- a/packages/flutter_tools/gradle/build.gradle.kts +++ b/packages/flutter_tools/gradle/build.gradle.kts @@ -65,6 +65,7 @@ dependencies { // * AGP version constants in packages/flutter_tools/lib/src/android/gradle_utils.dart compileOnly("com.android.tools.build:gradle:8.7.3") + implementation("org.yaml:snakeyaml:2.0") testImplementation(kotlin("test")) testImplementation("com.android.tools.build:gradle:8.7.3") testImplementation("org.mockito:mockito-core:4.8.0") diff --git a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy index b75a4601a6c00..2ee8c9b30fb01 100644 --- a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy +++ b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy @@ -7,6 +7,7 @@ import com.android.build.OutputFile import com.flutter.gradle.BaseApplicationNameHandler import groovy.json.JsonGenerator import groovy.xml.QName + import java.nio.file.Paths import org.apache.tools.ant.taskdefs.condition.Os import org.gradle.api.DefaultTask @@ -30,6 +31,7 @@ import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.bundling.Jar import org.gradle.internal.os.OperatingSystem +import org.yaml.snakeyaml.Yaml /** * For apps only. Provides the flutter extension used in the app-level Gradle @@ -103,6 +105,41 @@ class FlutterExtension { } } +// This buildscript block supplies dependencies for this file's own import +// declarations above. It exists solely for compatibility with projects that +// have not migrated to declaratively apply the Flutter Gradle Plugin; +// for those that have, FGP's `build.gradle.kts` takes care of this. +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + // When bumping, also update: + // * ndkVersion in FlutterExtension in packages/flutter_tools/gradle/src/main/groovy/flutter.groovy + // * AGP version in the buildscript block in packages/flutter_tools/gradle/src/main/kotlin/dependency_version_checker.gradle.kts + // * AGP version constants in packages/flutter_tools/lib/src/android/gradle_utils.dart + // * AGP version in dependencies block in packages/flutter_tools/gradle/build.gradle.kts + classpath("com.android.tools.build:gradle:7.3.0") + classpath(group: 'org.yaml', name: 'snakeyaml', version: '2.0') + } +} + +/** + * Some apps don't set default compile options. + * Apps can change these values in the app-level Gradle build file + * (android/app/build.gradle or android/app/build.gradle.kts). + * This just ensures that default values are set. + */ +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +apply plugin: FlutterPlugin + class FlutterPlugin implements Plugin { private static final String DEFAULT_MAVEN_HOST = "https://storage.googleapis.com" diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart index 790cec98bc22d..9755fa792d2fa 100644 --- a/packages/flutter_tools/lib/src/base/build.dart +++ b/packages/flutter_tools/lib/src/base/build.dart @@ -130,7 +130,18 @@ class AOTSnapshotter { final Directory outputDir = _fileSystem.directory(outputPath); outputDir.createSync(recursive: true); - final List genSnapshotArgs = ['--deterministic']; + final List dumpClassTableLinkInfoArgs = [ + // Shorebird dumps the class table information during snapshot compilation which is later used during linking. + '--print_class_table_link_debug_info_to=${_fileSystem.path.join(outputDir.parent.path, 'App.class_table.json')}', + '--print_class_table_link_info_to=${_fileSystem.path.join(outputDir.parent.path, 'App.ct.link')}', + ]; + + final List genSnapshotArgs = [ + // Shorebird uses --deterministic to improve snapshot stability and increase linking. + '--deterministic', + // Only use the default Shorebird gen_snapshot args on iOS. + if (platform == TargetPlatform.ios || platform == TargetPlatform.darwin) ...dumpClassTableLinkInfoArgs, + ]; final bool targetingApplePlatform = platform == TargetPlatform.ios || platform == TargetPlatform.darwin; diff --git a/packages/flutter_tools/lib/src/build_system/targets/assets.dart b/packages/flutter_tools/lib/src/build_system/targets/assets.dart index a2b28cd9de55c..d84b507e35193 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/assets.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/assets.dart @@ -14,6 +14,8 @@ import '../../convert.dart'; import '../../dart/package_map.dart'; import '../../devfs.dart'; import '../../flutter_manifest.dart'; +import '../../globals.dart' as globals show platform; +import '../../shorebird/shorebird_yaml.dart'; import '../build_system.dart'; import '../depfile.dart'; import '../exceptions.dart'; @@ -188,6 +190,19 @@ Future copyAssets( } if (doCopy) { await (content.file as File).copy(file.path); + if (file.basename == 'shorebird.yaml') { + try { + updateShorebirdYaml( + environment.defines[kFlavor], + file.path, + environment: globals.platform.environment, + ); + } on Exception catch (error) { + throw Exception( + 'Failed to generate shorebird configuration. Error: $error', + ); + } + } } } else { await file.writeAsBytes(await entry.value.content.contentsAsBytes()); 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 61da22b57d1b4..e1c4ed534add1 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -139,6 +139,22 @@ abstract class AotAssemblyBase extends Target { // Don't fail if the dSYM wasn't created (i.e. during a debug build). skipMissingInputs: true, ); + + // If the shorebird directory exists, delete it first. + final Directory shorebirdDir = environment.fileSystem.directory(environment.fileSystem.path.join(getIosBuildDirectory(), 'shorebird')); + if (shorebirdDir.existsSync()) { + shorebirdDir.deleteSync(recursive: true); + } + + // Copy the class table link information (generated by gen_snapshot) from the buildOutputPath to the iOS build directory. + final File classTableLink = environment.fileSystem.file(environment.fileSystem.path.join(buildOutputPath, 'App.ct.link')); + final File classTableLinkDebug = environment.fileSystem.file(environment.fileSystem.path.join(buildOutputPath, 'App.class_table.json')); + if (classTableLink.existsSync()) { + classTableLink.copySync(environment.fileSystem.path.join(shorebirdDir.path, 'App.ct.link')); + } + if (classTableLinkDebug.existsSync()) { + classTableLinkDebug.copySync(environment.fileSystem.path.join(shorebirdDir.path, 'App.class_table.json')); + } } } 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 5c4f74246130a..61408b8ecb609 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -402,6 +402,22 @@ class CompileMacOSFramework extends Target { // Don't fail if the dSYM wasn't created (i.e. during a debug build). skipMissingInputs: true, ); + + // If the shorebird directory exists, delete it first. + final Directory shorebirdDir = environment.fileSystem.directory(environment.fileSystem.path.join(getMacOSBuildDirectory(), 'shorebird')); + if (shorebirdDir.existsSync()) { + shorebirdDir.deleteSync(recursive: true); + } + + // Copy the class table link information (generated by gen_snapshot) from the buildOutputPath to the macos build directory. + final File classTableLink = environment.fileSystem.file(environment.fileSystem.path.join(buildOutputPath, 'App.ct.link')); + final File classTableLinkDebug = environment.fileSystem.file(environment.fileSystem.path.join(buildOutputPath, 'App.class_table.json')); + if (classTableLink.existsSync()) { + classTableLink.copySync(environment.fileSystem.path.join(shorebirdDir.path, 'App.ct.link')); + } + if (classTableLinkDebug.existsSync()) { + classTableLinkDebug.copySync(environment.fileSystem.path.join(shorebirdDir.path, 'App.class_table.json')); + } } @override diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart index be7160bed3028..db9f5c3a0d818 100644 --- a/packages/flutter_tools/lib/src/cache.dart +++ b/packages/flutter_tools/lib/src/cache.dart @@ -29,6 +29,7 @@ import 'base/user_messages.dart'; import 'convert.dart'; import 'features.dart'; +const String kShorebirdStorageUrl = 'https://download.shorebird.dev'; const String kFlutterRootEnvironmentVariableName = 'FLUTTER_ROOT'; // should point to //flutter/ (root of flutter/flutter repo) const String kFlutterEngineEnvironmentVariableName = @@ -535,6 +536,10 @@ class Cache { ? 'https://storage.googleapis.com' : 'https://storage.googleapis.com/$storageRealm'; } + // Shorebird's artifact proxy is a trusted source. + if (overrideUrl == kShorebirdStorageUrl) { + return overrideUrl; + } // verify that this is a valid URI. overrideUrl = storageRealm.isEmpty ? overrideUrl : '$overrideUrl/$storageRealm'; try { diff --git a/packages/flutter_tools/lib/src/ios/application_package.dart b/packages/flutter_tools/lib/src/ios/application_package.dart index 62f2c8b93df7a..7484bef7fc1e8 100644 --- a/packages/flutter_tools/lib/src/ios/application_package.dart +++ b/packages/flutter_tools/lib/src/ios/application_package.dart @@ -129,6 +129,18 @@ class BuildableIOSApp extends IOSApp { @override String? get name => _appProductName; + String get shorebirdYamlPath => + globals.fs.path.join( + archiveBundleOutputPath, + 'Products', + 'Applications', + _appProductName != null ? '$_appProductName.app' : 'Runner.app', + 'Frameworks', + 'App.framework', + 'flutter_assets', + 'shorebird.yaml', + ); + @override String get simulatorBundlePath => _buildAppPath('iphonesimulator'); diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index dba3560a09c39..03a560812527e 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:meta/meta.dart'; import 'package:process/process.dart'; @@ -10,7 +11,6 @@ import 'package:unified_analytics/unified_analytics.dart'; import '../artifacts.dart'; import '../base/file_system.dart'; -import '../base/io.dart'; import '../base/logger.dart'; import '../base/process.dart'; import '../base/project_migrator.dart'; @@ -32,6 +32,7 @@ import '../migrations/xcode_script_build_phase_migration.dart'; import '../migrations/xcode_thin_binary_build_phase_input_paths_migration.dart'; import '../plugins.dart'; import '../project.dart'; +import '../reporting/reporting.dart'; import 'application_package.dart'; import 'code_signing.dart'; import 'migrations/host_app_info_plist_migration.dart'; @@ -569,6 +570,7 @@ Future buildXcodeProject({ globals.printError('Archive succeeded but the expected xcarchive at $outputDir not found'); } } + return XcodeBuildResult( success: true, output: outputDir, diff --git a/packages/flutter_tools/lib/src/shorebird/shorebird_yaml.dart b/packages/flutter_tools/lib/src/shorebird/shorebird_yaml.dart new file mode 100644 index 0000000000000..9e40f392b7eb8 --- /dev/null +++ b/packages/flutter_tools/lib/src/shorebird/shorebird_yaml.dart @@ -0,0 +1,64 @@ +// Copyright 2024 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 'package:yaml/yaml.dart'; +import 'package:yaml_edit/yaml_edit.dart'; + +import '../base/file_system.dart'; +import '../globals.dart' as globals; + +void updateShorebirdYaml(String? flavor, String shorebirdYamlPath, {required Map environment}) { + final File shorebirdYaml = globals.fs.file(shorebirdYamlPath); + if (!shorebirdYaml.existsSync()) { + throw Exception('shorebird.yaml not found at $shorebirdYamlPath'); + } + final YamlDocument input = loadYamlDocument(shorebirdYaml.readAsStringSync()); + final YamlMap yamlMap = input.contents as YamlMap; + final Map compiled = compileShorebirdYaml(yamlMap, flavor: flavor, environment: environment); + // Currently we write out over the same yaml file, we should fix this to + // write to a new .json file instead and avoid naming confusion between the + // input and compiled files. + final YamlEditor yamlEditor = YamlEditor(''); + yamlEditor.update([], compiled); + shorebirdYaml.writeAsStringSync(yamlEditor.toString(), flush: true); +} + +String appIdForFlavor(YamlMap yamlMap, {required String? flavor}) { + if (flavor == null || flavor.isEmpty) { + final String? defaultAppId = yamlMap['app_id'] as String?; + if (defaultAppId == null || defaultAppId.isEmpty) { + throw Exception('Cannot find "app_id" in shorebird.yaml'); + } + return defaultAppId; + } + + final YamlMap? yamlFlavors = yamlMap['flavors'] as YamlMap?; + if (yamlFlavors == null) { + throw Exception('Cannot find "flavors" in shorebird.yaml.'); + } + final String? flavorAppId = yamlFlavors[flavor] as String?; + if (flavorAppId == null || flavorAppId.isEmpty) { + throw Exception('Cannot find "app_id" for $flavor in shorebird.yaml'); + } + return flavorAppId; +} + +Map compileShorebirdYaml(YamlMap yamlMap, {required String? flavor, required Map environment}) { + final String appId = appIdForFlavor(yamlMap, flavor: flavor); + final Map compiled = { + 'app_id': appId, + }; + void copyIfSet(String key) { + if (yamlMap[key] != null) { + compiled[key] = yamlMap[key]; + } + } + copyIfSet('base_url'); + copyIfSet('auto_update'); + final String? shorebirdPublicKeyEnvVar = environment['SHOREBIRD_PUBLIC_KEY']; + if (shorebirdPublicKeyEnvVar != null) { + compiled['patch_public_key'] = shorebirdPublicKeyEnvVar; + } + return compiled; +} diff --git a/packages/flutter_tools/lib/src/windows/build_windows.dart b/packages/flutter_tools/lib/src/windows/build_windows.dart index dd415628e9806..74be94dc5856a 100644 --- a/packages/flutter_tools/lib/src/windows/build_windows.dart +++ b/packages/flutter_tools/lib/src/windows/build_windows.dart @@ -21,6 +21,7 @@ import '../flutter_plugins.dart'; import '../globals.dart' as globals; import '../migrations/cmake_custom_command_migration.dart'; import '../migrations/cmake_native_assets_migration.dart'; +import '../shorebird/shorebird_yaml.dart'; import 'migrations/build_architecture_migration.dart'; import 'migrations/show_window_migration.dart'; import 'migrations/version_migration.dart'; diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart index 81f4feb5b6faa..e6ac80be81df6 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart @@ -50,6 +50,14 @@ final FakePlatform macosPlatformCustomEnv = FakePlatform( environment: {'FLUTTER_ROOT': '/', 'HOME': '/'}, ); +final Platform macosPlatformWithShorebirdPublicKey = FakePlatform( + operatingSystem: 'macos', + environment: { + 'FLUTTER_ROOT': '/', + 'HOME': '/', + 'SHOREBIRD_PUBLIC_KEY': 'my_public_key', + } +); final Platform notMacosPlatform = FakePlatform(environment: {'FLUTTER_ROOT': '/'}); void main() { @@ -941,4 +949,37 @@ STDERR STUFF FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true), }, ); + + testUsingContext('macOS build outputs path and size when successful', + () async { + final BuildCommand command = BuildCommand( + artifacts: artifacts, + androidSdk: FakeAndroidSdk(), + buildSystem: TestBuildSystem.all(BuildResult(success: true)), + fileSystem: MemoryFileSystem.test(), + processUtils: processUtils, + logger: BufferLogger.test(), + osUtils: FakeOperatingSystemUtils(), + ); + createMinimalMockProjectFiles(); + final File shorebirdYamlFile = fileSystem.file( + 'build/macos/Build/Products/Release/example.app/Contents/Frameworks/App.framework/Resources/flutter_assets/shorebird.yaml', + ) + ..createSync(recursive: true) + ..writeAsStringSync('app_id: my-app-id'); + + await createTestCommandRunner(command) + .run(const ['build', 'macos', '--no-pub']); + + final String updatedYaml = shorebirdYamlFile.readAsStringSync(); + expect(updatedYaml, contains('app_id: my-app-id')); + expect(updatedYaml, contains('patch_public_key: my_public_key')); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.list([ + setUpFakeXcodeBuildHandler('Release'), + ]), + Platform: () => macosPlatformWithShorebirdPublicKey, + FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true), + }); } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart index 5b8a7b356174f..59c5b5f7b1a42 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart @@ -23,9 +23,10 @@ import '../../src/test_flutter_command_runner.dart'; const String flutterRoot = r'C:\flutter'; const String buildFilePath = r'windows\CMakeLists.txt'; -const String visualStudioPath = r'C:\Program Files (x86)\Microsoft Visual Studio\2017\Community'; -const String _cmakePath = - visualStudioPath + r'\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe'; +const String visualStudioPath = + r'C:\Program Files (x86)\Microsoft Visual Studio\2017\Community'; +const String _cmakePath = visualStudioPath + + r'\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe'; const String _defaultGenerator = 'Visual Studio 16 2019'; final Platform windowsPlatform = FakePlatform( @@ -36,6 +37,13 @@ final Platform windowsPlatform = FakePlatform( 'USERPROFILE': '/', }, ); +final Platform windowsPlatformWithPublicKey = + FakePlatform(operatingSystem: 'windows', environment: { + 'PROGRAMFILES(X86)': r'C:\Program Files (x86)\', + 'FLUTTER_ROOT': flutterRoot, + 'USERPROFILE': '/', + 'SHOREBIRD_PUBLIC_KEY': 'my_public_key', +}); final Platform notWindowsPlatform = FakePlatform( environment: {'FLUTTER_ROOT': flutterRoot}, ); @@ -62,8 +70,13 @@ void main() { // Creates the mock files necessary to look like a Flutter project. void setUpMockCoreProjectFiles() { fileSystem.file('pubspec.yaml').createSync(); - fileSystem.directory('.dart_tool').childFile('package_config.json').createSync(recursive: true); - fileSystem.file(fileSystem.path.join('lib', 'main.dart')).createSync(recursive: true); + fileSystem + .directory('.dart_tool') + .childFile('package_config.json') + .createSync(recursive: true); + fileSystem + .file(fileSystem.path.join('lib', 'main.dart')) + .createSync(recursive: true); } // Creates the mock files necessary to run a build. @@ -113,7 +126,9 @@ void main() { ...['--target', 'INSTALL'], if (verbose) '--verbose', ], - environment: {if (verbose) 'VERBOSE_SCRIPT_LOGGING': 'true'}, + environment: { + if (verbose) 'VERBOSE_SCRIPT_LOGGING': 'true' + }, onRun: onRun, stdout: stdout, ); @@ -129,7 +144,8 @@ void main() { setUpMockProjectFilesForBuild(); expect( - createTestCommandRunner(command).run(const ['windows', '--no-pub']), + createTestCommandRunner(command) + .run(const ['windows', '--no-pub']), throwsToolExit(), ); }, @@ -152,10 +168,10 @@ void main() { setUpMockCoreProjectFiles(); expect( - createTestCommandRunner(command).run(const ['windows', '--no-pub']), + createTestCommandRunner(command) + .run(const ['windows', '--no-pub']), throwsToolExit( - message: - 'No Windows desktop project configured. See ' + message: 'No Windows desktop project configured. See ' 'https://flutter.dev/to/add-desktop-support ' 'to learn about adding Windows support to a project.', ), @@ -180,8 +196,10 @@ void main() { setUpMockProjectFilesForBuild(); expect( - createTestCommandRunner(command).run(const ['windows', '--no-pub']), - throwsToolExit(message: '"build windows" only supported on Windows hosts.'), + createTestCommandRunner(command) + .run(const ['windows', '--no-pub']), + throwsToolExit( + message: '"build windows" only supported on Windows hosts.'), ); }, overrides: { @@ -203,7 +221,8 @@ void main() { setUpMockProjectFilesForBuild(); expect( - createTestCommandRunner(command).run(const ['windows', '--no-pub']), + createTestCommandRunner(command) + .run(const ['windows', '--no-pub']), throwsToolExit( message: '"build windows" is not currently supported. To enable, run "flutter config --enable-windows-desktop".', @@ -233,7 +252,8 @@ void main() { buildCommand('Release', stdout: 'STDOUT STUFF'), ]); - await createTestCommandRunner(command).run(const ['windows', '--no-pub']); + await createTestCommandRunner(command) + .run(const ['windows', '--no-pub']); expect(testLogger.statusText, isNot(contains('STDOUT STUFF'))); expect(testLogger.traceText, contains('STDOUT STUFF')); }, @@ -260,7 +280,8 @@ void main() { buildCommand('Release'), ]); - await createTestCommandRunner(command).run(const ['windows', '--no-pub']); + await createTestCommandRunner(command) + .run(const ['windows', '--no-pub']); expect( analyticsTimingEventExists( @@ -330,7 +351,8 @@ C:\foo\windows\x64\runner\main.cpp(17,1): error C2065: 'Baz': undeclared identif buildCommand('Release', stdout: stdout), ]); - await createTestCommandRunner(command).run(const ['windows', '--no-pub']); + await createTestCommandRunner(command) + .run(const ['windows', '--no-pub']); // Just the warnings and errors should be surfaced. expect(testLogger.errorText, r''' C:\foo\windows\x64\runner\main.cpp(18): error C2220: the following warning is treated as an error [C:\foo\build\windows\x64\runner\test.vcxproj] @@ -363,7 +385,8 @@ C:\foo\windows\x64\runner\main.cpp(17,1): error C2065: 'Baz': undeclared identif buildCommand('Release', verbose: true, stdout: 'STDOUT STUFF'), ]); - await createTestCommandRunner(command).run(const ['windows', '--no-pub', '-v']); + await createTestCommandRunner(command) + .run(const ['windows', '--no-pub', '-v']); expect(testLogger.statusText, contains('STDOUT STUFF')); expect(testLogger.traceText, isNot(contains('STDOUT STUFF'))); }, @@ -378,7 +401,8 @@ C:\foo\windows\x64\runner\main.cpp(17,1): error C2065: 'Baz': undeclared identif testUsingContext( 'Windows build works around CMake generation bug', () async { - final FakeVisualStudio fakeVisualStudio = FakeVisualStudio(displayVersion: '17.1.0'); + final FakeVisualStudio fakeVisualStudio = + FakeVisualStudio(displayVersion: '17.1.0'); final BuildWindowsCommand command = BuildWindowsCommand( logger: BufferLogger.test(), operatingSystemUtils: FakeOperatingSystemUtils(), @@ -389,8 +413,12 @@ C:\foo\windows\x64\runner\main.cpp(17,1): error C2065: 'Baz': undeclared identif cmakeGenerationCommand(), buildCommand('Release'), ]); - fileSystem.file(fileSystem.path.join('lib', 'other.dart')).createSync(recursive: true); - fileSystem.file(fileSystem.path.join('foo', 'bar.sksl.json')).createSync(recursive: true); + fileSystem + .file(fileSystem.path.join('lib', 'other.dart')) + .createSync(recursive: true); + fileSystem + .file(fileSystem.path.join('foo', 'bar.sksl.json')) + .createSync(recursive: true); // Relevant portions of an incorrectly generated project, with some // irrelevant details removed for length. @@ -484,7 +512,8 @@ if %errorlevel% neq 0 goto :VCEnd assembleProject.createSync(recursive: true); assembleProject.writeAsStringSync(fakeBadProjectContent); - await createTestCommandRunner(command).run(const ['windows', '--no-pub']); + await createTestCommandRunner(command) + .run(const ['windows', '--no-pub']); final List projectLines = assembleProject.readAsLinesSync(); @@ -532,8 +561,12 @@ if %errorlevel% neq 0 goto :VCEnd cmakeGenerationCommand(), buildCommand('Release'), ]); - fileSystem.file(fileSystem.path.join('lib', 'other.dart')).createSync(recursive: true); - fileSystem.file(fileSystem.path.join('foo', 'bar.sksl.json')).createSync(recursive: true); + fileSystem + .file(fileSystem.path.join('lib', 'other.dart')) + .createSync(recursive: true); + fileSystem + .file(fileSystem.path.join('foo', 'bar.sksl.json')) + .createSync(recursive: true); await createTestCommandRunner(command).run(const [ 'windows', @@ -638,7 +671,8 @@ if %errorlevel% neq 0 goto :VCEnd await createTestCommandRunner( command, ).run(const ['windows', '--release', '--no-pub']); - expect(testLogger.statusText, contains(r'โœ“ Built build\windows\x64\runner\Release')); + expect(testLogger.statusText, + contains(r'โœ“ Built build\windows\x64\runner\Release')); }, overrides: { FileSystem: () => fileSystem, @@ -652,7 +686,8 @@ if %errorlevel% neq 0 goto :VCEnd 'Windows build passes correct generator', () async { const String generator = 'A different generator'; - final FakeVisualStudio fakeVisualStudio = FakeVisualStudio(cmakeGenerator: generator); + final FakeVisualStudio fakeVisualStudio = + FakeVisualStudio(cmakeGenerator: generator); final BuildWindowsCommand command = BuildWindowsCommand( logger: BufferLogger.test(), operatingSystemUtils: FakeOperatingSystemUtils(), @@ -695,7 +730,8 @@ if %errorlevel% neq 0 goto :VCEnd buildCommand('Release'), ]); - await createTestCommandRunner(command).run(const ['windows', '--no-pub']); + await createTestCommandRunner(command) + .run(const ['windows', '--no-pub']); final File cmakeConfig = fileSystem.currentDirectory .childDirectory('windows') @@ -743,7 +779,12 @@ if %errorlevel% neq 0 goto :VCEnd await createTestCommandRunner( command, - ).run(const ['windows', '--no-pub', '--build-name=1.2.3', '--build-number=4']); + ).run(const [ + 'windows', + '--no-pub', + '--build-name=1.2.3', + '--build-number=4' + ]); final File cmakeConfig = fileSystem.currentDirectory .childDirectory('windows') @@ -899,7 +940,12 @@ if %errorlevel% neq 0 goto :VCEnd await createTestCommandRunner( command, - ).run(const ['windows', '--no-pub', '--build-name=1.2.3', '--build-number=4']); + ).run(const [ + 'windows', + '--no-pub', + '--build-name=1.2.3', + '--build-number=4' + ]); final File cmakeConfig = fileSystem.currentDirectory .childDirectory('windows') @@ -947,7 +993,12 @@ if %errorlevel% neq 0 goto :VCEnd await createTestCommandRunner( command, - ).run(const ['windows', '--no-pub', '--build-name=1.2.3', '--build-number=hello']); + ).run(const [ + 'windows', + '--no-pub', + '--build-name=1.2.3', + '--build-number=hello' + ]); final File cmakeConfig = fileSystem.currentDirectory .childDirectory('windows') @@ -1004,7 +1055,12 @@ if %errorlevel% neq 0 goto :VCEnd await createTestCommandRunner( command, - ).run(const ['windows', '--no-pub', '--build-name=1.2.3', '--build-number=4.5']); + ).run(const [ + 'windows', + '--no-pub', + '--build-name=1.2.3', + '--build-number=4.5' + ]); final File cmakeConfig = fileSystem.currentDirectory .childDirectory('windows') @@ -1124,14 +1180,16 @@ if %errorlevel% neq 0 goto :VCEnd contains('A summary of your Windows bundle analysis can be found at'), ); expect(testLogger.statusText, contains('dart devtools --appSizeBase=')); - expect(fakeAnalytics.sentEvents, contains(Event.codeSizeAnalysis(platform: 'windows'))); + expect(fakeAnalytics.sentEvents, + contains(Event.codeSizeAnalysis(platform: 'windows'))); }, overrides: { FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true), FileSystem: () => fileSystem, ProcessManager: () => processManager, Platform: () => windowsPlatform, - FileSystemUtils: () => FileSystemUtils(fileSystem: fileSystem, platform: windowsPlatform), + FileSystemUtils: () => + FileSystemUtils(fileSystem: fileSystem, platform: windowsPlatform), Analytics: () => fakeAnalytics, }, ); @@ -1148,12 +1206,14 @@ if %errorlevel% neq 0 goto :VCEnd logger: BufferLogger.test(), operatingSystemUtils: FakeOperatingSystemUtils(), )..visualStudioOverride = fakeVisualStudio; - fileSystem.currentDirectory = fileSystem.directory("test_'path")..createSync(); + fileSystem.currentDirectory = fileSystem.directory("test_'path") + ..createSync(); final String absPath = fileSystem.currentDirectory.absolute.path; setUpMockCoreProjectFiles(); expect( - createTestCommandRunner(command).run(const ['windows', '--no-pub']), + createTestCommandRunner(command) + .run(const ['windows', '--no-pub']), throwsToolExit( message: 'Path $absPath contains invalid characters in "\'#!\$^&*=|,;<>?". ' @@ -1192,7 +1252,8 @@ No file or variants found for asset: images/a_dot_burr.jpeg. buildCommand('Release', stdout: stdout), ]); - await createTestCommandRunner(command).run(const ['windows', '--no-pub']); + await createTestCommandRunner(command) + .run(const ['windows', '--no-pub']); // Just the warnings and errors should be surfaced. expect(testLogger.errorText, r''' Error detected in pubspec.yaml: @@ -1206,6 +1267,39 @@ No file or variants found for asset: images/a_dot_burr.jpeg. FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true), }, ); + + testUsingContext( + 'shorebird.yaml is updated when SHOREBIRD_PUBLIC_KEY env var is set', + () async { + final FakeVisualStudio fakeVisualStudio = FakeVisualStudio(); + final BuildWindowsCommand command = BuildWindowsCommand( + logger: BufferLogger.test(), + operatingSystemUtils: FakeOperatingSystemUtils()) + ..visualStudioOverride = fakeVisualStudio; + setUpMockProjectFilesForBuild(); + final File shorebirdYamlFile = fileSystem.file( + r'build\windows\x64\runner\Release\data\flutter_assets\shorebird.yaml', + ) + ..createSync(recursive: true) + ..writeAsStringSync('app_id: my-app-id'); + + processManager = FakeProcessManager.list([ + cmakeGenerationCommand(), + buildCommand('Release'), + ]); + + await createTestCommandRunner(command) + .run(const ['windows', '--release', '--no-pub']); + + final String updatedYaml = shorebirdYamlFile.readAsStringSync(); + expect(updatedYaml, contains('app_id: my-app-id')); + expect(updatedYaml, contains('patch_public_key: my_public_key')); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + Platform: () => windowsPlatformWithPublicKey, + FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true), + }); } class FakeVisualStudio extends Fake implements VisualStudio { 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 6e595213ab3f1..218dfb0563324 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 @@ -144,7 +144,14 @@ void main() { ); lipoExtractX86_64Command = FakeCommand( - command: ['lipo', '-output', binary.path, '-extract', 'x86_64', binary.path], + command: [ + 'lipo', + '-output', + binary.path, + '-extract', + 'x86_64', + binary.path + ], ); }); @@ -172,13 +179,17 @@ void main() { 'deletes entitlements.txt, without_entitlements.txt, unsigned_binaries.txt files after copying', () async { binary.createSync(recursive: true); - final File entitlements = environment.outputDir.childFile('entitlements.txt'); - final File withoutEntitlements = environment.outputDir.childFile('without_entitlements.txt'); - final File unsignedBinaries = environment.outputDir.childFile('unsigned_binaries.txt'); + final File entitlements = + environment.outputDir.childFile('entitlements.txt'); + final File withoutEntitlements = + environment.outputDir.childFile('without_entitlements.txt'); + final File unsignedBinaries = + environment.outputDir.childFile('unsigned_binaries.txt'); final File nestedEntitlements = environment.outputDir - .childDirectory('first_level') - .childDirectory('second_level') - .childFile('entitlements.txt')..createSync(recursive: true); + .childDirectory('first_level') + .childDirectory('second_level') + .childFile('entitlements.txt') + ..createSync(recursive: true); processManager.addCommands([ FakeCommand( @@ -229,7 +240,8 @@ void main() { isException.having( (Exception exception) => exception.toString(), 'description', - contains('FlutterMacOS.framework/Versions/A/FlutterMacOS does not exist, cannot thin'), + contains( + 'FlutterMacOS.framework/Versions/A/FlutterMacOS does not exist, cannot thin'), ), ), ); @@ -249,7 +261,13 @@ void main() { copyFrameworkCommand, lipoInfoFatCommand, FakeCommand( - command: ['lipo', binary.path, '-verify_arch', 'arm64', 'x86_64'], + command: [ + 'lipo', + binary.path, + '-verify_arch', + 'arm64', + 'x86_64' + ], exitCode: 1, ), ]); @@ -285,7 +303,8 @@ void main() { expect( logger.traceText, - contains('Skipping lipo for non-fat file /FlutterMacOS.framework/Versions/A/FlutterMacOS'), + contains( + 'Skipping lipo for non-fat file /FlutterMacOS.framework/Versions/A/FlutterMacOS'), ); }); @@ -313,7 +332,8 @@ void main() { lipoVerifyX86_64Command, ]); - await const ReleaseUnpackMacOS().build(environment..defines[kBuildMode] = 'release'); + await const ReleaseUnpackMacOS() + .build(environment..defines[kBuildMode] = 'release'); expect(processManager, hasNoRemainingExpectations); }, @@ -335,7 +355,8 @@ void main() { copyFrameworkDsymCommand, ]); - await const ReleaseUnpackMacOS().build(environment..defines[kBuildMode] = 'release'); + await const ReleaseUnpackMacOS() + .build(environment..defines[kBuildMode] = 'release'); expect(processManager, hasNoRemainingExpectations); }, @@ -372,7 +393,8 @@ void main() { ]); await expectLater( - const ReleaseUnpackMacOS().build(environment..defines[kBuildMode] = 'release'), + const ReleaseUnpackMacOS() + .build(environment..defines[kBuildMode] = 'release'), throwsA( isException.having( (Exception exception) => exception.toString(), @@ -393,15 +415,19 @@ void main() { () async { fileSystem .directory( - artifacts.getArtifactPath(Artifact.flutterMacOSFramework, mode: BuildMode.debug), + artifacts.getArtifactPath(Artifact.flutterMacOSFramework, + mode: BuildMode.debug), ) .createSync(); - final String inputKernel = fileSystem.path.join(environment.buildDir.path, 'app.dill'); + final String inputKernel = + fileSystem.path.join(environment.buildDir.path, 'app.dill'); fileSystem.file(inputKernel) ..createSync(recursive: true) ..writeAsStringSync('testing'); - expect(() async => const DebugMacOSBundleFlutterAssets().build(environment), throwsException); + expect( + () async => const DebugMacOSBundleFlutterAssets().build(environment), + throwsException); }, overrides: { FileSystem: () => fileSystem, @@ -414,7 +440,8 @@ void main() { () async { fileSystem .directory( - artifacts.getArtifactPath(Artifact.flutterMacOSFramework, mode: BuildMode.debug), + artifacts.getArtifactPath(Artifact.flutterMacOSFramework, + mode: BuildMode.debug), ) .createSync(); environment.defines[kBundleSkSLPath] = 'bundle.sksl'; @@ -436,11 +463,11 @@ void main() { ), ) .createSync(recursive: true); - fileSystem.file('${environment.buildDir.path}/App.framework/App').createSync(recursive: true); - // sksl bundle fileSystem - .file('bundle.sksl') - .writeAsStringSync( + .file('${environment.buildDir.path}/App.framework/App') + .createSync(recursive: true); + // sksl bundle + fileSystem.file('bundle.sksl').writeAsStringSync( json.encode({ 'engineRevision': '2', 'platform': 'ios', @@ -458,20 +485,25 @@ void main() { expect( fileSystem - .file('App.framework/Versions/A/Resources/flutter_assets/kernel_blob.bin') + .file( + 'App.framework/Versions/A/Resources/flutter_assets/kernel_blob.bin') .readAsStringSync(), 'testing', ); expect( - fileSystem.file('App.framework/Versions/A/Resources/Info.plist').readAsStringSync(), + fileSystem + .file('App.framework/Versions/A/Resources/Info.plist') + .readAsStringSync(), contains('io.flutter.flutter.app'), ); expect( - fileSystem.file('App.framework/Versions/A/Resources/flutter_assets/vm_snapshot_data'), + fileSystem.file( + 'App.framework/Versions/A/Resources/flutter_assets/vm_snapshot_data'), exists, ); expect( - fileSystem.file('App.framework/Versions/A/Resources/flutter_assets/isolate_snapshot_data'), + fileSystem.file( + 'App.framework/Versions/A/Resources/flutter_assets/isolate_snapshot_data'), exists, ); @@ -497,23 +529,30 @@ void main() { 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}/native_assets.json').createSync(); + fileSystem + .file('${environment.buildDir.path}/App.framework/App') + .createSync(recursive: true); + fileSystem + .file('${environment.buildDir.path}/native_assets.json') + .createSync(); await const ProfileMacOSBundleFlutterAssets().build( environment..defines[kBuildMode] = 'profile', ); expect( - fileSystem.file('App.framework/Versions/A/Resources/flutter_assets/kernel_blob.bin'), + fileSystem.file( + 'App.framework/Versions/A/Resources/flutter_assets/kernel_blob.bin'), isNot(exists), ); expect( - fileSystem.file('App.framework/Versions/A/Resources/flutter_assets/vm_snapshot_data'), + fileSystem.file( + 'App.framework/Versions/A/Resources/flutter_assets/vm_snapshot_data'), isNot(exists), ); expect( - fileSystem.file('App.framework/Versions/A/Resources/flutter_assets/isolate_snapshot_data'), + fileSystem.file( + 'App.framework/Versions/A/Resources/flutter_assets/isolate_snapshot_data'), isNot(exists), ); }, @@ -532,17 +571,23 @@ void main() { 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') + .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); - fileSystem.file('${environment.buildDir.path}/native_assets.json').createSync(); + fileSystem + .file('${environment.buildDir.path}/native_assets.json') + .createSync(); await const ReleaseMacOSBundleFlutterAssets().build( environment..defines[kBuildMode] = 'release', ); - expect(fileSystem.file('App.framework.dSYM/Contents/Resources/DWARF/App'), exists); + expect(fileSystem.file('App.framework.dSYM/Contents/Resources/DWARF/App'), + exists); }, overrides: { FileSystem: () => fileSystem, @@ -559,17 +604,20 @@ void main() { fileSystem .file('bin/cache/artifacts/engine/darwin-x64/isolate_snapshot.bin') .createSync(recursive: true); - final File inputFramework = - fileSystem.file(fileSystem.path.join(environment.buildDir.path, 'App.framework', 'App')) - ..createSync(recursive: true) - ..writeAsStringSync('ABC'); - fileSystem.file(environment.buildDir.childFile('native_assets.json')).createSync(); + final File inputFramework = fileSystem.file(fileSystem.path + .join(environment.buildDir.path, 'App.framework', 'App')) + ..createSync(recursive: true) + ..writeAsStringSync('ABC'); + fileSystem + .file(environment.buildDir.childFile('native_assets.json')) + .createSync(); await const ProfileMacOSBundleFlutterAssets().build( environment..defines[kBuildMode] = 'profile', ); final File outputFramework = fileSystem.file( - fileSystem.path.join(environment.outputDir.path, 'App.framework', 'App'), + fileSystem.path + .join(environment.outputDir.path, 'App.framework', 'App'), ); expect(outputFramework.readAsStringSync(), 'ABC'); @@ -600,14 +648,18 @@ void main() { .file('bin/cache/artifacts/engine/darwin-x64/isolate_snapshot.bin') .createSync(recursive: true); fileSystem - .file(fileSystem.path.join(environment.buildDir.path, 'App.framework', 'App')) + .file(fileSystem.path + .join(environment.buildDir.path, 'App.framework', 'App')) .createSync(recursive: true); - fileSystem.file(environment.buildDir.childFile('native_assets.json')).createSync(); + fileSystem + .file(environment.buildDir.childFile('native_assets.json')) + .createSync(); await const ReleaseMacOSBundleFlutterAssets().build(environment); expect( usage.events, - contains(const TestUsageEvent('assemble', 'macos-archive', label: 'success')), + contains(const TestUsageEvent('assemble', 'macos-archive', + label: 'success')), ); expect( fakeAnalytics.sentEvents, @@ -639,12 +691,14 @@ void main() { ); expect( usage.events, - contains(const TestUsageEvent('assemble', 'macos-archive', label: 'fail')), + contains( + const TestUsageEvent('assemble', 'macos-archive', label: 'fail')), ); expect( fakeAnalytics.sentEvents, contains( - Event.appleUsageEvent(workflow: 'assemble', parameter: 'macos-archive', result: 'fail'), + Event.appleUsageEvent( + workflow: 'assemble', parameter: 'macos-archive', result: 'fail'), ), ); }, @@ -681,7 +735,10 @@ void main() { '-install_name', '@rpath/App.framework/App', '-o', - environment.buildDir.childDirectory('App.framework').childFile('App').path, + environment.buildDir + .childDirectory('App.framework') + .childFile('App') + .path, ], ), ); @@ -695,6 +752,54 @@ void main() { }, ); + testUsingContext( + 'ReleaseMacOSBundleFlutterAssets updates shorebird.yaml if present', + () async { + environment.defines[kBuildMode] = 'release'; + environment.defines[kXcodeAction] = 'install'; + environment.defines[kFlavor] = 'internal'; + + 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(fileSystem.path + .join(environment.buildDir.path, 'App.framework', 'App')) + .createSync(recursive: true); + final String shorebirdYamlPath = fileSystem.path.join( + environment.buildDir.path, + 'App.framework', + 'Versions', + 'A', + 'Resources', + 'flutter_assets', + 'shorebird.yaml', + ); + fileSystem.file(fileSystem.path + .join(environment.buildDir.path, 'App.framework', 'App')) + ..createSync(recursive: true) + ..writeAsStringSync(''' +# Some other text that should be removed +app_id: base-app-id +flavors: + internal: internal-app-id + stable: stable-app-id +'''); + + await const ReleaseMacOSBundleFlutterAssets().build(environment); + + expect(fileSystem.file(shorebirdYamlPath).readAsStringSync(), + 'app_id: internal-app-id'); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }, + ); + testUsingContext( 'DebugMacOSFramework creates universal binary', () async { @@ -724,7 +829,10 @@ void main() { '-install_name', '@rpath/App.framework/App', '-o', - environment.buildDir.childDirectory('App.framework').childFile('App').path, + environment.buildDir + .childDirectory('App.framework') + .childFile('App') + .path, ], ), ); @@ -893,14 +1001,18 @@ void main() { command: [ 'lipo', environment.buildDir - .childFile('arm64/App.framework.dSYM/Contents/Resources/DWARF/App') + .childFile( + 'arm64/App.framework.dSYM/Contents/Resources/DWARF/App') .path, environment.buildDir - .childFile('x86_64/App.framework.dSYM/Contents/Resources/DWARF/App') + .childFile( + 'x86_64/App.framework.dSYM/Contents/Resources/DWARF/App') .path, '-create', '-output', - environment.buildDir.childFile('App.framework.dSYM/Contents/Resources/DWARF/App').path, + environment.buildDir + .childFile('App.framework.dSYM/Contents/Resources/DWARF/App') + .path, ], ), ]); diff --git a/packages/flutter_tools/test/general.shard/cache_test.dart b/packages/flutter_tools/test/general.shard/cache_test.dart index c6f510be2daf5..5b42a59972ba1 100644 --- a/packages/flutter_tools/test/general.shard/cache_test.dart +++ b/packages/flutter_tools/test/general.shard/cache_test.dart @@ -97,15 +97,19 @@ void main() { fileSystem: fileSystem, processManager: FakeProcessManager.any(), ); - fileSystem.file(fileSystem.path.join('bin', 'cache', 'lockfile')).createSync(recursive: true); + fileSystem + .file(fileSystem.path.join('bin', 'cache', 'lockfile')) + .createSync(recursive: true); expect(() async => cache.lock(), throwsToolExit()); // TODO(zanderso): implement support for lock so this can be tested with the memory file system. }, skip: true); // https://github.com/flutter/flutter/issues/87923 - testWithoutContext('should not throw when FLUTTER_ALREADY_LOCKED is set', () { + testWithoutContext('should not throw when FLUTTER_ALREADY_LOCKED is set', + () { final Cache cache = Cache.test( - platform: FakePlatform(environment: {'FLUTTER_ALREADY_LOCKED': 'true'}), + platform: FakePlatform( + environment: {'FLUTTER_ALREADY_LOCKED': 'true'}), processManager: FakeProcessManager.any(), ); @@ -117,23 +121,25 @@ void main() { testWithoutContext('Continues on failed stamp file update', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final BufferLogger logger = BufferLogger.test(); - final Directory artifactDir = fileSystem.systemTempDirectory.createTempSync( + final Directory artifactDir = + fileSystem.systemTempDirectory.createTempSync( 'flutter_cache_test_artifact.', ); - final Directory downloadDir = fileSystem.systemTempDirectory.createTempSync( + final Directory downloadDir = + fileSystem.systemTempDirectory.createTempSync( 'flutter_cache_test_download.', ); - final Cache cache = - FakeSecondaryCache() - ..version = 'asdasd' - ..artifactDirectory = artifactDir - ..downloadDir = downloadDir - ..onSetStamp = (String name, String version) { - throw const FileSystemException('stamp write failed'); - }; + final Cache cache = FakeSecondaryCache() + ..version = 'asdasd' + ..artifactDirectory = artifactDir + ..downloadDir = downloadDir + ..onSetStamp = (String name, String version) { + throw const FileSystemException('stamp write failed'); + }; final FakeSimpleArtifact artifact = FakeSimpleArtifact(cache); - await artifact.update(FakeArtifactUpdater(), logger, fileSystem, FakeOperatingSystemUtils()); + await artifact.update(FakeArtifactUpdater(), logger, fileSystem, + FakeOperatingSystemUtils()); expect(logger.warningText, contains('stamp write failed')); }); @@ -141,23 +147,25 @@ void main() { testWithoutContext('Continues on missing version file', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final BufferLogger logger = BufferLogger.test(); - final Directory artifactDir = fileSystem.systemTempDirectory.createTempSync( + final Directory artifactDir = + fileSystem.systemTempDirectory.createTempSync( 'flutter_cache_test_artifact.', ); - final Directory downloadDir = fileSystem.systemTempDirectory.createTempSync( + final Directory downloadDir = + fileSystem.systemTempDirectory.createTempSync( 'flutter_cache_test_download.', ); - final Cache cache = - FakeSecondaryCache() - ..version = - null // version is missing. - ..artifactDirectory = artifactDir - ..downloadDir = downloadDir; + final Cache cache = FakeSecondaryCache() + ..version = null // version is missing. + ..artifactDirectory = artifactDir + ..downloadDir = downloadDir; final FakeSimpleArtifact artifact = FakeSimpleArtifact(cache); - await artifact.update(FakeArtifactUpdater(), logger, fileSystem, FakeOperatingSystemUtils()); + await artifact.update(FakeArtifactUpdater(), logger, fileSystem, + FakeOperatingSystemUtils()); - expect(logger.warningText, contains('No known version for the artifact name "fake"')); + expect(logger.warningText, + contains('No known version for the artifact name "fake"')); }); testWithoutContext( @@ -173,22 +181,25 @@ void main() { fileSystem.path.join('artifacts', 'gradle_wrapper'), ); fileSystem - .file(fileSystem.path.join(directory.path, 'gradle', 'wrapper', 'gradle-wrapper.jar')) + .file(fileSystem.path.join( + directory.path, 'gradle', 'wrapper', 'gradle-wrapper.jar')) .createSync(recursive: true); expect(gradleWrapper.isUpToDateInner(fileSystem), false); }, ); - testWithoutContext('Gradle wrapper will delete .properties/NOTICES if they exist', () async { + testWithoutContext( + 'Gradle wrapper will delete .properties/NOTICES if they exist', + () async { final FileSystem fileSystem = MemoryFileSystem.test(); - final Directory artifactDir = fileSystem.systemTempDirectory.createTempSync( + final Directory artifactDir = + fileSystem.systemTempDirectory.createTempSync( 'flutter_cache_test_artifact.', ); - final FakeSecondaryCache cache = - FakeSecondaryCache() - ..artifactDirectory = artifactDir - ..version = '123456'; + final FakeSecondaryCache cache = FakeSecondaryCache() + ..artifactDirectory = artifactDir + ..version = '123456'; final OperatingSystemUtils operatingSystemUtils = OperatingSystemUtils( processManager: FakeProcessManager.any(), @@ -198,12 +209,15 @@ void main() { ); final GradleWrapper gradleWrapper = GradleWrapper(cache); final File propertiesFile = fileSystem.file( - fileSystem.path.join(artifactDir.path, 'gradle', 'wrapper', 'gradle-wrapper.properties'), + fileSystem.path.join( + artifactDir.path, 'gradle', 'wrapper', 'gradle-wrapper.properties'), )..createSync(recursive: true); - final File noticeFile = fileSystem.file(fileSystem.path.join(artifactDir.path, 'NOTICE')) + final File noticeFile = fileSystem + .file(fileSystem.path.join(artifactDir.path, 'NOTICE')) ..createSync(recursive: true); - await gradleWrapper.updateInner(FakeArtifactUpdater(), fileSystem, operatingSystemUtils); + await gradleWrapper.updateInner( + FakeArtifactUpdater(), fileSystem, operatingSystemUtils); expect(propertiesFile, isNot(exists)); expect(noticeFile, isNot(exists)); @@ -222,7 +236,8 @@ void main() { fileSystem.path.join('artifacts', 'gradle_wrapper'), ); fileSystem - .file(fileSystem.path.join(directory.path, 'gradle', 'wrapper', 'gradle-wrapper.jar')) + .file(fileSystem.path.join( + directory.path, 'gradle', 'wrapper', 'gradle-wrapper.jar')) .createSync(recursive: true); fileSystem .file(fileSystem.path.join(directory.path, 'gradlew')) @@ -235,9 +250,12 @@ void main() { }, ); - testWithoutContext('should not be up to date, if some cached artifact is not', () async { - final CachedArtifact artifact1 = FakeSecondaryCachedArtifact()..upToDate = true; - final CachedArtifact artifact2 = FakeSecondaryCachedArtifact()..upToDate = false; + testWithoutContext( + 'should not be up to date, if some cached artifact is not', () async { + final CachedArtifact artifact1 = FakeSecondaryCachedArtifact() + ..upToDate = true; + final CachedArtifact artifact2 = FakeSecondaryCachedArtifact() + ..upToDate = false; final FileSystem fileSystem = MemoryFileSystem.test(); final Cache cache = Cache.test( @@ -249,9 +267,12 @@ void main() { expect(await cache.isUpToDate(), isFalse); }); - testWithoutContext('should be up to date, if all cached artifacts are', () async { - final FakeSecondaryCachedArtifact artifact1 = FakeSecondaryCachedArtifact()..upToDate = true; - final FakeSecondaryCachedArtifact artifact2 = FakeSecondaryCachedArtifact()..upToDate = true; + testWithoutContext('should be up to date, if all cached artifacts are', + () async { + final FakeSecondaryCachedArtifact artifact1 = + FakeSecondaryCachedArtifact()..upToDate = true; + final FakeSecondaryCachedArtifact artifact2 = + FakeSecondaryCachedArtifact()..upToDate = true; final FileSystem fileSystem = MemoryFileSystem.test(); final Cache cache = Cache.test( fileSystem: fileSystem, @@ -262,9 +283,12 @@ void main() { expect(await cache.isUpToDate(), isTrue); }); - testWithoutContext('should update cached artifacts which are not up to date', () async { - final FakeSecondaryCachedArtifact artifact1 = FakeSecondaryCachedArtifact()..upToDate = true; - final FakeSecondaryCachedArtifact artifact2 = FakeSecondaryCachedArtifact()..upToDate = false; + testWithoutContext( + 'should update cached artifacts which are not up to date', () async { + final FakeSecondaryCachedArtifact artifact1 = + FakeSecondaryCachedArtifact()..upToDate = true; + final FakeSecondaryCachedArtifact artifact2 = + FakeSecondaryCachedArtifact()..upToDate = false; final FileSystem fileSystem = MemoryFileSystem.test(); final Cache cache = Cache.test( @@ -273,7 +297,8 @@ void main() { processManager: FakeProcessManager.any(), ); - await cache.updateAll({DevelopmentArtifact.universal}); + await cache + .updateAll({DevelopmentArtifact.universal}); expect(artifact1.didUpdate, false); expect(artifact2.didUpdate, true); }); @@ -304,14 +329,17 @@ void main() { }, ); - testWithoutContext('failed storage.googleapis.com download shows China warning', () async { + testWithoutContext( + 'failed storage.googleapis.com download shows China warning', () async { final InternetAddress address = (await InternetAddress.lookup('storage.googleapis.com')).first; - final FakeSecondaryCachedArtifact artifact1 = FakeSecondaryCachedArtifact()..upToDate = false; + final FakeSecondaryCachedArtifact artifact1 = + FakeSecondaryCachedArtifact()..upToDate = false; final FakeSecondaryCachedArtifact artifact2 = FakeSecondaryCachedArtifact() ..upToDate = false - ..updateException = SocketException('Connection reset by peer', address: address); + ..updateException = + SocketException('Connection reset by peer', address: address); final BufferLogger logger = BufferLogger.test(); final Cache cache = Cache.test( @@ -320,7 +348,8 @@ void main() { logger: logger, ); await expectLater( - () => cache.updateAll({DevelopmentArtifact.universal}), + () => cache + .updateAll({DevelopmentArtifact.universal}), throwsException, ); expect(artifact1.didUpdate, true); @@ -329,10 +358,13 @@ void main() { expect(logger.errorText, contains('https://flutter.dev/to/china-setup')); }); - testWithoutContext('Invalid URI for FLUTTER_STORAGE_BASE_URL throws ToolExit', () async { + testWithoutContext( + 'Invalid URI for FLUTTER_STORAGE_BASE_URL throws ToolExit', () async { final Cache cache = Cache.test( platform: FakePlatform( - environment: {'FLUTTER_STORAGE_BASE_URL': ' http://foo'}, + environment: { + 'FLUTTER_STORAGE_BASE_URL': ' http://foo' + }, ), processManager: FakeProcessManager.any(), ); @@ -344,17 +376,20 @@ void main() { final BufferLogger logger = BufferLogger.test(); const String baseUrl = 'https://storage.com'; final Cache cache = Cache.test( - platform: FakePlatform(environment: {'FLUTTER_STORAGE_BASE_URL': baseUrl}), + platform: FakePlatform( + environment: {'FLUTTER_STORAGE_BASE_URL': baseUrl}), processManager: FakeProcessManager.any(), logger: logger, ); expect(cache.storageBaseUrl, baseUrl); - expect(logger.warningText, contains('Flutter assets will be downloaded from $baseUrl')); + expect(logger.warningText, + contains('Flutter assets will be downloaded from $baseUrl')); expect(logger.statusText, isEmpty); }); - testWithoutContext('a non-empty realm is included in the storage url', () async { + testWithoutContext('a non-empty realm is included in the storage url', + () async { final MemoryFileSystem fileSystem = MemoryFileSystem.test(); final Directory internalDir = fileSystem.currentDirectory .childDirectory('bin') @@ -378,15 +413,18 @@ void main() { testWithoutContext('flattenNameSubdirs', () { expect( - flattenNameSubdirs(Uri.parse('http://flutter.dev/foo/bar'), MemoryFileSystem.test()), + flattenNameSubdirs( + Uri.parse('http://flutter.dev/foo/bar'), MemoryFileSystem.test()), 'flutter.dev/foo/bar', ); expect( - flattenNameSubdirs(Uri.parse('http://api.flutter.dev/foo/bar'), MemoryFileSystem.test()), + flattenNameSubdirs( + Uri.parse('http://api.flutter.dev/foo/bar'), MemoryFileSystem.test()), 'api.flutter.dev/foo/bar', ); expect( - flattenNameSubdirs(Uri.parse('https://www.flutter.dev'), MemoryFileSystem.test()), + flattenNameSubdirs( + Uri.parse('https://www.flutter.dev'), MemoryFileSystem.test()), 'www.flutter.dev', ); }); @@ -394,18 +432,20 @@ void main() { testWithoutContext( 'EngineCachedArtifact makes binary dirs readable and executable by all', () async { - final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils(); + final FakeOperatingSystemUtils operatingSystemUtils = + FakeOperatingSystemUtils(); final FileSystem fileSystem = MemoryFileSystem.test(); - final Directory artifactDir = fileSystem.systemTempDirectory.createTempSync( + final Directory artifactDir = + fileSystem.systemTempDirectory.createTempSync( 'flutter_cache_test_artifact.', ); - final Directory downloadDir = fileSystem.systemTempDirectory.createTempSync( + final Directory downloadDir = + fileSystem.systemTempDirectory.createTempSync( 'flutter_cache_test_download.', ); - final FakeSecondaryCache cache = - FakeSecondaryCache() - ..artifactDirectory = artifactDir - ..downloadDir = downloadDir; + final FakeSecondaryCache cache = FakeSecondaryCache() + ..artifactDirectory = artifactDir + ..downloadDir = downloadDir; artifactDir.childDirectory('bin_dir').createSync(); artifactDir.childFile('unused_url_path').createSync(); @@ -416,23 +456,29 @@ void main() { ], requiredArtifacts: DevelopmentArtifact.universal, ); - await artifact.updateInner(FakeArtifactUpdater(), fileSystem, operatingSystemUtils); - final Directory dir = - fileSystem.systemTempDirectory - .listSync(recursive: true) - .whereType() - .singleWhereOrNull((Directory directory) => directory.basename == 'bin_dir')!; + await artifact.updateInner( + FakeArtifactUpdater(), fileSystem, operatingSystemUtils); + final Directory dir = fileSystem.systemTempDirectory + .listSync(recursive: true) + .whereType() + .singleWhereOrNull( + (Directory directory) => directory.basename == 'bin_dir')!; expect(dir, isNotNull); expect(dir.path, artifactDir.childDirectory('bin_dir').path); expect(operatingSystemUtils.chmods, >[ - ['/.tmp_rand0/flutter_cache_test_artifact.rand0/bin_dir', 'a+r,a+x'], + [ + '/.tmp_rand0/flutter_cache_test_artifact.rand0/bin_dir', + 'a+r,a+x' + ], ]); }, ); - testWithoutContext('EngineCachedArtifact downloads package zip from expected URL', () async { - final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils(); + testWithoutContext( + 'EngineCachedArtifact downloads package zip from expected URL', () async { + final FakeOperatingSystemUtils operatingSystemUtils = + FakeOperatingSystemUtils(); final FileSystem fileSystem = MemoryFileSystem.test(); final Directory artifactDir = fileSystem.systemTempDirectory.createTempSync( 'flutter_cache_test_artifact.', @@ -440,10 +486,9 @@ void main() { final Directory downloadDir = fileSystem.systemTempDirectory.createTempSync( 'flutter_cache_test_download.', ); - final FakeSecondaryCache cache = - FakeSecondaryCache() - ..artifactDirectory = artifactDir - ..downloadDir = downloadDir; + final FakeSecondaryCache cache = FakeSecondaryCache() + ..artifactDirectory = artifactDir + ..downloadDir = downloadDir; artifactDir.childDirectory('pkg').createSync(); final FakeCachedArtifact artifact = FakeCachedArtifact( @@ -454,25 +499,25 @@ void main() { ); Uri? packageUrl; - final ArtifactUpdater artifactUpdater = - FakeArtifactUpdater() - ..onDownloadZipArchive = (String message, Uri url, Directory location) { - location.childDirectory('package_dir').createSync(); - packageUrl = url; - }; - - await artifact.updateInner(artifactUpdater, fileSystem, operatingSystemUtils); + final ArtifactUpdater artifactUpdater = FakeArtifactUpdater() + ..onDownloadZipArchive = (String message, Uri url, Directory location) { + location.childDirectory('package_dir').createSync(); + packageUrl = url; + }; + + await artifact.updateInner( + artifactUpdater, fileSystem, operatingSystemUtils); expect(packageUrl, isNotNull); expect( packageUrl.toString(), 'https://storage.googleapis.com/flutter_infra_release/flutter/null/package_dir.zip', ); - final Directory dir = - fileSystem.systemTempDirectory - .listSync(recursive: true) - .whereType() - .singleWhereOrNull((Directory directory) => directory.basename == 'pkg')!; + final Directory dir = fileSystem.systemTempDirectory + .listSync(recursive: true) + .whereType() + .singleWhereOrNull( + (Directory directory) => directory.basename == 'pkg')!; expect(dir.path, artifactDir.childDirectory('pkg').path); expect(dir.childDirectory('package_dir').existsSync(), isTrue); }); @@ -485,7 +530,8 @@ void main() { child.createSync(); final Directory tempStorage = parent.childDirectory('temp'); tempStorage.createSync(); - final FakeArtifactUpdaterDownload fakeArtifact = FakeArtifactUpdaterDownload( + final FakeArtifactUpdaterDownload fakeArtifact = + FakeArtifactUpdaterDownload( operatingSystemUtils: FakeOperatingSystemUtils(), logger: BufferLogger.test(), fileSystem: fileSystem, @@ -515,8 +561,8 @@ void main() { platform: FakePlatform(operatingSystem: 'macos'), ); iosUsbArtifacts.location.createSync(); - final File ideviceScreenshotFile = iosUsbArtifacts.location.childFile('idevicescreenshot') - ..createSync(); + final File ideviceScreenshotFile = + iosUsbArtifacts.location.childFile('idevicescreenshot')..createSync(); iosUsbArtifacts.location.childFile('idevicesyslog').createSync(); expect(iosUsbArtifacts.isUpToDateInner(fileSystem), true); @@ -527,7 +573,9 @@ void main() { }, ); - testWithoutContext('IosUsbArtifacts verifies iproxy for usbmuxd in isUpToDateInner', () async { + testWithoutContext( + 'IosUsbArtifacts verifies iproxy for usbmuxd in isUpToDateInner', + () async { final FileSystem fileSystem = MemoryFileSystem.test(); final Cache cache = Cache.test( fileSystem: fileSystem, @@ -539,7 +587,8 @@ void main() { platform: FakePlatform(operatingSystem: 'macos'), ); iosUsbArtifacts.location.createSync(); - final File iproxy = iosUsbArtifacts.location.childFile('iproxy')..createSync(); + final File iproxy = iosUsbArtifacts.location.childFile('iproxy') + ..createSync(); expect(iosUsbArtifacts.isUpToDateInner(fileSystem), true); @@ -579,7 +628,8 @@ void main() { expect(iosUsbArtifacts.archiveUri.toString(), contains('/unsigned/')); }); - testWithoutContext('IosUsbArtifacts does not use unsigned when not specified', () async { + testWithoutContext('IosUsbArtifacts does not use unsigned when not specified', + () async { final Cache cache = Cache.test(processManager: FakeProcessManager.any()); final IosUsbArtifacts iosUsbArtifacts = IosUsbArtifacts( 'name', @@ -587,18 +637,22 @@ void main() { platform: FakePlatform(operatingSystem: 'macos'), ); - expect(iosUsbArtifacts.archiveUri.toString(), isNot(contains('/unsigned/'))); + expect( + iosUsbArtifacts.archiveUri.toString(), isNot(contains('/unsigned/'))); }); - testWithoutContext('FlutterRunnerDebugSymbols downloads Flutter runner debug symbols', () async { + testWithoutContext( + 'FlutterRunnerDebugSymbols downloads Flutter runner debug symbols', + () async { final FileSystem fileSystem = MemoryFileSystem.test(); - final Cache cache = - FakeSecondaryCache() - ..artifactDirectory = fileSystem.currentDirectory - ..version = '123456'; - - final FakeVersionedPackageResolver packageResolver = FakeVersionedPackageResolver(); - final FlutterRunnerDebugSymbols flutterRunnerDebugSymbols = FlutterRunnerDebugSymbols( + final Cache cache = FakeSecondaryCache() + ..artifactDirectory = fileSystem.currentDirectory + ..version = '123456'; + + final FakeVersionedPackageResolver packageResolver = + FakeVersionedPackageResolver(); + final FlutterRunnerDebugSymbols flutterRunnerDebugSymbols = + FlutterRunnerDebugSymbols( cache, packageResolver: packageResolver, platform: FakePlatform(), @@ -618,7 +672,8 @@ void main() { testWithoutContext('FontSubset in universal artifacts', () { final Cache cache = Cache.test(processManager: FakeProcessManager.any()); - final FontSubsetArtifacts artifacts = FontSubsetArtifacts(cache, platform: FakePlatform()); + final FontSubsetArtifacts artifacts = + FontSubsetArtifacts(cache, platform: FakePlatform()); expect(artifacts.developmentArtifact, DevelopmentArtifact.universal); }); @@ -627,7 +682,8 @@ void main() { fakeProcessManager.addCommand(unameCommandForX64); final Cache cache = createCache(FakePlatform()); - final FontSubsetArtifacts artifacts = FontSubsetArtifacts(cache, platform: FakePlatform()); + final FontSubsetArtifacts artifacts = + FontSubsetArtifacts(cache, platform: FakePlatform()); cache.includeAllPlatforms = false; expect(artifacts.getBinaryDirs(), >[ @@ -639,7 +695,8 @@ void main() { fakeProcessManager.addCommand(unameCommandForArm64); final Cache cache = createCache(FakePlatform()); - final FontSubsetArtifacts artifacts = FontSubsetArtifacts(cache, platform: FakePlatform()); + final FontSubsetArtifacts artifacts = + FontSubsetArtifacts(cache, platform: FakePlatform()); cache.includeAllPlatforms = false; expect(artifacts.getBinaryDirs(), >[ @@ -662,7 +719,8 @@ void main() { testWithoutContext('FontSubset artifacts on macos', () { fakeProcessManager.addCommands([ - const FakeCommand(command: ['which', 'sysctl'], stdout: '/sbin/sysctl'), + const FakeCommand( + command: ['which', 'sysctl'], stdout: '/sbin/sysctl'), const FakeCommand( command: ['sysctl', 'hw.optional.arm64'], stdout: 'hw.optional.arm64: 0', @@ -714,7 +772,8 @@ void main() { ]); }); - testWithoutContext('FontSubset artifacts for all platforms on arm64 hosts', () { + testWithoutContext('FontSubset artifacts for all platforms on arm64 hosts', + () { fakeProcessManager.addCommand(unameCommandForArm64); final Cache cache = createCache(FakePlatform(operatingSystem: 'fuchsia')); @@ -731,9 +790,11 @@ void main() { ]); }); - testWithoutContext('macOS desktop artifacts include all gen_snapshot binaries', () { + testWithoutContext( + 'macOS desktop artifacts include all gen_snapshot binaries', () { final Cache cache = Cache.test(processManager: FakeProcessManager.any()); - final MacOSEngineArtifacts artifacts = MacOSEngineArtifacts(cache, platform: FakePlatform()); + final MacOSEngineArtifacts artifacts = + MacOSEngineArtifacts(cache, platform: FakePlatform()); cache.includeAllPlatforms = false; cache.platformOverrideArtifacts = {'macos'}; @@ -747,16 +808,19 @@ void main() { ); }); - testWithoutContext('macOS desktop artifacts ignore filtering when requested', () { + testWithoutContext('macOS desktop artifacts ignore filtering when requested', + () { final Cache cache = Cache.test(processManager: FakeProcessManager.any()); - final MacOSEngineArtifacts artifacts = MacOSEngineArtifacts(cache, platform: FakePlatform()); + final MacOSEngineArtifacts artifacts = + MacOSEngineArtifacts(cache, platform: FakePlatform()); cache.includeAllPlatforms = false; cache.platformOverrideArtifacts = {'macos'}; expect(artifacts.getBinaryDirs(), isNotEmpty); }); - testWithoutContext('Windows desktop artifacts ignore filtering when requested', () { + testWithoutContext( + 'Windows desktop artifacts ignore filtering when requested', () { final Cache cache = Cache.test(processManager: FakeProcessManager.any()); final WindowsEngineArtifacts artifacts = WindowsEngineArtifacts( cache, @@ -768,7 +832,8 @@ void main() { expect(artifacts.getBinaryDirs(), isNotEmpty); }); - testWithoutContext('Windows desktop artifacts include profile and release artifacts', () { + testWithoutContext( + 'Windows desktop artifacts include profile and release artifacts', () { final Cache cache = Cache.test(processManager: FakeProcessManager.any()); final WindowsEngineArtifacts artifacts = WindowsEngineArtifacts( cache, @@ -777,11 +842,15 @@ void main() { expect( artifacts.getBinaryDirs(), - containsAll([contains(contains('profile')), contains(contains('release'))]), + containsAll([ + contains(contains('profile')), + contains(contains('release')) + ]), ); }); - testWithoutContext('Linux desktop artifacts ignore filtering when requested', () { + testWithoutContext('Linux desktop artifacts ignore filtering when requested', + () { fakeProcessManager.addCommand(unameCommandForX64); final Cache cache = createCache(FakePlatform()); @@ -795,29 +864,47 @@ void main() { expect(artifacts.getBinaryDirs(), isNotEmpty); }); - testWithoutContext('Linux desktop artifacts for x64 include profile and release artifacts', () { + testWithoutContext( + 'Linux desktop artifacts for x64 include profile and release artifacts', + () { fakeProcessManager.addCommand(unameCommandForX64); final Cache cache = createCache(FakePlatform()); - final LinuxEngineArtifacts artifacts = LinuxEngineArtifacts(cache, platform: FakePlatform()); + final LinuxEngineArtifacts artifacts = + LinuxEngineArtifacts(cache, platform: FakePlatform()); expect(artifacts.getBinaryDirs(), >[ ['linux-x64', 'linux-x64-debug/linux-x64-flutter-gtk.zip'], - ['linux-x64-profile', 'linux-x64-profile/linux-x64-flutter-gtk.zip'], - ['linux-x64-release', 'linux-x64-release/linux-x64-flutter-gtk.zip'], + [ + 'linux-x64-profile', + 'linux-x64-profile/linux-x64-flutter-gtk.zip' + ], + [ + 'linux-x64-release', + 'linux-x64-release/linux-x64-flutter-gtk.zip' + ], ]); }); - testWithoutContext('Linux desktop artifacts for arm64 include profile and release artifacts', () { + testWithoutContext( + 'Linux desktop artifacts for arm64 include profile and release artifacts', + () { fakeProcessManager.addCommand(unameCommandForArm64); final Cache cache = createCache(FakePlatform()); - final LinuxEngineArtifacts artifacts = LinuxEngineArtifacts(cache, platform: FakePlatform()); + final LinuxEngineArtifacts artifacts = + LinuxEngineArtifacts(cache, platform: FakePlatform()); expect(artifacts.getBinaryDirs(), >[ ['linux-arm64', 'linux-arm64-debug/linux-arm64-flutter-gtk.zip'], - ['linux-arm64-profile', 'linux-arm64-profile/linux-arm64-flutter-gtk.zip'], - ['linux-arm64-release', 'linux-arm64-release/linux-arm64-flutter-gtk.zip'], + [ + 'linux-arm64-profile', + 'linux-arm64-profile/linux-arm64-flutter-gtk.zip' + ], + [ + 'linux-arm64-release', + 'linux-arm64-release/linux-arm64-flutter-gtk.zip' + ], ]); }); @@ -847,7 +934,8 @@ void main() { expect(toolStampFile, isNot(exists)); }); - testWithoutContext('Cache does not attempt to delete already missing stamp files', () { + testWithoutContext( + 'Cache does not attempt to delete already missing stamp files', () { final FileSystem fileSystem = MemoryFileSystem.test(); final FakeIosUsbArtifacts artifactSet = FakeIosUsbArtifacts(); final BufferLogger logger = BufferLogger.test(); @@ -872,7 +960,8 @@ void main() { expect(toolStampFile, isNot(exists)); }); - testWithoutContext('Cache catches file system exception from missing tool stamp file', () { + testWithoutContext( + 'Cache catches file system exception from missing tool stamp file', () { final FileSystem fileSystem = MemoryFileSystem.test(); final FakeIosUsbArtifacts artifactSet = FakeIosUsbArtifacts(); final BufferLogger logger = BufferLogger.test(); @@ -899,7 +988,8 @@ void main() { final Directory internalDir = fileSystem.currentDirectory .childDirectory('bin') .childDirectory('internal'); - final File canvasKitVersionFile = internalDir.childFile('canvaskit.version'); + final File canvasKitVersionFile = + internalDir.childFile('canvaskit.version'); canvasKitVersionFile.createSync(recursive: true); canvasKitVersionFile.writeAsStringSync('abcdefg'); @@ -918,7 +1008,8 @@ void main() { final List messages = []; final List downloads = []; final List locations = []; - artifactUpdater.onDownloadZipArchive = (String message, Uri uri, Directory location) { + artifactUpdater.onDownloadZipArchive = + (String message, Uri uri, Directory location) { messages.add(message); downloads.add(uri.toString()); locations.add(location.path); @@ -927,7 +1018,8 @@ void main() { }; webCacheDirectory.childFile('bar').createSync(recursive: true); - await webSdk.updateInner(artifactUpdater, fileSystem, FakeOperatingSystemUtils()); + await webSdk.updateInner( + artifactUpdater, fileSystem, FakeOperatingSystemUtils()); expect(messages, ['Downloading Web SDK...']); @@ -949,7 +1041,8 @@ void main() { final Directory internalDir = fileSystem.currentDirectory .childDirectory('bin') .childDirectory('internal'); - final File canvasKitVersionFile = internalDir.childFile('canvaskit.version'); + final File canvasKitVersionFile = + internalDir.childFile('canvaskit.version'); canvasKitVersionFile.createSync(recursive: true); canvasKitVersionFile.writeAsStringSync('abcdefg'); @@ -972,7 +1065,8 @@ void main() { final List downloads = []; final List locations = []; - artifactUpdater.onDownloadZipArchive = (String message, Uri uri, Directory location) { + artifactUpdater.onDownloadZipArchive = + (String message, Uri uri, Directory location) { downloads.add(uri.toString()); locations.add(location.path); location.createSync(recursive: true); @@ -980,7 +1074,8 @@ void main() { }; webCacheDirectory.childFile('bar').createSync(recursive: true); - await webSdk.updateInner(artifactUpdater, fileSystem, FakeOperatingSystemUtils()); + await webSdk.updateInner( + artifactUpdater, fileSystem, FakeOperatingSystemUtils()); expect(downloads, [ 'https://flutter.storage.com/override/flutter_infra_release/flutter/hijklmnop/flutter-web-sdk.zip', @@ -988,9 +1083,33 @@ void main() { }, ); - testWithoutContext('FlutterWebSdk uses tryToDelete to handle directory edge cases', () async { + testWithoutContext( + 'No warning is logged when FLUTTER_STORAGE_BASE_URL points to $kShorebirdStorageUrl', + () async { + final MemoryFileSystem fileSystem = MemoryFileSystem.test(); + final BufferLogger logger = BufferLogger.test(); + final Cache cache = Cache.test( + logger: logger, + processManager: FakeProcessManager.any(), + fileSystem: fileSystem, + platform: FakePlatform( + environment: { + 'FLUTTER_STORAGE_BASE_URL': kShorebirdStorageUrl, + }, + ), + ); + + expect(cache.storageBaseUrl, kShorebirdStorageUrl); + expect(logger.warningText, isEmpty); + }, + ); + + testWithoutContext( + 'FlutterWebSdk uses tryToDelete to handle directory edge cases', + () async { final FileExceptionHandler handler = FileExceptionHandler(); - final MemoryFileSystem fileSystem = MemoryFileSystem.test(opHandle: handler.opHandle); + final MemoryFileSystem fileSystem = + MemoryFileSystem.test(opHandle: handler.opHandle); final Cache cache = Cache.test( processManager: FakeProcessManager.any(), fileSystem: fileSystem, @@ -999,7 +1118,8 @@ void main() { final FakeArtifactUpdater artifactUpdater = FakeArtifactUpdater(); final FlutterWebSdk webSdk = FlutterWebSdk(cache); - artifactUpdater.onDownloadZipArchive = (String message, Uri uri, Directory location) { + artifactUpdater.onDownloadZipArchive = + (String message, Uri uri, Directory location) { location.createSync(recursive: true); location.childFile('foo').createSync(); }; @@ -1011,9 +1131,11 @@ void main() { ); await expectLater( - () => webSdk.updateInner(artifactUpdater, fileSystem, FakeOperatingSystemUtils()), + () => webSdk.updateInner( + artifactUpdater, fileSystem, FakeOperatingSystemUtils()), throwsToolExit( - message: RegExp('Unable to delete file or directory at "/bin/cache/flutter_web_sdk"'), + message: RegExp( + 'Unable to delete file or directory at "/bin/cache/flutter_web_sdk"'), ), ); }); @@ -1022,13 +1144,15 @@ void main() { 'LegacyCanvasKitRemover removes old canvaskit artifacts if they exist', () async { final FileExceptionHandler handler = FileExceptionHandler(); - final MemoryFileSystem fileSystem = MemoryFileSystem.test(opHandle: handler.opHandle); + final MemoryFileSystem fileSystem = + MemoryFileSystem.test(opHandle: handler.opHandle); final Cache cache = Cache.test( processManager: FakeProcessManager.any(), fileSystem: fileSystem, ); final File canvasKitWasm = fileSystem.file( - fileSystem.path.join(cache.getRoot().path, 'canvaskit', 'canvaskit.wasm'), + fileSystem.path + .join(cache.getRoot().path, 'canvaskit', 'canvaskit.wasm'), ); canvasKitWasm.createSync(recursive: true); canvasKitWasm.writeAsStringSync('hello world'); @@ -1047,9 +1171,11 @@ void main() { }, ); - testWithoutContext('Cache handles exception thrown if stamp file cannot be parsed', () { + testWithoutContext( + 'Cache handles exception thrown if stamp file cannot be parsed', () { final FileExceptionHandler exceptionHandler = FileExceptionHandler(); - final FileSystem fileSystem = MemoryFileSystem.test(opHandle: exceptionHandler.opHandle); + final FileSystem fileSystem = + MemoryFileSystem.test(opHandle: exceptionHandler.opHandle); final Logger logger = BufferLogger.test(); final FakeCache cache = FakeCache( fileSystem: fileSystem, @@ -1063,7 +1189,8 @@ void main() { expect(cache.getStampFor('foo'), null); file.createSync(); - exceptionHandler.addError(file, FileSystemOp.read, const FileSystemException()); + exceptionHandler.addError( + file, FileSystemOp.read, const FileSystemException()); expect(cache.getStampFor('foo'), null); }); @@ -1084,7 +1211,8 @@ void main() { expect(cache.getStampFor('foo'), 'ABC'); }); - testWithoutContext('PubDependencies needs to be updated if the package config' + testWithoutContext( + 'PubDependencies needs to be updated if the package config' ' file or the source directories are missing', () async { final BufferLogger logger = BufferLogger.test(); final MemoryFileSystem fileSystem = MemoryFileSystem.test(); @@ -1095,7 +1223,8 @@ void main() { projectFactory: FakeFlutterProjectFactory(), ); - expect(await pubDependencies.isUpToDate(fileSystem), false); // no package config + expect(await pubDependencies.isUpToDate(fileSystem), + false); // no package config fileSystem.file('packages/flutter_tools/.dart_tool/package_config.json') ..createSync(recursive: true) @@ -1116,7 +1245,8 @@ void main() { } '''); - expect(await pubDependencies.isUpToDate(fileSystem), false); // dependencies are missing. + expect(await pubDependencies.isUpToDate(fileSystem), + false); // dependencies are missing. fileSystem .file('.pub-cache/hosted/pub.dartlang.org/example-7.0.0/pubspec.yaml') @@ -1147,7 +1277,8 @@ void main() { expect( pub.invocations.first, predicate( - (FakePubInvocation invocation) => invocation.outputMode == PubOutputMode.failuresOnly, + (FakePubInvocation invocation) => + invocation.outputMode == PubOutputMode.failuresOnly, 'Pub invoked with PubOutputMode.none', ), ); @@ -1175,17 +1306,21 @@ void main() { setUp(() { memoryFileSystem = MemoryFileSystem.test(); - cache = Cache.test(fileSystem: memoryFileSystem, processManager: FakeProcessManager.any()); + cache = Cache.test( + fileSystem: memoryFileSystem, + processManager: FakeProcessManager.any()); fakeAndroidSdk = FakeAndroidSdk(); }); - testWithoutContext('AndroidMavenArtifacts has a specified development artifact', () async { + testWithoutContext( + 'AndroidMavenArtifacts has a specified development artifact', () async { final AndroidMavenArtifacts mavenArtifacts = AndroidMavenArtifacts( cache!, java: FakeJava(), platform: FakePlatform(), ); - expect(mavenArtifacts.developmentArtifact, DevelopmentArtifact.androidMaven); + expect( + mavenArtifacts.developmentArtifact, DevelopmentArtifact.androidMaven); }); testUsingContext( @@ -1201,10 +1336,13 @@ void main() { ); expect(await mavenArtifacts.isUpToDate(memoryFileSystem!), isFalse); - final Directory gradleWrapperDir = cache!.getArtifactDirectory('gradle_wrapper') + final Directory gradleWrapperDir = cache! + .getArtifactDirectory('gradle_wrapper') ..createSync(recursive: true); gradleWrapperDir.childFile('gradlew').writeAsStringSync('irrelevant'); - gradleWrapperDir.childFile('gradlew.bat').writeAsStringSync('irrelevant'); + gradleWrapperDir + .childFile('gradlew.bat') + .writeAsStringSync('irrelevant'); await mavenArtifacts.update( FakeArtifactUpdater(), @@ -1223,8 +1361,7 @@ void main() { Cache: () => cache, FileSystem: () => memoryFileSystem, Platform: () => FakePlatform(), - ProcessManager: - () => FakeProcessManager.list([ + ProcessManager: () => FakeProcessManager.list([ const FakeCommand( command: [ '/bin/cache/flutter_gradle_wrapper.rand0/gradlew', @@ -1294,7 +1431,8 @@ class FakeCachedArtifact extends EngineCachedArtifact { } class FakeSimpleArtifact extends CachedArtifact { - FakeSimpleArtifact(Cache cache) : super('fake', cache, DevelopmentArtifact.universal); + FakeSimpleArtifact(Cache cache) + : super('fake', cache, DevelopmentArtifact.universal); @override Future updateInner( @@ -1372,7 +1510,8 @@ class FakeSecondaryCache extends Fake implements Cache { } } -class FakeVersionedPackageResolver extends Fake implements VersionedPackageResolver { +class FakeVersionedPackageResolver extends Fake + implements VersionedPackageResolver { final List> resolved = >[]; @override @@ -1439,12 +1578,14 @@ class FakeArtifactUpdater extends Fake implements ArtifactUpdater { void Function(String, Uri, Directory)? onDownloadZipTarball; @override - Future downloadZippedTarball(String message, Uri url, Directory location) async { + Future downloadZippedTarball( + String message, Uri url, Directory location) async { onDownloadZipTarball?.call(message, url, location); } @override - Future downloadZipArchive(String message, Uri url, Directory location) async { + Future downloadZipArchive( + String message, Uri url, Directory location) async { onDownloadZipArchive?.call(message, url, location); } diff --git a/packages/flutter_tools/test/general.shard/shorebird/shorebird_yaml_test.dart b/packages/flutter_tools/test/general.shard/shorebird/shorebird_yaml_test.dart new file mode 100644 index 0000000000000..602de37681555 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/shorebird/shorebird_yaml_test.dart @@ -0,0 +1,101 @@ +// Copyright 2024 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 'dart:io'; + +import 'package:flutter_tools/src/shorebird/shorebird_yaml.dart'; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; + +void main() { + group('ShorebirdYaml', () { + test('yaml ignores comments', () { + const String yamlContents = ''' +# This file is used to configure the Shorebird updater used by your app. +app_id: 6160a7d8-cc18-4928-1233-05b51c0bb02c + +# auto_update controls if Shorebird should automatically update in the background on launch. +auto_update: false +'''; + final YamlDocument input = loadYamlDocument(yamlContents); + final YamlMap yamlMap = input.contents as YamlMap; + final Map compiled = + compileShorebirdYaml(yamlMap, flavor: null, environment: {}); + expect(compiled, { + 'app_id': '6160a7d8-cc18-4928-1233-05b51c0bb02c', + 'auto_update': false, + }); + }); + test('flavors', () { + // These are invalid app_ids but make for easy testing. + const String yamlContents = ''' +app_id: 1-a +auto_update: false +flavors: + foo: 2-a + bar: 3-a +'''; + final YamlDocument input = loadYamlDocument(yamlContents); + final YamlMap yamlMap = input.contents as YamlMap; + expect(appIdForFlavor(yamlMap, flavor: null), '1-a'); + expect(appIdForFlavor(yamlMap, flavor: 'foo'), '2-a'); + expect(appIdForFlavor(yamlMap, flavor: 'bar'), '3-a'); + expect(() => appIdForFlavor(yamlMap, flavor: 'unknown'), throwsException); + }); + test('all values', () { + // These are invalid app_ids but make for easy testing. + const String yamlContents = ''' +app_id: 1-a +auto_update: false +flavors: + foo: 2-a + bar: 3-a +base_url: https://example.com +'''; + final YamlDocument input = loadYamlDocument(yamlContents); + final YamlMap yamlMap = input.contents as YamlMap; + final Map compiled1 = + compileShorebirdYaml(yamlMap, flavor: null, environment: {}); + expect(compiled1, { + 'app_id': '1-a', + 'auto_update': false, + 'base_url': 'https://example.com', + }); + final Map compiled2 = + compileShorebirdYaml(yamlMap, flavor: 'foo', environment: {'SHOREBIRD_PUBLIC_KEY': '4-a'}); + expect(compiled2, { + 'app_id': '2-a', + 'auto_update': false, + 'base_url': 'https://example.com', + 'patch_public_key': '4-a', + }); + }); + test('edit in place', () { + const String yamlContents = ''' +app_id: 1-a +auto_update: false +flavors: + foo: 2-a + bar: 3-a +base_url: https://example.com +'''; + // Make a temporary file to test editing in place. + final Directory tempDir = Directory.systemTemp.createTempSync('shorebird_yaml_test.'); + final File tempFile = File('${tempDir.path}/shorebird.yaml'); + tempFile.writeAsStringSync(yamlContents); + updateShorebirdYaml( + 'foo', + tempFile.path, + environment: {'SHOREBIRD_PUBLIC_KEY': '4-a'}, + ); + final String updatedContents = tempFile.readAsStringSync(); + // Order is not guaranteed, so parse as YAML to compare. + final YamlDocument updated = loadYamlDocument(updatedContents); + final YamlMap yamlMap = updated.contents as YamlMap; + expect(yamlMap['app_id'], '2-a'); + expect(yamlMap['auto_update'], false); + expect(yamlMap['base_url'], 'https://example.com'); + }); + }); +} diff --git a/packages/shorebird_tests/.gitignore b/packages/shorebird_tests/.gitignore new file mode 100644 index 0000000000000..3a85790408401 --- /dev/null +++ b/packages/shorebird_tests/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/packages/shorebird_tests/README.md b/packages/shorebird_tests/README.md new file mode 100644 index 0000000000000..b87fb24a480da --- /dev/null +++ b/packages/shorebird_tests/README.md @@ -0,0 +1,2 @@ +A dart project that includes tests that perform asserts in the modifications +made on the Flutter framework by the Shorebird team. diff --git a/packages/shorebird_tests/analysis_options.yaml b/packages/shorebird_tests/analysis_options.yaml new file mode 100644 index 0000000000000..a767d79d7f4b1 --- /dev/null +++ b/packages/shorebird_tests/analysis_options.yaml @@ -0,0 +1,2 @@ +# This file configures the static analysis results for your project (errors, +include: package:lints/recommended.yaml diff --git a/packages/shorebird_tests/pubspec.yaml b/packages/shorebird_tests/pubspec.yaml new file mode 100644 index 0000000000000..d0fba0c1fa95d --- /dev/null +++ b/packages/shorebird_tests/pubspec.yaml @@ -0,0 +1,16 @@ +name: shorebird_tests +description: Shorebird's Flutter customizations tests +version: 1.0.0 + +environment: + sdk: ^3.3.4 + +dependencies: + archive: ^3.5.1 + path: ^1.9.0 + yaml: ^3.1.2 + +dev_dependencies: + lints: ^3.0.0 + meta: ^1.15.0 + test: ^1.24.0 diff --git a/packages/shorebird_tests/test/android_test.dart b/packages/shorebird_tests/test/android_test.dart new file mode 100644 index 0000000000000..b24cce0ee9bf6 --- /dev/null +++ b/packages/shorebird_tests/test/android_test.dart @@ -0,0 +1,92 @@ +import 'package:test/test.dart'; + +import 'shorebird_tests.dart'; + +void main() { + group('shorebird android projects', () { + testWithShorebirdProject('can build an apk', (projectDirectory) async { + await projectDirectory.runFlutterBuildApk(); + + expect(projectDirectory.apkFile().existsSync(), isTrue); + expect(projectDirectory.shorebirdFile.existsSync(), isTrue); + expect(projectDirectory.getGeneratedAndroidShorebirdYaml(), completes); + }); + + group('when passing the public key through the environment variable', () { + testWithShorebirdProject( + 'adds the public key on top of the original file', + (projectDirectory) async { + final originalYaml = projectDirectory.shorebirdYaml; + + const base64PublicKey = 'public_123'; + await projectDirectory.runFlutterBuildApk( + environment: { + 'SHOREBIRD_PUBLIC_KEY': base64PublicKey, + }, + ); + + final generatedYaml = + await projectDirectory.getGeneratedAndroidShorebirdYaml(); + + expect( + generatedYaml.keys, + containsAll(originalYaml.keys), + ); + + expect( + generatedYaml['patch_public_key'], + equals(base64PublicKey), + ); + }, + ); + }); + + group('when building with a flavor', () { + testWithShorebirdProject( + 'correctly changes the app id', + (projectDirectory) async { + await projectDirectory.addProjectFlavors(); + projectDirectory.addShorebirdFlavors(); + + await projectDirectory.runFlutterBuildApk(flavor: 'internal'); + + final generatedYaml = + await projectDirectory.getGeneratedAndroidShorebirdYaml( + flavor: 'internal', + ); + + expect(generatedYaml['app_id'], equals('internal_123')); + }, + ); + + group('when public key passed through environment variable', () { + testWithShorebirdProject( + 'correctly changes the app id and adds the public key', + (projectDirectory) async { + const base64PublicKey = 'public_123'; + await projectDirectory.addProjectFlavors(); + projectDirectory.addShorebirdFlavors(); + + await projectDirectory.runFlutterBuildApk( + flavor: 'internal', + environment: { + 'SHOREBIRD_PUBLIC_KEY': base64PublicKey, + }, + ); + + final generatedYaml = + await projectDirectory.getGeneratedAndroidShorebirdYaml( + flavor: 'internal', + ); + + expect(generatedYaml['app_id'], equals('internal_123')); + expect( + generatedYaml['patch_public_key'], + equals(base64PublicKey), + ); + }, + ); + }); + }); + }); +} diff --git a/packages/shorebird_tests/test/base_test.dart b/packages/shorebird_tests/test/base_test.dart new file mode 100644 index 0000000000000..14dba7ca6cc85 --- /dev/null +++ b/packages/shorebird_tests/test/base_test.dart @@ -0,0 +1,15 @@ +import 'package:test/test.dart'; + +import 'shorebird_tests.dart'; + +void main() { + group('shorebird helpers', () { + testWithShorebirdProject('can build a base project', + (projectDirectory) async { + expect(projectDirectory.existsSync(), isTrue); + + expect(projectDirectory.pubspecFile.existsSync(), isTrue); + expect(projectDirectory.shorebirdFile.existsSync(), isTrue); + }); + }); +} diff --git a/packages/shorebird_tests/test/ios_test.dart b/packages/shorebird_tests/test/ios_test.dart new file mode 100644 index 0000000000000..3fe6e1652c3e5 --- /dev/null +++ b/packages/shorebird_tests/test/ios_test.dart @@ -0,0 +1,91 @@ +import 'package:test/test.dart'; + +import 'shorebird_tests.dart'; + +void main() { + group( + 'shorebird ios projects', + () { + testWithShorebirdProject('can build', (projectDirectory) async { + await projectDirectory.runFlutterBuildIos(); + + expect(projectDirectory.iosArchiveFile().existsSync(), isTrue); + expect(projectDirectory.getGeneratedIosShorebirdYaml(), completes); + }); + + group('when passing the public key through the environment variable', () { + testWithShorebirdProject( + 'adds the public key on top of the original file', + (projectDirectory) async { + final originalYaml = projectDirectory.shorebirdYaml; + + const base64PublicKey = 'public_123'; + await projectDirectory.runFlutterBuildIos( + environment: { + 'SHOREBIRD_PUBLIC_KEY': base64PublicKey, + }, + ); + + final generatedYaml = + await projectDirectory.getGeneratedIosShorebirdYaml(); + + expect( + generatedYaml.keys, + containsAll(originalYaml.keys), + ); + + expect( + generatedYaml['patch_public_key'], + equals(base64PublicKey), + ); + }, + ); + }); + + group('when building with a flavor', () { + testWithShorebirdProject( + 'correctly changes the app id', + (projectDirectory) async { + await projectDirectory.addProjectFlavors(); + projectDirectory.addShorebirdFlavors(); + + await projectDirectory.runFlutterBuildIos(flavor: 'internal'); + + final generatedYaml = + await projectDirectory.getGeneratedIosShorebirdYaml(); + + expect(generatedYaml['app_id'], equals('internal_123')); + }, + ); + + group('when public key passed through environment variable', () { + testWithShorebirdProject( + 'correctly changes the app id and adds the public key', + (projectDirectory) async { + const base64PublicKey = 'public_123'; + await projectDirectory.addProjectFlavors(); + projectDirectory.addShorebirdFlavors(); + + await projectDirectory.runFlutterBuildIos( + flavor: 'internal', + environment: { + 'SHOREBIRD_PUBLIC_KEY': base64PublicKey, + }, + ); + + final generatedYaml = + await projectDirectory.getGeneratedIosShorebirdYaml(); + + expect(generatedYaml['app_id'], equals('internal_123')); + expect( + generatedYaml['patch_public_key'], + equals(base64PublicKey), + ); + }, + ); + }); + }); + }, + testOn: 'mac-os', + ); +} diff --git a/packages/shorebird_tests/test/shorebird_tests.dart b/packages/shorebird_tests/test/shorebird_tests.dart new file mode 100644 index 0000000000000..847b319e6e6df --- /dev/null +++ b/packages/shorebird_tests/test/shorebird_tests.dart @@ -0,0 +1,269 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:archive/archive_io.dart'; +import 'package:path/path.dart' as path; + +import 'package:meta/meta.dart'; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; + +/// This will be the path to the flutter binary housed in this flutter repository. +/// +/// Which since we are running the tests from this inner package , we need to go up two directories +/// in order to find the flutter binary in the bin folder. +File get _flutterBinaryFile => File( + path.join( + Directory.current.path, + '..', + '..', + 'bin', + 'flutter${Platform.isWindows ? '.bat' : ''}', + ), + ); + +/// Runs a flutter command using the correct binary ([_flutterBinaryFile]) with the given arguments. +Future _runFlutterCommand( + List arguments, { + required Directory workingDirectory, + Map? environment, +}) { + return Process.run( + _flutterBinaryFile.absolute.path, + arguments, + workingDirectory: workingDirectory.path, + environment: { + 'FLUTTER_STORAGE_BASE_URL': 'https://download.shorebird.dev', + if (environment != null) ...environment, + }, + ); +} + +Future _createFlutterProject(Directory projectDirectory) async { + final result = await _runFlutterCommand( + ['create', '--empty', '.'], + workingDirectory: projectDirectory, + ); + if (result.exitCode != 0) { + throw Exception('Failed to create Flutter project: ${result.stderr}'); + } +} + +@isTest +Future testWithShorebirdProject(String name, + FutureOr Function(Directory projectDirectory) testFn) async { + test( + name, + () async { + final parentDirectory = Directory.systemTemp.createTempSync(); + final projectDirectory = Directory( + path.join( + parentDirectory.path, + 'shorebird_test', + ), + )..createSync(); + + try { + await _createFlutterProject(projectDirectory); + + projectDirectory.pubspecFile.writeAsStringSync(''' +${projectDirectory.pubspecFile.readAsStringSync()} + assets: + - shorebird.yaml +'''); + + File( + path.join( + projectDirectory.path, + 'shorebird.yaml', + ), + ).writeAsStringSync(''' +app_id: "123" +'''); + + await testFn(projectDirectory); + } finally { + projectDirectory.deleteSync(recursive: true); + } + }, + timeout: Timeout( + // These tests usually run flutter create, flutter build, etc, which can take a while, + // specially in CI, so setting from the default of 30 seconds to 6 minutes. + Duration(minutes: 6), + ), + ); +} + +extension ShorebirdProjectDirectoryOnDirectory on Directory { + File get pubspecFile => File( + path.join(this.path, 'pubspec.yaml'), + ); + + File get shorebirdFile => File( + path.join(this.path, 'shorebird.yaml'), + ); + + YamlMap get shorebirdYaml => + loadYaml(shorebirdFile.readAsStringSync()) as YamlMap; + + File get appGradleFile => File( + path.join(this.path, 'android', 'app', 'build.gradle'), + ); + + Future addPubDependency(String name, {bool dev = false}) { + return _runFlutterCommand( + ['pub', 'add', if (dev) '--dev', name], + workingDirectory: this, + ); + } + + Future addProjectFlavors() async { + await addPubDependency('flutter_flavorizr', dev: true); + + await File( + path.join( + this.path, + 'flavorizr.yaml', + ), + ).writeAsString(''' +flavors: + playStore: + app: + name: "App" + + android: + applicationId: "com.example.shorebird_test" + ios: + bundleId: "com.example.shorebird_test" + internal: + app: + name: "App (Internal)" + + android: + applicationId: "com.example.shorebird_test.internal" + ios: + bundleId: "com.example.shorebird_test.internal" + global: + app: + name: "App (Global)" + + android: + applicationId: "com.example.shorebird_test.global" + ios: + bundleId: "com.example.shorebird_test.global" +'''); + + await _runFlutterCommand( + ['pub', 'run', 'flutter_flavorizr'], + workingDirectory: this, + ); + } + + void addShorebirdFlavors() { + const flavors = ''' +flavors: + global: global_123 + internal: internal_123 + playStore: playStore_123 +'''; + + final currentShorebirdContent = shorebirdFile.readAsStringSync(); + shorebirdFile.writeAsStringSync( + ''' +$currentShorebirdContent +$flavors +''', + ); + } + + Future runFlutterBuildApk({ + String? flavor, + Map? environment, + }) async { + final result = await _runFlutterCommand( + [ + 'build', + 'apk', + if (flavor != null) '--flavor=$flavor', + ], + workingDirectory: this, + environment: environment, + ); + if (result.exitCode != 0) { + throw Exception('Failed to run `flutter build apk`: ${result.stderr}'); + } + } + + Future runFlutterBuildIos({ + Map? environment, + String? flavor, + }) async { + final result = await _runFlutterCommand( + // The projects used to test are generated on spot, to make it simpler we don't + // configure any apple accounts on it, so we skip code signing here. + ['build', 'ipa', '--no-codesign', if (flavor != null) '--flavor=$flavor'], + workingDirectory: this, + environment: environment, + ); + + if (result.exitCode != 0) { + throw Exception('Failed to run `flutter build ios`: ${result.stderr}'); + } + } + + File apkFile({String? flavor}) => File( + path.join( + this.path, + 'build', + 'app', + 'outputs', + 'flutter-apk', + 'app-${flavor != null ? '$flavor-' : ''}release.apk', + ), + ); + + Directory iosArchiveFile() => Directory( + path.join( + this.path, + 'build', + 'ios', + 'archive', + 'Runner.xcarchive', + ), + ); + + Future getGeneratedAndroidShorebirdYaml({String? flavor}) async { + final decodedBytes = + ZipDecoder().decodeBytes(apkFile(flavor: flavor).readAsBytesSync()); + + await extractArchiveToDisk( + decodedBytes, path.join(this.path, 'apk-extracted')); + + final yamlString = File( + path.join( + this.path, + 'apk-extracted', + 'assets', + 'flutter_assets', + 'shorebird.yaml', + ), + ).readAsStringSync(); + return loadYaml(yamlString) as YamlMap; + } + + Future getGeneratedIosShorebirdYaml() async { + final yamlString = File( + path.join( + iosArchiveFile().path, + 'Products', + 'Applications', + 'Runner.app', + 'Frameworks', + 'App.framework', + 'flutter_assets', + 'shorebird.yaml', + ), + ).readAsStringSync(); + return loadYaml(yamlString) as YamlMap; + } +} From ebcb6a309c2097e5324a0fc325ebd94cc9f30051 Mon Sep 17 00:00:00 2001 From: Eric Seidel Date: Wed, 8 Mar 2023 14:20:02 -0800 Subject: [PATCH 02/14] chore: squash all commits on 3.27.4 (buildroot) --- engine/src/build/config/compiler/BUILD.gn | 9 ++++++++- engine/src/build/toolchain/win/BUILD.gn | 5 ++++- engine/src/build/toolchain/win/tool_wrapper.py | 11 +++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/engine/src/build/config/compiler/BUILD.gn b/engine/src/build/config/compiler/BUILD.gn index 61e54bb04b754..af022c0e5aa22 100644 --- a/engine/src/build/config/compiler/BUILD.gn +++ b/engine/src/build/config/compiler/BUILD.gn @@ -401,7 +401,10 @@ config("compiler") { # Example PR: https://github.com/dart-lang/native/pull/1615 ldflags += [ "-Wl,--no-undefined", - "-Wl,--exclude-libs,ALL", + + # TODO: Terrible hack, but otherwise libupdater.a symbols can't + # be exported from libflutter.so, even when added to android_exports.lst. + # "-Wl,--exclude-libs,ALL", # Enable identical code folding to reduce size. "-Wl,--icf=all", @@ -555,6 +558,8 @@ config("runtime_library") { ] libs += [ + # Rust requires libunwind. + "unwind", "c", "dl", "m", @@ -569,6 +574,8 @@ config("runtime_library") { } else if (current_cpu == "x86") { current_android_cpu = "i686" } + # libunwind.a is located in the respective android cpu subdirectories. + lib_dirs += [ "${android_toolchain_root}/lib/clang/17/lib/linux/${current_android_cpu}/" ] libs += [ "clang_rt.builtins-${current_android_cpu}-android" ] } diff --git a/engine/src/build/toolchain/win/BUILD.gn b/engine/src/build/toolchain/win/BUILD.gn index 006c80560afc5..eda5b22197c01 100644 --- a/engine/src/build/toolchain/win/BUILD.gn +++ b/engine/src/build/toolchain/win/BUILD.gn @@ -201,8 +201,11 @@ template("msvc_toolchain") { expname = "${dllname}.exp" pdbname = "${dllname}.pdb" rspfile = "${dllname}.rsp" + # .def files are used to export symbols from the DLL. This arg will be + # removed by the python tool wrapper if the .def file doesn't exist. + deffile = "${dllname}.def" - link_command = "\"$python_path\" $tool_wrapper_path link-wrapper $env False link.exe /nologo /IMPLIB:$libname /DLL /OUT:$dllname /PDB:${dllname}.pdb @$rspfile" + link_command = "\"$python_path\" $tool_wrapper_path link-wrapper $env False link.exe /nologo /IMPLIB:$libname /DLL /OUT:$dllname /PDB:${dllname}.pdb /DEF:$deffile @$rspfile" # TODO(brettw) support manifests #manifest_command = "\"$python_path\" $tool_wrapper_path manifest-wrapper $env mt.exe -nologo -manifest $manifests -out:${dllname}.manifest" diff --git a/engine/src/build/toolchain/win/tool_wrapper.py b/engine/src/build/toolchain/win/tool_wrapper.py index b4fc8485ffa17..7866c6122d832 100644 --- a/engine/src/build/toolchain/win/tool_wrapper.py +++ b/engine/src/build/toolchain/win/tool_wrapper.py @@ -121,6 +121,17 @@ def ExecLinkWrapper(self, arch, use_separate_mspdbsrv, *args): self._UseSeparateMspdbsrv(env, args) if sys.platform == 'win32': args = list(args) # *args is a tuple by default, which is read-only. + + # Remove the /DEF arg if not provided. We would ideally be able to do this + # in build\toolchain\win\BUILD.gn, but there doesn't seem to be a way to + # conditionally add args to the command line based on whether a file exists + # or not, so we do it here instead. + def_arg_prefix = "/DEF:" + for arg in args: + if arg.startswith(def_arg_prefix): + def_file = arg[len(def_arg_prefix):] + if not os.path.exists(def_file): + args.remove(arg) args[0] = args[0].replace('/', '\\') # https://docs.python.org/2/library/subprocess.html: # "On Unix with shell=True [...] if args is a sequence, the first item From 4a734b5e9de2943807b80c2dfd5a0b8e977d6c56 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Tue, 6 Aug 2024 14:29:01 -0700 Subject: [PATCH 03/14] chore: squash all our changes to a single commit (engine) --- DEPS | 25 +- engine/src/flutter/BUILD.gn | 27 +- engine/src/flutter/build/archives/BUILD.gn | 18 +- engine/src/flutter/common/config.gni | 8 + engine/src/flutter/lib/snapshot/BUILD.gn | 70 +++- engine/src/flutter/runtime/dart_snapshot.cc | 105 ++++-- engine/src/flutter/shell/common/BUILD.gn | 27 ++ engine/src/flutter/shell/common/shell.cc | 10 + .../flutter/shell/platform/android/BUILD.gn | 38 ++- .../platform/android/android_exports.lst | 11 + .../shell/platform/android/flutter_main.cc | 21 +- .../shell/platform/android/flutter_main.h | 3 + .../flutter/embedding/engine/FlutterJNI.java | 48 ++- .../shell/platform/darwin/ios/BUILD.gn | 40 +++ .../framework/Source/FlutterDartProject.mm | 33 ++ .../framework/Source/FlutterViewController.mm | 3 + .../shell/platform/darwin/macos/BUILD.gn | 2 + .../macos/framework/Source/FlutterEngine.mm | 58 +++- .../src/flutter/shell/platform/linux/BUILD.gn | 2 + .../flutter/shell/platform/linux/fl_engine.cc | 84 ++++- .../flutter/shell/platform/windows/BUILD.gn | 8 + .../platform/windows/flutter_project_bundle.h | 4 + .../platform/windows/flutter_windows.dll.def | 16 + .../windows/flutter_windows_engine.cc | 131 +++++++- engine/src/flutter/shell/testing/BUILD.gn | 1 + .../sky/tools/create_full_ios_framework.py | 300 ++++++++++++++++++ .../flutter/sky/tools/create_ios_framework.py | 12 +- engine/src/flutter/testing/run_tests.py | 1 + shell/common/shorebird/BUILD.gn | 56 ++++ shell/common/shorebird/shorebird.cc | 299 +++++++++++++++++ shell/common/shorebird/shorebird.h | 45 +++ .../common/shorebird/snapshots_data_handle.cc | 144 +++++++++ .../common/shorebird/snapshots_data_handle.h | 47 +++ .../snapshots_data_handle_unittests.cc | 177 +++++++++++ 34 files changed, 1815 insertions(+), 59 deletions(-) create mode 100644 engine/src/flutter/shell/platform/windows/flutter_windows.dll.def create mode 100644 engine/src/flutter/sky/tools/create_full_ios_framework.py create mode 100644 shell/common/shorebird/BUILD.gn create mode 100644 shell/common/shorebird/shorebird.cc create mode 100644 shell/common/shorebird/shorebird.h create mode 100644 shell/common/shorebird/snapshots_data_handle.cc create mode 100644 shell/common/shorebird/snapshots_data_handle.h create mode 100644 shell/common/shorebird/snapshots_data_handle_unittests.cc diff --git a/DEPS b/DEPS index 4847b1eb073bd..f115005f6e71c 100644 --- a/DEPS +++ b/DEPS @@ -15,6 +15,10 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'skia_revision': 'e7b8d078851fd505475fe74359e31a421e6968ea', + "dart_sdk_revision": "ddb0f3b6c38b1774a3413c2c159b124be6bd1df7", + "dart_sdk_git": "git@github.com:shorebirdtech/dart-sdk.git", + "updater_git": "https://github.com/shorebirdtech/updater.git", + "updater_rev": "78c84e5bf72266da07df536e98d431782cb39a6d", # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. @@ -225,14 +229,14 @@ gclient_gn_args = [ # Only these hosts are allowed for dependencies in this DEPS file. # If you need to add a new host, contact chrome infrastructure team. allowed_hosts = [ - 'boringssl.googlesource.com', - 'chrome-infra-packages.appspot.com', - 'chromium.googlesource.com', - 'dart.googlesource.com', - 'flutter.googlesource.com', - 'llvm.googlesource.com', - 'skia.googlesource.com', - 'swiftshader.googlesource.com', + "boringssl.googlesource.com", + "chrome-infra-packages.appspot.com", + "chromium.googlesource.com", + "dart.googlesource.com", + "flutter.googlesource.com", + "llvm.googlesource.com", + "skia.googlesource.com", + "swiftshader.googlesource.com", ] deps = { @@ -296,7 +300,7 @@ deps = { # Var('flutter_git') + '/third_party/protobuf-gn' + '@' + Var('dart_protobuf_gn_rev'), 'engine/src/flutter/third_party/dart': - Var('dart_git') + '/sdk.git' + '@' + Var('dart_revision'), + Var('dart_sdk_git') + '@' + Var('dart_sdk_revision'), # WARNING: Unused Dart dependencies in the list below till "WARNING:" marker are removed automatically - see create_updated_flutter_deps.py. @@ -484,6 +488,9 @@ deps = { 'engine/src/flutter/third_party/ocmock': Var('flutter_git') + '/third_party/ocmock' + '@' + Var('ocmock_rev'), + 'engine/src/flutter/third_party/updater': + Var('updater_git') + '@' + Var('updater_rev'), + 'engine/src/flutter/third_party/libjpeg-turbo/src': Var('flutter_git') + '/third_party/libjpeg-turbo' + '@' + '0fb821f3b2e570b2783a94ccd9a2fb1f4916ae9f', diff --git a/engine/src/flutter/BUILD.gn b/engine/src/flutter/BUILD.gn index b5720381d1148..60129bbd96bce 100644 --- a/engine/src/flutter/BUILD.gn +++ b/engine/src/flutter/BUILD.gn @@ -112,14 +112,10 @@ group("flutter") { # path_ops "//flutter/tools/path_ops", - ] - if (host_os == "linux") { - public_deps += [ - # Built alongside gen_snapshot for 64 bit targets - "$dart_src/runtime/bin:analyze_snapshot", - ] - } + # Built alongside gen_snapshot arm64 targets. + "$dart_src/runtime/bin:analyze_snapshot", + ] if (full_dart_sdk) { public_deps += [ "//flutter/web_sdk" ] @@ -193,6 +189,7 @@ group("unittests") { "//flutter/runtime:no_dart_plugin_registrant_unittests", "//flutter/runtime:runtime_unittests", "//flutter/shell/common:shell_unittests", + "//flutter/shell/common/shorebird:shorebird_unittests", "//flutter/shell/platform/embedder:embedder_a11y_unittests", "//flutter/shell/platform/embedder:embedder_proctable_unittests", "//flutter/shell/platform/embedder:embedder_unittests", @@ -319,3 +316,19 @@ if (host_os == "win") { outputs = [ "$root_build_dir/gen_snapshot/gen_snapshot.exe" ] } } + +# A top-level target for analyze_snapshot, modeled after the gen_snapshot +# target above. +if (host_os == "win") { + _analyze_snapshot_target = + "$dart_src/runtime/bin:analyze_snapshot($host_toolchain)" + + copy("analyze_snapshot") { + deps = [ _analyze_snapshot_target ] + + analyze_snapshot_out_dir = + get_label_info(_analyze_snapshot_target, "root_out_dir") + sources = [ "$analyze_snapshot_out_dir/analyze_snapshot.exe" ] + outputs = [ "$root_build_dir/analyze_snapshot/analyze_snapshot.exe" ] + } +} diff --git a/engine/src/flutter/build/archives/BUILD.gn b/engine/src/flutter/build/archives/BUILD.gn index 97d9483a23aba..8082f7d199f06 100644 --- a/engine/src/flutter/build/archives/BUILD.gn +++ b/engine/src/flutter/build/archives/BUILD.gn @@ -45,6 +45,7 @@ generated_file("artifacts_entitlement_config") { if (build_engine_artifacts) { zip_bundle("artifacts") { deps = [ + "$dart_src/runtime/bin:analyze_snapshot", "$dart_src/runtime/bin:gen_snapshot", "//flutter/flutter_frontend_server:frontend_server", "//flutter/impeller/compiler:impellerc", @@ -139,6 +140,10 @@ if (build_engine_artifacts) { if (host_os == "mac") { deps += [ ":artifacts_entitlement_config" ] files += [ + { + source = "$root_out_dir/analyze_snapshot$exe" + destination = "analyze_snapshot$exe" + }, { source = "$target_gen_dir/entitlements.txt" destination = "entitlements.txt" @@ -308,14 +313,25 @@ if (is_mac) { } if (host_os == "win") { + # This rule archives both gen_snapshot *and* analyze_snapshot. The name is + # misleading. We (shorebird) have updated this rule to include + # analyze_snapshot but did not update the name because it is referenced + # elsewhere in the tooling. zip_bundle("archive_win_gen_snapshot") { - deps = [ "//flutter:gen_snapshot" ] + deps = [ + "//flutter:analyze_snapshot", + "//flutter:gen_snapshot", + ] output = "$full_target_platform_name-$flutter_runtime_mode/windows-x64.zip" files = [ { source = "$root_out_dir/gen_snapshot/gen_snapshot.exe" destination = "gen_snapshot.exe" }, + { + source = "$root_out_dir/analyze_snapshot/analyze_snapshot.exe" + destination = "analyze_snapshot.exe" + }, ] } } diff --git a/engine/src/flutter/common/config.gni b/engine/src/flutter/common/config.gni index b2ae1118e2881..41ea8be3c96ff 100644 --- a/engine/src/flutter/common/config.gni +++ b/engine/src/flutter/common/config.gni @@ -73,6 +73,14 @@ if (slimpeller) { feature_defines_list += [ "SLIMPELLER=1" ] } +if (is_android || is_ios || is_linux || is_mac || is_win) { + feature_defines_list += [ "SHOREBIRD_PLATFORM_SUPPORTED=1" ] +} + +if (is_ios) { + feature_defines_list += [ "SHOREBIRD_USE_INTERPRETER=1" ] +} + if (is_ios || is_mac) { flutter_cflags_objc = [ "-Werror=overriding-method-mismatch", diff --git a/engine/src/flutter/lib/snapshot/BUILD.gn b/engine/src/flutter/lib/snapshot/BUILD.gn index 02f7956c1e147..674847a437698 100644 --- a/engine/src/flutter/lib/snapshot/BUILD.gn +++ b/engine/src/flutter/lib/snapshot/BUILD.gn @@ -34,14 +34,17 @@ group("generate_snapshot_bins") { # For macOS target builds: needed for both target CPUs (arm64, x64). # For iOS, Android target builds: all AOT target CPUs are arm/arm64. if (host_os == "mac" && (target_os == "mac" || target_os == "ios")) { - deps += [ ":create_macos_gen_snapshots" ] + deps += [ + ":create_macos_analyze_snapshots", + ":create_macos_gen_snapshots", + ] } else if (host_os == "mac" && (target_cpu == "arm" || target_cpu == "arm64")) { deps += [ ":create_arm_gen_snapshot" ] } # Build analyze_snapshot for 64-bit target CPUs. - if (host_os == "linux" && (target_cpu == "x64" || target_cpu == "arm64")) { + if (target_cpu == "arm64") { deps += [ "$dart_src/runtime/bin:analyze_snapshot($host_toolchain)" ] } } @@ -240,6 +243,69 @@ if (host_os == "mac" && (target_os == "mac" || target_os == "ios")) { ":create_macos_gen_snapshot_x64_${target_cpu}", ] } + + # Added by shorebird. + # analyze_snapshot targets below were copied from the gen_snapshot targets + # above to allow us to include analyze_snapshot in the artifacts generated + # for create_ios_framework.py. + template("build_mac_analyze_snapshot") { + assert(defined(invoker.host_arch)) + host_cpu = invoker.host_arch + + build_toolchain = "//build/toolchain/mac:clang_$host_cpu" + analyze_snapshot_target_name = "analyze_snapshot" + + # At this point, the gen_snapshot equivalent changes + # gen_ snapshot_target_name to "gen_snapshot_host_targeting_host". There is + # no equivalent for analyze_snapshot, so we don't do that here. + # + # It's unclear whether we need to do so now, but we didn't previously, so + # we're not doing it now until we have a reason to. + + analyze_snapshot_target = + "$dart_src/runtime/bin:$analyze_snapshot_target_name($build_toolchain)" + + copy(target_name) { + # The toolchain-specific output directory. For cross-compiles, this is a + # clang-x64 or clang-arm64 subdirectory of the top-level build directory. + output_dir = get_label_info(analyze_snapshot_target, "root_out_dir") + + sources = [ "${output_dir}/${analyze_snapshot_target_name}" ] + outputs = [ + "${root_out_dir}/artifacts_$host_cpu/analyze_snapshot_${target_cpu}", + ] + deps = [ analyze_snapshot_target ] + } + } + + build_mac_analyze_snapshot( + "create_macos_analyze_snapshot_arm64_${target_cpu}") { + host_arch = "arm64" + } + + build_mac_analyze_snapshot( + "create_macos_analyze_snapshot_x64_${target_cpu}") { + host_arch = "x64" + } + + action("create_macos_analyze_snapshots") { + script = "//flutter/sky/tools/create_macos_binary.py" + outputs = [ "${root_out_dir}/analyze_snapshot_${target_cpu}" ] + args = [ + "--in-arm64", + rebase_path( + "${root_out_dir}/artifacts_arm64/analyze_snapshot_${target_cpu}"), + "--in-x64", + rebase_path( + "${root_out_dir}/artifacts_x64/analyze_snapshot_${target_cpu}"), + "--out", + rebase_path("${root_out_dir}/analyze_snapshot_${target_cpu}"), + ] + deps = [ + ":create_macos_analyze_snapshot_arm64_${target_cpu}", + ":create_macos_analyze_snapshot_x64_${target_cpu}", + ] + } } source_set("snapshot") { diff --git a/engine/src/flutter/runtime/dart_snapshot.cc b/engine/src/flutter/runtime/dart_snapshot.cc index 5943524aa9f80..d735b299cad42 100644 --- a/engine/src/flutter/runtime/dart_snapshot.cc +++ b/engine/src/flutter/runtime/dart_snapshot.cc @@ -6,6 +6,7 @@ #include +#include #include "flutter/fml/native_library.h" #include "flutter/fml/paths.h" #include "flutter/fml/trace_event.h" @@ -56,33 +57,93 @@ static std::shared_ptr SearchMapping( const std::vector& native_library_path, const char* native_library_symbol_name, bool is_executable) { - // Ask the embedder. There is no fallback as we expect the embedders (via - // their embedding APIs) to just specify the mappings directly. - if (embedder_mapping_callback) { - // Note that mapping will be nullptr if the mapping callback returns an - // invalid mapping. If all the other methods for resolving the data also - // fail, the engine will stop with accompanying error logs. - if (auto mapping = embedder_mapping_callback()) { - return mapping; +#if SHOREBIRD_USE_INTERPRETER + // Detect when we're trying to load a Shorebird patch. + auto patch_path = native_library_path.front(); + bool is_patch = patch_path.find(".vmcode") != std::string::npos; + if (is_patch) { + // We use this terrible hack to load in the patch and then extract the + // symbols from it when the path is not App.framework/App but rather + // foo.vmcode, etc. We read the symbols into static variables, but then I + // believe we need to hold onto the ELF itself, otherwise the symbols + // become invalid. + // "leaked_elf" is meant to indicate that we're not freeing the ELF. + static Dart_LoadedElf* leaked_elf = nullptr; + // The VM Snapshot is identical for all binaries produced by a given version + // of Dart. Our linker checks this and will fail to link if ever the VM + // snapshot changes. + const uint8_t* ignored_vm_data = nullptr; + const uint8_t* ignored_vm_instrs = nullptr; + static const uint8_t* isolate_data = nullptr; + static const uint8_t* isolate_instrs = nullptr; + if (leaked_elf == nullptr) { + const char* error = nullptr; + // vmcode files are elf files prefixed with a shorebird linker header. + auto elf_mapping = GetFileMapping(patch_path, false /* executable */); + int elf_file_offset = Shorebird_ReadLinkHeader(elf_mapping->GetMapping(), + elf_mapping->GetSize()); + + leaked_elf = Dart_LoadELF(patch_path.c_str(), elf_file_offset, &error, + &ignored_vm_data, &ignored_vm_instrs, + &isolate_data, &isolate_instrs, + /* load as read-only, not rx */ false); + if (leaked_elf != nullptr) { + FML_LOG(INFO) << "Loaded ELF"; + } else { + FML_LOG(FATAL) << "Failed to load ELF at " << patch_path + << " error: " << error; + abort(); + } } - } - // Attempt to open file at path specified. - if (!file_path.empty()) { - if (auto file_mapping = GetFileMapping(file_path, is_executable)) { - return file_mapping; + FML_LOG(INFO) << "Loading symbol from ELF " << native_library_symbol_name; + + if (native_library_symbol_name == DartSnapshot::kIsolateDataSymbol) { + return std::make_unique(isolate_data, 0, + nullptr, true); + } else if (native_library_symbol_name == + DartSnapshot::kIsolateInstructionsSymbol) { + return std::make_unique(isolate_instrs, 0, + nullptr, true); + } + // Fall through to normal lookups for VM data and instructions. + // This fallthrough depends on the fact that NativeLibrary below can't + // read the ELF out of our .vmcode files. + } else { + // Only try to open the file if we're not loading a patch. +#endif + + // Ask the embedder. There is no fallback as we expect the embedders (via + // their embedding APIs) to just specify the mappings directly. + if (embedder_mapping_callback) { + // Note that mapping will be nullptr if the mapping callback returns an + // invalid mapping. If all the other methods for resolving the data also + // fail, the engine will stop with accompanying error logs. + if (auto mapping = embedder_mapping_callback()) { + return mapping; + } } - } - // Look in application specified native library if specified. - for (const std::string& path : native_library_path) { - auto native_library = fml::NativeLibrary::Create(path.c_str()); - auto symbol_mapping = std::make_unique( - native_library, native_library_symbol_name); - if (symbol_mapping->GetMapping() != nullptr) { - return symbol_mapping; + // Attempt to open file at path specified. + if (!file_path.empty()) { + if (auto file_mapping = GetFileMapping(file_path, is_executable)) { + return file_mapping; + } } - } + + // Look in application specified native library if specified. + for (const std::string& path : native_library_path) { + auto native_library = fml::NativeLibrary::Create(path.c_str()); + auto symbol_mapping = std::make_unique( + native_library, native_library_symbol_name); + if (symbol_mapping->GetMapping() != nullptr) { + return symbol_mapping; + } + } + +#if SHOREBIRD_USE_INTERPRETER + } // !is_patch +#endif // Look inside the currently loaded process. { diff --git a/engine/src/flutter/shell/common/BUILD.gn b/engine/src/flutter/shell/common/BUILD.gn index ddceabbf3fb69..9809113c06c35 100644 --- a/engine/src/flutter/shell/common/BUILD.gn +++ b/engine/src/flutter/shell/common/BUILD.gn @@ -152,6 +152,8 @@ source_set("common") { "//flutter/skia", ] + include_dirs = [ "//flutter/updater" ] + if (impeller_supports_rendering) { sources += [ "snapshot_controller_impeller.cc", @@ -160,6 +162,31 @@ source_set("common") { deps += [ "//flutter/impeller" ] } + + # Needed to compile flutter_tester for macOS. + if (host_os == "mac" && target_os == "mac") { + if (target_cpu == "arm64") { + libs = [ "//flutter/third_party/updater/target/aarch64-apple-darwin/release/libupdater.a" ] + } else if (target_cpu == "x64") { + libs = [ "//flutter/third_party/updater/target/x86_64-apple-darwin/release/libupdater.a" ] + } + } + + # Needed to compile flutter_tester for Windows. + if (host_os == "win" && target_os == "win") { + if (target_cpu == "x64") { + libs = [ + "userenv.lib", + "//flutter/third_party/updater/target/x86_64-pc-windows-msvc/release/updater.lib", + ] + } + } + + if (host_os == "linux" && target_os == "linux") { + if (target_cpu == "x64") { + libs = [ "//flutter/third_party/updater/target/x86_64-unknown-linux-gnu/release/libupdater.a" ] + } + } } # These are in their own source_set to avoid a dependency cycle with //common/graphics diff --git a/engine/src/flutter/shell/common/shell.cc b/engine/src/flutter/shell/common/shell.cc index 378524986552b..99b175cd89c66 100644 --- a/engine/src/flutter/shell/common/shell.cc +++ b/engine/src/flutter/shell/common/shell.cc @@ -43,6 +43,8 @@ #include "third_party/skia/include/core/SkGraphics.h" #include "third_party/tonic/common/log.h" +#include "third_party/updater/library/include/updater.h" + namespace flutter { constexpr char kSkiaChannel[] = "flutter/skia"; @@ -434,6 +436,14 @@ Shell::Shell(DartVMRef vm, is_gpu_disabled_sync_switch_(new fml::SyncSwitch(is_gpu_disabled)), weak_factory_gpu_(nullptr), weak_factory_(this) { + // FIXME: This is probably the wrong place to hook into. +#if SHOREBIRD_PLATFORM_SUPPORTED + if (!vm_) { + shorebird_report_launch_failure(); + } else { + shorebird_report_launch_success(); + } +#endif FML_CHECK(!settings.enable_software_rendering || !settings.enable_impeller) << "Software rendering is incompatible with Impeller."; if (!settings.enable_impeller && settings.warn_on_impeller_opt_out) { diff --git a/engine/src/flutter/shell/platform/android/BUILD.gn b/engine/src/flutter/shell/platform/android/BUILD.gn index 4b6ea453c09ed..f6d4c11c60359 100644 --- a/engine/src/flutter/shell/platform/android/BUILD.gn +++ b/engine/src/flutter/shell/platform/android/BUILD.gn @@ -159,6 +159,7 @@ source_set("flutter_shell_native_src") { "//flutter/runtime", "//flutter/runtime:libdart", "//flutter/shell/common", + "//flutter/shell/common/shorebird", "//flutter/shell/platform/android/context", "//flutter/shell/platform/android/external_view_embedder", "//flutter/shell/platform/android/jni", @@ -172,6 +173,8 @@ source_set("flutter_shell_native_src") { public_configs = [ "//flutter:config" ] + include_dirs = [ "//flutter/updater" ] + defines = [] libs = [ @@ -179,6 +182,17 @@ source_set("flutter_shell_native_src") { "EGL", "GLESv2", ] + if (target_cpu == "arm") { + libs += [ "//flutter/third_party/updater/target/armv7-linux-androideabi/release/libupdater.a" ] + } else if (target_cpu == "arm64") { + libs += [ "//flutter/third_party/updater/target/aarch64-linux-android/release/libupdater.a" ] + } else if (target_cpu == "x64") { + libs += [ "//flutter/third_party/updater/target/x86_64-linux-android/release/libupdater.a" ] + } else if (target_cpu == "x86") { + libs += [ "//flutter/third_party/updater/target/i686-linux-android/release/libupdater.a" ] + } else { + assert(false, "Unsupported target_cpu") + } } action("gen_android_build_config_java") { @@ -716,6 +730,9 @@ if (target_cpu != "x86") { # TODO(godofredoc): Remove gen_snapshot and rename new_gen_snapshot when v2 migration is complete. # BUG: https://github.com/flutter/flutter/issues/105351 + # This has a somewhat misleading name. We (shorebird) have updated this target + # to include analyze_snapshot as well as gen_snapshot. Because this target is + # used elsewhere in the toolchain, we did not rename it. zip_bundle("new_gen_snapshot") { gen_snapshot_bin = "gen_snapshot" gen_snapshot_out_dir = @@ -723,6 +740,14 @@ if (target_cpu != "x86") { "root_out_dir") gen_snapshot_path = rebase_path("$gen_snapshot_out_dir/$gen_snapshot_bin") + analyze_snapshot_bin = "analyze_snapshot" + analyze_snapshot_out_dir = + get_label_info( + "$dart_src/runtime/bin:analyze_snapshot($host_toolchain)", + "root_out_dir") + analyze_snapshot_path = + rebase_path("$analyze_snapshot_out_dir/$analyze_snapshot_bin") + if (host_os == "linux") { output = "$android_zip_archive_dir/$full_platform_name-$flutter_runtime_mode/linux-x64.zip" } else if (host_os == "mac") { @@ -731,6 +756,8 @@ if (target_cpu != "x86") { output = "$android_zip_archive_dir/$full_platform_name-$flutter_runtime_mode/windows-x64.zip" gen_snapshot_bin = "gen_snapshot.exe" gen_snapshot_path = rebase_path("$root_out_dir/$gen_snapshot_bin") + analyze_snapshot_bin = "analyze_snapshot.exe" + analyze_snapshot_path = rebase_path("$root_out_dir/$analyze_snapshot_bin") } files = [ @@ -738,9 +765,16 @@ if (target_cpu != "x86") { source = gen_snapshot_path destination = gen_snapshot_bin }, + { + source = analyze_snapshot_path + destination = analyze_snapshot_bin + }, ] - deps = [ "$dart_src/runtime/bin:gen_snapshot($host_toolchain)" ] + deps = [ + "$dart_src/runtime/bin:analyze_snapshot($host_toolchain)", + "$dart_src/runtime/bin:gen_snapshot($host_toolchain)", + ] if (host_os == "mac") { deps += [ ":android_entitlement_config" ] @@ -754,7 +788,7 @@ if (target_cpu != "x86") { } } -if (host_os == "linux" && (target_cpu == "x64" || target_cpu == "arm64")) { +if (target_cpu == "arm64") { zip_bundle("analyze_snapshot") { deps = [ "$dart_src/runtime/bin:analyze_snapshot($host_toolchain)" ] diff --git a/engine/src/flutter/shell/platform/android/android_exports.lst b/engine/src/flutter/shell/platform/android/android_exports.lst index 198bff773dd74..1f483306bbab0 100644 --- a/engine/src/flutter/shell/platform/android/android_exports.lst +++ b/engine/src/flutter/shell/platform/android/android_exports.lst @@ -11,6 +11,17 @@ _binary_icudtl_dat_size; InternalFlutterGpu*; kInternalFlutterGpu*; + shorebird_init; + shorebird_active_path; + shorebird_active_patch_number; + shorebird_free_string; + shorebird_free_update_result; + shorebird_check_for_downloadable_update; + shorebird_check_for_update; + shorebird_update; + shorebird_update_with_result; + shorebird_next_boot_patch_number; + shorebird_current_boot_patch_number; local: *; }; diff --git a/engine/src/flutter/shell/platform/android/flutter_main.cc b/engine/src/flutter/shell/platform/android/flutter_main.cc index 31aa094b0784e..db9a3e89cfc3e 100644 --- a/engine/src/flutter/shell/platform/android/flutter_main.cc +++ b/engine/src/flutter/shell/platform/android/flutter_main.cc @@ -23,6 +23,7 @@ #include "flutter/lib/ui/plugins/callback_cache.h" #include "flutter/runtime/dart_vm.h" #include "flutter/shell/common/shell.h" +#include "flutter/shell/common/shorebird/shorebird.h" #include "flutter/shell/common/switches.h" #include "flutter/shell/platform/android/android_context_vk_impeller.h" #include "flutter/shell/platform/android/context/android_context.h" @@ -32,6 +33,8 @@ #include "third_party/dart/runtime/include/dart_tools_api.h" #include "txt/platform.h" +#include "third_party/updater/library/include/updater.h" + namespace flutter { constexpr int kMinimumAndroidApiLevelForVulkan = 29; @@ -74,6 +77,9 @@ void FlutterMain::Init(JNIEnv* env, jstring kernelPath, jstring appStoragePath, jstring engineCachesPath, + jstring shorebirdYaml, + jstring version, + jstring versionCode, jlong initTimeMillis) { std::vector args; args.push_back("flutter"); @@ -122,8 +128,18 @@ void FlutterMain::Init(JNIEnv* env, flutter::DartCallbackCache::SetCachePath( fml::jni::JavaStringToString(env, appStoragePath)); - fml::paths::InitializeAndroidCachesPath( - fml::jni::JavaStringToString(env, engineCachesPath)); + auto code_cache_path = fml::jni::JavaStringToString(env, engineCachesPath); + auto app_storage_path = fml::jni::JavaStringToString(env, appStoragePath); + fml::paths::InitializeAndroidCachesPath(code_cache_path); + +#if FLUTTER_RELEASE + std::string shorebird_yaml = fml::jni::JavaStringToString(env, shorebirdYaml); + std::string version_string = fml::jni::JavaStringToString(env, version); + std::string version_code_string = + fml::jni::JavaStringToString(env, versionCode); + ConfigureShorebird(code_cache_path, app_storage_path, settings, + shorebird_yaml, version_string, version_code_string); +#endif flutter::DartCallbackCache::LoadCacheFromDisk(); @@ -213,6 +229,7 @@ bool FlutterMain::Register(JNIEnv* env) { { .name = "nativeInit", .signature = "(Landroid/content/Context;[Ljava/lang/String;Ljava/" + "lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/" "lang/String;Ljava/lang/String;Ljava/lang/String;J)V", .fnPtr = reinterpret_cast(&Init), }, diff --git a/engine/src/flutter/shell/platform/android/flutter_main.h b/engine/src/flutter/shell/platform/android/flutter_main.h index 4db12308826a5..91c1b14256e03 100644 --- a/engine/src/flutter/shell/platform/android/flutter_main.h +++ b/engine/src/flutter/shell/platform/android/flutter_main.h @@ -41,6 +41,9 @@ class FlutterMain { jstring kernelPath, jstring appStoragePath, jstring engineCachesPath, + jstring shorebirdYaml, + jstring version, + jstring versionCode, jlong initTimeMillis); void SetupDartVMServiceUriCallback(JNIEnv* env); diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index 4747abac87ecc..f96d5efbc3640 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -7,6 +7,8 @@ import static io.flutter.Build.API_LEVELS; import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.ColorSpace; @@ -40,7 +42,10 @@ import io.flutter.view.AccessibilityBridge; import io.flutter.view.FlutterCallbackInformation; import io.flutter.view.TextureRegistry; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -177,6 +182,9 @@ private static native void nativeInit( @Nullable String bundlePath, @NonNull String appStoragePath, @NonNull String engineCachesPath, + @Nullable String shorebirdYaml, + @Nullable String version, + @Nullable String versionCode, long initTimeMillis); /** @@ -202,8 +210,46 @@ public void init( Log.w(TAG, "FlutterJNI.init called more than once"); } + String version = null; + String versionCode = null; + try { + PackageInfo packageInfo = + context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + version = packageInfo.versionName; + if (Build.VERSION.SDK_INT >= API_LEVELS.API_28) { + versionCode = String.valueOf(packageInfo.getLongVersionCode()); + } else { + versionCode = String.valueOf(packageInfo.versionCode); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Failed to read app version. Shorebird updater can't run.", e); + } + + String shorebirdYaml = null; + try { + InputStream yaml = context.getAssets().open("flutter_assets/shorebird.yaml"); + BufferedReader r = new BufferedReader(new InputStreamReader(yaml)); + StringBuilder total = new StringBuilder(); + for (String line; (line = r.readLine()) != null; ) { + total.append(line).append('\n'); + } + shorebirdYaml = total.toString(); + Log.d(TAG, "shorebird.yaml: " + shorebirdYaml); + } catch (IOException e) { + Log.e(TAG, "Failed to load shorebird.yaml", e); + Log.e(TAG, "Did you remember to include shorebird.yaml in your pubspec.yaml's assets?"); + } + FlutterJNI.nativeInit( - context, args, bundlePath, appStoragePath, engineCachesPath, initTimeMillis); + context, + args, + bundlePath, + appStoragePath, + engineCachesPath, + shorebirdYaml, + version, + versionCode, + initTimeMillis); FlutterJNI.initCalled = true; } diff --git a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn index 6020365844afb..cbd8073c2e002 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn @@ -52,6 +52,15 @@ source_set("flutter_framework_source") { ":ios_gpu_configuration_config", "//flutter:config", ] + allow_circular_includes_from = [ ":flutter_framework_source" ] + deps = [ + ":flutter_framework_source", + "//flutter/fml", + "//flutter/shell/common/shorebird", + "//flutter/shell/platform/common:common_cpp_input", + "//flutter/shell/platform/darwin/common:framework_common", + "//flutter/third_party/icu", + ] sources = [ "framework/Source/FlutterAppDelegate.mm", @@ -159,6 +168,37 @@ source_set("flutter_framework_source") { ] sources += _flutter_framework_headers + defines = [ "FLUTTER_FRAMEWORK=1" ] + if (darwin_extension_safe) { + defines += [ "APPLICATION_EXTENSION_API_ONLY=1" ] + } + + deps += [ + "$dart_src/runtime/bin:elf_loader", + "//flutter/fml", + "//flutter/runtime", + "//flutter/shell/common", + "//flutter/shell/common/shorebird", + "//flutter/shell/platform/darwin/common", + "//flutter/shell/platform/darwin/common:framework_common", + "//flutter/shell/platform/embedder:embedder_as_internal_library", + "//flutter/shell/profiling:profiling", + "//flutter/third_party/spring_animation", + ] + + public_configs = [ + ":ios_gpu_configuration_config", + "//flutter:config", + ] + + if (target_cpu == "arm64") { + libs = [ "//flutter/third_party/updater/target/aarch64-apple-ios/release/libupdater.a" ] + } else if (target_cpu == "x64") { + libs = [ "//flutter/third_party/updater/target/x86_64-apple-ios/release/libupdater.a" ] + } else { + assert(false, "Unsupported target_cpu") + } + frameworks = [ "AudioToolbox.framework", "CoreMedia.framework", diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm index 3f31c181f3d17..dbb2922cb2763 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm @@ -13,6 +13,8 @@ #include "flutter/common/constants.h" #include "flutter/fml/build_config.h" +#include "flutter/fml/paths.h" +#include "flutter/shell/common/shorebird/shorebird.h" #include "flutter/shell/common/switches.h" #include "flutter/shell/platform/darwin/common/command_line.h" @@ -97,10 +99,12 @@ static BOOL DoesHardwareSupportWideGamut() { } if (flutter::DartVM::IsRunningPrecompiledCode()) { + NSLog(@"SANITY CHECK: Running precompiled code."); if (hasExplicitBundle) { NSString* executablePath = bundle.executablePath; if ([[NSFileManager defaultManager] fileExistsAtPath:executablePath]) { settings.application_library_path.push_back(executablePath.UTF8String); + NSLog(@"Using precompiled library from %@", executablePath); } } @@ -112,6 +116,7 @@ static BOOL DoesHardwareSupportWideGamut() { NSString* executablePath = [NSBundle bundleWithPath:libraryPath].executablePath; if (executablePath.length > 0) { settings.application_library_path.push_back(executablePath.UTF8String); + NSLog(@"Using library from %@", libraryPath); } } } @@ -126,6 +131,7 @@ static BOOL DoesHardwareSupportWideGamut() { [NSBundle bundleWithPath:applicationFrameworkPath].executablePath; if (executablePath.length > 0) { settings.application_library_path.push_back(executablePath.UTF8String); + NSLog(@"Using App.framework from %@", applicationFrameworkPath); } } } @@ -157,6 +163,33 @@ static BOOL DoesHardwareSupportWideGamut() { } } + NSString* assetsPath = [NSString stringWithUTF8String:settings.assets_path.c_str()]; + NSLog(@"ASSET PATH %@", assetsPath); + + // FIXME: This may not be the correct path (e.g., should it include the organization id?) + // See + // https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW13 + // /private/var/mobile/Containers/Data/Application/264477BF-6E38-47C9-AAD9-532BB842F197/Library/Application + // Support/shorebird/shorebird_updater + std::string cache_path = + fml::paths::JoinPaths({getenv("HOME"), "Library/Application Support/shorebird"}); + NSURL* shorebirdYamlPath = [NSURL URLWithString:@"shorebird.yaml" + relativeToURL:[NSURL fileURLWithPath:assetsPath]]; + NSString* appVersion = [mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + NSString* appBuildNumber = [mainBundle objectForInfoDictionaryKey:@"CFBundleVersion"]; + NSString* shorebirdYamlContents = [NSString stringWithContentsOfURL:shorebirdYamlPath + encoding:NSUTF8StringEncoding + error:nil]; + if (shorebirdYamlContents != nil) { + // Note: we intentionally pass cache_path twice. We provide two different directories + // to ConfigureShorebird because Android differentiates between data that persists + // between releases and data that does not. iOS does not make this distinction. + flutter::ConfigureShorebird(cache_path, cache_path, settings, shorebirdYamlContents.UTF8String, + appVersion.UTF8String, appBuildNumber.UTF8String); + } else { + NSLog(@"Failed to find shorebird.yaml, not starting updater."); + } + // Domain network configuration // Disabled in https://github.com/flutter/flutter/issues/72723. // Re-enable in https://github.com/flutter/flutter/issues/54448. diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index ad6eedc910c50..267e6b4998b5d 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -34,6 +34,7 @@ #import "flutter/third_party/spring_animation/spring_animation.h" FLUTTER_ASSERT_ARC +#import static constexpr int kMicrosecondsPerSecond = 1000 * 1000; static constexpr CGFloat kScrollViewContentSize = 2.0; @@ -256,6 +257,8 @@ - (void)sharedSetupWithProject:(nullable FlutterDartProject*)project if (!project) { project = [[FlutterDartProject alloc] init]; } + FML_LOG(INFO) << "CPU::Id(): " << dart::CPU::Id(); + FlutterView.forceSoftwareRendering = project.settings.enable_software_rendering; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:project diff --git a/engine/src/flutter/shell/platform/darwin/macos/BUILD.gn b/engine/src/flutter/shell/platform/darwin/macos/BUILD.gn index 0939591bb45ae..064795989055a 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/macos/BUILD.gn @@ -125,10 +125,12 @@ source_set("flutter_framework_source") { ":macos_gpu_configuration", "//flutter/flow:flow", "//flutter/fml", + "//flutter/shell/common/shorebird", "//flutter/shell/platform/common:common_cpp_accessibility", "//flutter/shell/platform/common:common_cpp_enums", "//flutter/shell/platform/common:common_cpp_input", "//flutter/shell/platform/common:common_cpp_switches", + "//flutter/shell/platform/darwin/common", "//flutter/shell/platform/darwin/common:availability_version_check", "//flutter/shell/platform/darwin/common:framework_common", "//flutter/shell/platform/darwin/graphics:graphics", diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index 1a87c66386094..01016ce0a4287 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -10,6 +10,8 @@ #include #include "flutter/common/constants.h" +#include "flutter/fml/paths.h" +#include "flutter/shell/common/shorebird/shorebird.h" #include "flutter/shell/platform/common/app_lifecycle_state.h" #include "flutter/shell/platform/common/engine_switches.h" #include "flutter/shell/platform/embedder/embedder.h" @@ -584,6 +586,40 @@ - (void)dealloc { } } +- (BOOL)configureShorebird:(NSString**)patchPath { + NSLog(@"[shorebird] setting up non-linker shorebird"); + NSString* bundlePath = + [[NSBundle bundleWithURL:[NSBundle.mainBundle.privateFrameworksURL + URLByAppendingPathComponent:@"App.framework"]] bundlePath]; + bundlePath = [bundlePath stringByAppendingString:@"/App"]; + NSString* assetsPath = _project.assetsPath; + NSURL* shorebirdYamlPath = [NSURL URLWithString:@"shorebird.yaml" + relativeToURL:[NSURL fileURLWithPath:assetsPath]]; + NSString* shorebirdYamlContents = [NSString stringWithContentsOfURL:shorebirdYamlPath + encoding:NSUTF8StringEncoding + error:nil]; + NSString* appVersion = + [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + NSString* appBuildNumber = [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleVersion"]; + std::string cache_path = + fml::paths::JoinPaths({getenv("HOME"), "Library", "Application Support", "shorebird"}); + flutter::ReleaseVersion release_version = {appVersion.UTF8String, appBuildNumber.UTF8String}; + flutter::ShorebirdConfigArgs shorebird_args(cache_path, cache_path, bundlePath.UTF8String, + shorebirdYamlContents.UTF8String, release_version); + NSLog(@"[shorebird] calling ConfigureShorebird"); + std::string patch_path; + auto res = flutter::ConfigureShorebird(shorebird_args, patch_path); + if (!res) { + NSLog(@"[shorebird] ConfigureShorebird failed"); + return NO; + } + + NSLog(@"[shorebird] ConfigureShorebird success!"); + *patchPath = [NSString stringWithUTF8String:patch_path.c_str()]; + NSLog(@"[shorebird] patchPath: %@", *patchPath); + return YES; +} + - (BOOL)runWithEntrypoint:(NSString*)entrypoint { if (self.running) { return NO; @@ -669,7 +705,19 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { .thread_priority_setter = SetThreadPriority}; flutterArguments.custom_task_runners = &custom_task_runners; - [self loadAOTData:_project.assetsPath]; + NSString* elfPath; + BOOL configureShorebirdRes = [self configureShorebird:&elfPath]; + if (!configureShorebirdRes) { + // No patch exists, or we failed to configure shorebird. This is a fallback. + // Upstream, this code lives in -(void)loadAOTData:. + // + // This is the location where the test fixture places the snapshot file. + // For applications built by Flutter tool, this is in "App.framework". + elfPath = [NSString pathWithComponents:@[ _project.assetsPath, @"app_elf_snapshot.so" ]]; + } + + [self loadAOTData:elfPath]; + if (_aotData) { flutterArguments.aot_data = _aotData; } @@ -687,6 +735,7 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { }; FlutterRendererConfig rendererConfig = [_renderer createRendererConfig]; + FlutterEngineResult result = _embedderAPI.Initialize( FLUTTER_ENGINE_VERSION, &rendererConfig, &flutterArguments, (__bridge void*)(self), &_engine); if (result != kSuccess) { @@ -716,7 +765,7 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { return YES; } -- (void)loadAOTData:(NSString*)assetsDir { +- (void)loadAOTData:(NSString*)elfPath { if (!_embedderAPI.RunsAOTCompiledDartCode()) { return; } @@ -724,11 +773,8 @@ - (void)loadAOTData:(NSString*)assetsDir { BOOL isDirOut = false; // required for NSFileManager fileExistsAtPath. NSFileManager* fileManager = [NSFileManager defaultManager]; - // This is the location where the test fixture places the snapshot file. - // For applications built by Flutter tool, this is in "App.framework". - NSString* elfPath = [NSString pathWithComponents:@[ assetsDir, @"app_elf_snapshot.so" ]]; - if (![fileManager fileExistsAtPath:elfPath isDirectory:&isDirOut]) { + FML_LOG(INFO) << "in loadAOTData, elfPath does not exist: " << elfPath.UTF8String; return; } diff --git a/engine/src/flutter/shell/platform/linux/BUILD.gn b/engine/src/flutter/shell/platform/linux/BUILD.gn index ba446740891ef..0f78a2b8462b3 100644 --- a/engine/src/flutter/shell/platform/linux/BUILD.gn +++ b/engine/src/flutter/shell/platform/linux/BUILD.gn @@ -174,11 +174,13 @@ source_set("flutter_linux_sources") { deps = [ "//flutter/fml", + "//flutter/shell/common/shorebird", "//flutter/shell/platform/common:common_cpp_enums", "//flutter/shell/platform/common:common_cpp_input", "//flutter/shell/platform/common:common_cpp_switches", "//flutter/shell/platform/embedder:embedder_headers", "//flutter/third_party/rapidjson", + "//flutter/third_party/tonic", ] } diff --git a/engine/src/flutter/shell/platform/linux/fl_engine.cc b/engine/src/flutter/shell/platform/linux/fl_engine.cc index 01329cfe9b37e..0fe2224aa6ac2 100644 --- a/engine/src/flutter/shell/platform/linux/fl_engine.cc +++ b/engine/src/flutter/shell/platform/linux/fl_engine.cc @@ -4,10 +4,19 @@ #include "flutter/shell/platform/linux/public/flutter_linux/fl_engine.h" +#include #include #include - +#include +#include +#include +#include + +#include "flutter/common/constants.h" +#include "flutter/fml/logging.h" +#include "flutter/fml/paths.h" +#include "flutter/shell/common/shorebird/shorebird.h" #include "flutter/shell/platform/common/engine_switches.h" #include "flutter/shell/platform/embedder/embedder.h" #include "flutter/shell/platform/linux/fl_binary_messenger_private.h" @@ -24,6 +33,8 @@ #include "flutter/shell/platform/linux/fl_texture_gl_private.h" #include "flutter/shell/platform/linux/fl_texture_registrar_private.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_plugin_registry.h" +#include "rapidjson/document.h" +#include "third_party/tonic/filesystem/filesystem/file.h" // Unique number associated with platform tasks. static constexpr size_t kPlatformTaskRunnerIdentifier = 1; @@ -541,6 +552,59 @@ FlDisplayMonitor* fl_engine_get_display_monitor(FlEngine* self) { return self->display_monitor; } +gboolean fl_set_up_shorebird(const char* assets_path, std::string& patch_path) { + auto shorebird_yaml_path = + fml::paths::JoinPaths({assets_path, "shorebird.yaml"}); + std::string shorebird_yaml_contents(""); + if (!filesystem::ReadFileToString(shorebird_yaml_path, + &shorebird_yaml_contents)) { + FML_LOG(ERROR) << "Failed to read shorebird.yaml."; + return false; + } + + // Read appid from shorebird.yaml + std::string appid = ""; + std::stringstream ss(shorebird_yaml_contents); + std::string line; + std::string appid_prefix = "appid:"; + while (std::getline(ss, line, '\n')) { + if (line.find(appid_prefix) != std::string::npos) { + appid = line.substr(line.find(appid_prefix) + appid_prefix.size()); + break; + } + } + + std::string code_cache_path = + fml::paths::JoinPaths({g_get_home_dir(), ".shorebird_cache", appid}); + auto executable_location = fml::paths::GetExecutableDirectoryPath().second; + auto app_path = + fml::paths::JoinPaths({executable_location, "lib", "libapp.so"}); + auto version_json_path = fml::paths::JoinPaths({assets_path, "version.json"}); + std::ifstream input(version_json_path); + if (!input) { + return false; + } + std::string json_contents{std::istreambuf_iterator(input), + std::istreambuf_iterator()}; + + rapidjson::Document json_doc; + json_doc.Parse(json_contents.c_str()); + if (json_doc.HasParseError()) { + // Could not parse version file, aborting. + return false; + } + + const auto version_map = json_doc.GetObject(); + flutter::ReleaseVersion release_version{ + version_map["version"].GetString(), + version_map["build_number"].GetString()}; + + flutter::ShorebirdConfigArgs shorebird_args(code_cache_path, code_cache_path, + app_path, shorebird_yaml_contents, + release_version); + return ConfigureShorebird(shorebird_args, patch_path); +} + gboolean fl_engine_start(FlEngine* self, GError** error) { g_return_val_if_fail(FL_IS_ENGINE(self), FALSE); @@ -573,6 +637,9 @@ gboolean fl_engine_start(FlEngine* self, GError** error) { g_autoptr(GPtrArray) command_line_args = g_ptr_array_new_with_free_func(g_free); + // FlutterProjectArgs expects a full argv, so when processing it for flags + // the first item is treated as the executable and ignored. Add a dummy + // value so that all switches are used. g_ptr_array_insert(command_line_args, 0, g_strdup("flutter")); for (const auto& env_switch : flutter::GetSwitchesFromEnvironment()) { g_ptr_array_add(command_line_args, g_strdup(env_switch.c_str())); @@ -609,9 +676,22 @@ gboolean fl_engine_start(FlEngine* self, GError** error) { args.compositor = &compositor; if (self->embedder_api.RunsAOTCompiledDartCode()) { + // This struct contains raw C strings and needs to have its lifetime scoped + // to this block. FlutterEngineAOTDataSource source = {}; source.type = kFlutterEngineAOTDataSourceTypeElfPath; - source.elf_path = fl_dart_project_get_aot_library_path(self->project); + std::string patch_path; + auto setup_shorebird_result = + fl_set_up_shorebird(args.assets_path, patch_path); + if (setup_shorebird_result) { + // If we have a patch installed, we replace the default AOT library path + // with the patch path here. + source.elf_path = patch_path.c_str(); + } else { + FML_LOG(ERROR) << "Failed to configure Shorebird."; + source.elf_path = fl_dart_project_get_aot_library_path(self->project); + } + if (self->embedder_api.CreateAOTData(&source, &self->aot_data) != kSuccess) { g_set_error(error, fl_engine_error_quark(), FL_ENGINE_ERROR_FAILED, diff --git a/engine/src/flutter/shell/platform/windows/BUILD.gn b/engine/src/flutter/shell/platform/windows/BUILD.gn index e80662e592a72..431676d2a4891 100644 --- a/engine/src/flutter/shell/platform/windows/BUILD.gn +++ b/engine/src/flutter/shell/platform/windows/BUILD.gn @@ -156,6 +156,7 @@ source_set("flutter_windows_source") { ":flutter_windows_headers", "//flutter/fml:fml", "//flutter/impeller/renderer/backend/gles", + "//flutter/shell/common/shorebird:shorebird", "//flutter/shell/platform/common:common_cpp", "//flutter/shell/platform/common:common_cpp_input", "//flutter/shell/platform/common:common_cpp_switches", @@ -169,6 +170,7 @@ source_set("flutter_windows_source") { "//flutter/third_party/angle:libEGL_static", "//flutter/third_party/angle:libGLESv2_static", "//flutter/third_party/rapidjson", + "//flutter/third_party/tonic", ] } @@ -180,6 +182,11 @@ copy("publish_headers_windows") { deps = [ "//flutter/shell/platform/common:publish_headers" ] } +copy("updater_exports_windows") { + sources = [ "flutter_windows.dll.def" ] + outputs = [ "$root_out_dir/{{source_file_part}}" ] +} + shared_library("flutter_windows") { deps = [ ":flutter_windows_source" ] @@ -292,6 +299,7 @@ group("windows") { deps = [ ":flutter_windows", ":publish_headers_windows", + ":updater_exports_windows", "//flutter/shell/platform/windows/client_wrapper:publish_wrapper_windows", ] diff --git a/engine/src/flutter/shell/platform/windows/flutter_project_bundle.h b/engine/src/flutter/shell/platform/windows/flutter_project_bundle.h index 09770b5c094fc..8e9126dbacedd 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_project_bundle.h +++ b/engine/src/flutter/shell/platform/windows/flutter_project_bundle.h @@ -44,6 +44,10 @@ class FlutterProjectBundle { // Sets engine switches. void SetSwitches(const std::vector& switches); + void SetAotLibraryPath(const std::filesystem::path& aot_library_path) { + aot_library_path_ = aot_library_path; + } + // Attempts to load AOT data for this bundle. The returned data must be // retained until any engine instance it is passed to has been shut down. // diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows.dll.def b/engine/src/flutter/shell/platform/windows/flutter_windows.dll.def new file mode 100644 index 0000000000000..062c655d31e8e --- /dev/null +++ b/engine/src/flutter/shell/platform/windows/flutter_windows.dll.def @@ -0,0 +1,16 @@ +EXPORTS + shorebird_check_for_downloadable_update = shorebird_check_for_downloadable_update + shorebird_check_for_update = shorebird_check_for_update + shorebird_current_boot_patch_number = shorebird_current_boot_patch_number + shorebird_free_string = shorebird_free_string + shorebird_free_update_result = shorebird_free_update_result + shorebird_init = shorebird_init + shorebird_next_boot_patch_number = shorebird_next_boot_patch_number + shorebird_next_boot_patch_path = shorebird_next_boot_patch_path + shorebird_report_launch_failure = shorebird_report_launch_failure + shorebird_report_launch_start = shorebird_report_launch_start + shorebird_report_launch_success = shorebird_report_launch_success + shorebird_should_auto_update = shorebird_should_auto_update + shorebird_start_update_thread = shorebird_start_update_thread + shorebird_update = shorebird_update + shorebird_update_with_result = shorebird_update_with_result \ No newline at end of file diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc index d08591a10b520..c6cff1b63b53d 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc @@ -5,15 +5,20 @@ #include "flutter/shell/platform/windows/flutter_windows_engine.h" #include +#include +#include +#include #include #include #include +#include #include "flutter/fml/logging.h" #include "flutter/fml/paths.h" #include "flutter/fml/platform/win/wstring_conversion.h" #include "flutter/fml/synchronization/waitable_event.h" +#include "flutter/shell/common/shorebird/shorebird.h" #include "flutter/shell/platform/common/client_wrapper/binary_messenger_impl.h" #include "flutter/shell/platform/common/client_wrapper/include/flutter/standard_message_codec.h" #include "flutter/shell/platform/common/path_utils.h" @@ -26,6 +31,7 @@ #include "flutter/shell/platform/windows/system_utils.h" #include "flutter/shell/platform/windows/task_runner.h" #include "flutter/third_party/accessibility/ax/ax_node.h" +#include "third_party/tonic/filesystem/filesystem/file.h" // winbase.h defines GetCurrentTime as a macro. #undef GetCurrentTime @@ -240,13 +246,125 @@ bool FlutterWindowsEngine::Run() { return Run(""); } +int GetReleaseVersionAndBuildNumber(ReleaseVersion* release_version) { + char module_path[MAX_PATH]; + // Get the full path of the currently running executable. The return value is + // the size of the string that was copied to the buffer, with -1 indicating + // failure. + if (GetModuleFileNameA(NULL, module_path, MAX_PATH) == -1) { + return -1; + } + + // Get the size of the version information + DWORD handle = -1; + DWORD version_info_size = GetFileVersionInfoSizeA(module_path, &handle); + if (version_info_size == -1) { + return -1; + } + + // Allocate memory for version info + // std::vector version_data(version_info_size); + std::unique_ptr version_data(new char[version_info_size]); + if (!GetFileVersionInfoA(module_path, handle, version_info_size, + version_data.get())) { + return -1; + } + + // Get the version info structure + VS_FIXEDFILEINFO* file_info = nullptr; + UINT file_info_size = -1; + if (!VerQueryValueA(version_data.get(), "\\", + reinterpret_cast(&file_info), &file_info_size)) { + return -1; + } + + if (file_info) { + // Extract version numbers + DWORD major = HIWORD(file_info->dwFileVersionMS); + DWORD minor = LOWORD(file_info->dwFileVersionMS); + DWORD build = HIWORD(file_info->dwFileVersionLS); + + char version[49]; + snprintf(version, sizeof(version), "%lu.%lu.%lu", major, minor, build); + release_version->version = std::string(version); + release_version->build_number = + std::to_string(LOWORD(file_info->dwFileVersionLS)); + return kSuccess; + } + + return -1; +} + +bool GetLocalAppDataPath(std::string& outPath) { + PWSTR path = nullptr; + HRESULT result = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, NULL, &path); + if (!SUCCEEDED(result)) { + return false; + } + + std::wstring widePath(path); + std::string localAppDataPath(widePath.begin(), widePath.end()); + // The calling process is responsible for freeing this resource + // https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath + CoTaskMemFree(path); + outPath = localAppDataPath; + return true; +} + +bool SetUpShorebird(std::string assets_path_string, std::string& patch_path) { + auto shorebird_yaml_path = + fml::paths::JoinPaths({assets_path_string, "shorebird.yaml"}); + std::string shorebird_yaml_contents(""); + if (!filesystem::ReadFileToString(shorebird_yaml_path, + &shorebird_yaml_contents)) { + FML_LOG(ERROR) << "Failed to read shorebird.yaml."; + return false; + } + + std::string code_cache_path; + if (!GetLocalAppDataPath(code_cache_path)) { + FML_LOG(ERROR) << "Failed to retrieve the local AppData directory."; + return false; + } + + auto executable_location = fml::paths::GetExecutableDirectoryPath().second; + auto app_path = + fml::paths::JoinPaths({executable_location, "data", "app.so"}); + ReleaseVersion release_version; + auto release_version_result = + GetReleaseVersionAndBuildNumber(&release_version); + if (release_version_result != kSuccess) { + FML_LOG(ERROR) + << "Failed to retrieve the release version and build number."; + return false; + } + + ShorebirdConfigArgs shorebird_args(code_cache_path, code_cache_path, app_path, + shorebird_yaml_contents, release_version); + return ConfigureShorebird(shorebird_args, patch_path); +} + bool FlutterWindowsEngine::Run(std::string_view entrypoint) { + std::string assets_path_string = project_->assets_path().u8string(); + std::string icu_path_string = project_->icu_path().u8string(); + if (!project_->HasValidPaths()) { FML_LOG(ERROR) << "Missing or unresolvable paths to assets."; return false; } - std::string assets_path_string = project_->assets_path().u8string(); - std::string icu_path_string = project_->icu_path().u8string(); + + std::string patch_path; + auto setup_shorebird_result = SetUpShorebird(assets_path_string, patch_path); + if (setup_shorebird_result) { + // If we have a patch installed, we replace the default AOT library path + // with the patch path here. + FML_LOG(INFO) << "Setting project patch path: " << patch_path; + project_->SetAotLibraryPath(patch_path); + } else { + FML_LOG(ERROR) << "Failed to configure Shorebird."; + } + + // This loads AOT data from the project_'s aot_library_path_. if (embedder_api_.RunsAOTCompiledDartCode()) { aot_data_ = project_->LoadAotData(embedder_api_); if (!aot_data_) { @@ -370,6 +488,15 @@ bool FlutterWindowsEngine::Run(std::string_view entrypoint) { host->root_isolate_create_callback_(); } }; + // Copied from shell\platform\darwin\macos\framework\Source\FlutterEngine.mm + // Writes log messages to stdout. + args.log_message_callback = [](const char* tag, const char* message, + void* user_data) { + if (tag && tag[0]) { + std::cout << tag << ": "; + } + std::cout << message << std::endl; + }; args.channel_update_callback = [](const FlutterChannelUpdate* update, void* user_data) { auto host = static_cast(user_data); diff --git a/engine/src/flutter/shell/testing/BUILD.gn b/engine/src/flutter/shell/testing/BUILD.gn index 28a19c75d1bb9..ce47535e8db12 100644 --- a/engine/src/flutter/shell/testing/BUILD.gn +++ b/engine/src/flutter/shell/testing/BUILD.gn @@ -38,6 +38,7 @@ executable("testing") { deps = [ "$dart_src/runtime:libdart_jit", "$dart_src/runtime/bin:dart_io_api", + "$dart_src/runtime/bin:elf_loader", "//flutter/assets", "//flutter/common", "//flutter/flow", diff --git a/engine/src/flutter/sky/tools/create_full_ios_framework.py b/engine/src/flutter/sky/tools/create_full_ios_framework.py new file mode 100644 index 0000000000000..2738b8693302f --- /dev/null +++ b/engine/src/flutter/sky/tools/create_full_ios_framework.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python3 +# +# Copyright 2013 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. + +# Generates and zip the ios flutter framework including the architecture +# dependent snapshot. + +import argparse +import os +import platform +import shutil +import subprocess +import sys + +from create_xcframework import create_xcframework # pylint: disable=import-error + +ARCH_SUBPATH = 'mac-arm64' if platform.processor() == 'arm' else 'mac-x64' +DSYMUTIL = os.path.join( + os.path.dirname(__file__), '..', '..', 'buildtools', ARCH_SUBPATH, 'clang', 'bin', 'dsymutil' +) + +buildroot_dir = os.path.abspath(os.path.join(os.path.realpath(__file__), '..', '..', '..', '..')) + + +def main(): + parser = argparse.ArgumentParser( + description=( + 'Creates Flutter.framework, Flutter.xcframework and ' + 'copies architecture-dependent gen_snapshot binaries to output dir' + ) + ) + + parser.add_argument('--dst', type=str, required=True) + parser.add_argument('--clang-dir', type=str, default='clang_x64') + parser.add_argument('--x64-out-dir', type=str) + parser.add_argument('--arm64-out-dir', type=str, required=True) + parser.add_argument('--simulator-x64-out-dir', type=str, required=True) + parser.add_argument('--simulator-arm64-out-dir', type=str, required=False) + parser.add_argument('--strip', action='store_true', default=False) + parser.add_argument('--dsym', action='store_true', default=False) + + args = parser.parse_args() + + dst = (args.dst if os.path.isabs(args.dst) else os.path.join(buildroot_dir, args.dst)) + + arm64_out_dir = ( + args.arm64_out_dir + if os.path.isabs(args.arm64_out_dir) else os.path.join(buildroot_dir, args.arm64_out_dir) + ) + + x64_out_dir = None + if args.x64_out_dir: + x64_out_dir = ( + args.x64_out_dir + if os.path.isabs(args.x64_out_dir) else os.path.join(buildroot_dir, args.x64_out_dir) + ) + + simulator_x64_out_dir = None + if args.simulator_x64_out_dir: + simulator_x64_out_dir = ( + args.simulator_x64_out_dir if os.path.isabs(args.simulator_x64_out_dir) else + os.path.join(buildroot_dir, args.simulator_x64_out_dir) + ) + + framework = os.path.join(dst, 'Flutter.framework') + simulator_framework = os.path.join(dst, 'sim', 'Flutter.framework') + arm64_framework = os.path.join(arm64_out_dir, 'Flutter.framework') + simulator_x64_framework = os.path.join(simulator_x64_out_dir, 'Flutter.framework') + + simulator_arm64_out_dir = None + if args.simulator_arm64_out_dir: + simulator_arm64_out_dir = ( + args.simulator_arm64_out_dir if os.path.isabs(args.simulator_arm64_out_dir) else + os.path.join(buildroot_dir, args.simulator_arm64_out_dir) + ) + + if args.simulator_arm64_out_dir is not None: + simulator_arm64_framework = os.path.join(simulator_arm64_out_dir, 'Flutter.framework') + + if not os.path.isdir(arm64_framework): + print('Cannot find iOS arm64 Framework at %s' % arm64_framework) + return 1 + + if not os.path.isdir(simulator_x64_framework): + print('Cannot find iOS x64 simulator Framework at %s' % simulator_framework) + return 1 + + if not os.path.isfile(DSYMUTIL): + print('Cannot find dsymutil at %s' % DSYMUTIL) + return 1 + + create_framework( + args, dst, framework, arm64_framework, simulator_framework, simulator_x64_framework, + simulator_arm64_framework + ) + + extension_safe_dst = os.path.join(dst, 'extension_safe') + create_extension_safe_framework( + args, extension_safe_dst, '%s_extension_safe' % arm64_out_dir, + '%s_extension_safe' % simulator_x64_out_dir, '%s_extension_safe' % simulator_arm64_out_dir + ) + + generate_gen_snapshot(args, dst, x64_out_dir, arm64_out_dir) + generate_analyze_snapshot(args, dst, x64_out_dir, arm64_out_dir) + zip_archive(dst) + return 0 + +def create_extension_safe_framework( # pylint: disable=too-many-arguments + args, dst, arm64_out_dir, simulator_x64_out_dir, simulator_arm64_out_dir +): + framework = os.path.join(dst, 'Flutter.framework') + simulator_framework = os.path.join(dst, 'sim', 'Flutter.framework') + arm64_framework = os.path.join(arm64_out_dir, 'Flutter.framework') + simulator_x64_framework = os.path.join(simulator_x64_out_dir, 'Flutter.framework') + simulator_arm64_framework = os.path.join(simulator_arm64_out_dir, 'Flutter.framework') + + if not os.path.isdir(arm64_framework): + print('Cannot find extension safe iOS arm64 Framework at %s' % arm64_framework) + return 1 + + if not os.path.isdir(simulator_x64_framework): + print('Cannot find extension safe iOS x64 simulator Framework at %s' % simulator_x64_framework) + return 1 + + create_framework( + args, dst, framework, arm64_framework, simulator_framework, simulator_x64_framework, + simulator_arm64_framework + ) + return 0 + +def create_framework( # pylint: disable=too-many-arguments + args, dst, framework, arm64_framework, simulator_framework, + simulator_x64_framework, simulator_arm64_framework +): + arm64_dylib = os.path.join(arm64_framework, 'Flutter') + simulator_x64_dylib = os.path.join(simulator_x64_framework, 'Flutter') + simulator_arm64_dylib = os.path.join(simulator_arm64_framework, 'Flutter') + if not os.path.isfile(arm64_dylib): + print('Cannot find iOS arm64 dylib at %s' % arm64_dylib) + return 1 + + if not os.path.isfile(simulator_x64_dylib): + print('Cannot find iOS simulator dylib at %s' % simulator_x64_dylib) + return 1 + + # Compute dsym output paths, if enabled. + framework_dsym = None + simulator_dsym = None + if args.dsym: + framework_dsym = framework + '.dSYM' + simulator_dsym = simulator_framework + '.dSYM' + + # Emit the framework for physical devices. + shutil.rmtree(framework, True) + shutil.copytree(arm64_framework, framework) + framework_binary = os.path.join(framework, 'Flutter') + process_framework(args, dst, framework_binary, framework_dsym) + + # Emit the framework for simulators. + if args.simulator_arm64_out_dir is not None: + shutil.rmtree(simulator_framework, True) + shutil.copytree(simulator_arm64_framework, simulator_framework) + + simulator_framework_binary = os.path.join(simulator_framework, 'Flutter') + + # Create the arm64/x64 simulator fat framework. + subprocess.check_call([ + 'lipo', simulator_x64_dylib, simulator_arm64_dylib, '-create', '-output', + simulator_framework_binary + ]) + process_framework(args, dst, simulator_framework_binary, simulator_dsym) + else: + simulator_framework = simulator_x64_framework + + # Create XCFramework from the arm-only fat framework and the arm64/x64 + # simulator frameworks, or just the x64 simulator framework if only that one + # exists. + xcframeworks = [simulator_framework, framework] + dsyms = [simulator_dsym, framework_dsym] if args.dsym else None + create_xcframework(location=dst, name='Flutter', frameworks=xcframeworks, dsyms=dsyms) + + # Add the x64 simulator into the fat framework. + subprocess.check_call([ + 'lipo', arm64_dylib, simulator_x64_dylib, '-create', '-output', framework_binary + ]) + + process_framework(args, dst, framework_binary, framework_dsym) + return 0 + + +def embed_codesign_configuration(config_path, contents): + with open(config_path, 'w') as file: + file.write('\n'.join(contents) + '\n') + + +def zip_archive(dst): + ios_file_with_entitlements = ['gen_snapshot_arm64'] + ios_file_without_entitlements = [ + 'Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', + 'Flutter.xcframework/ios-arm64/dSYMs/Flutter.framework.dSYM/Contents/Resources/DWARF/Flutter', + 'Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', + 'Flutter.xcframework/ios-arm64_x86_64-simulator/dSYMs/Flutter.framework.dSYM/Contents/Resources/DWARF/Flutter', # pylint: disable=line-too-long + 'extension_safe/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', + 'extension_safe/Flutter.xcframework/ios-arm64/dSYMs/Flutter.framework.dSYM/Contents/Resources/DWARF/Flutter', # pylint: disable=line-too-long + 'extension_safe/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', + 'extension_safe/Flutter.xcframework/ios-arm64_x86_64-simulator/dSYMs/Flutter.framework.dSYM/Contents/Resources/DWARF/Flutter' # pylint: disable=line-too-long + ] + embed_codesign_configuration(os.path.join(dst, 'entitlements.txt'), ios_file_with_entitlements) + + embed_codesign_configuration( + os.path.join(dst, 'without_entitlements.txt'), ios_file_without_entitlements + ) + + subprocess.check_call([ + 'zip', + '-r', + 'artifacts.zip', + 'analyze_snapshot_arm64', + 'gen_snapshot_arm64', + 'Flutter.xcframework', + 'entitlements.txt', + 'without_entitlements.txt', + 'extension_safe/Flutter.xcframework', + ], + cwd=dst) + + # Generate Flutter.dSYM.zip for manual symbolification. + # + # Historically, the framework dSYM was named Flutter.dSYM, so in order to + # remain backward-compatible with existing instructions in docs/Crashes.md + # and existing tooling such as dart-lang/dart_ci, we rename back to that name + # + # TODO(cbracken): remove these archives and the upload steps once we bundle + # dSYMs in app archives. https://github.com/flutter/flutter/issues/116493 + framework_dsym = os.path.join(dst, 'Flutter.framework.dSYM') + if os.path.exists(framework_dsym): + renamed_dsym = framework_dsym.replace('Flutter.framework.dSYM', 'Flutter.dSYM') + os.rename(framework_dsym, renamed_dsym) + subprocess.check_call(['zip', '-r', 'Flutter.dSYM.zip', 'Flutter.dSYM'], cwd=dst) + + extension_safe_dsym = os.path.join(dst, 'extension_safe', 'Flutter.framework.dSYM') + if os.path.exists(extension_safe_dsym): + renamed_dsym = extension_safe_dsym.replace('Flutter.framework.dSYM', 'Flutter.dSYM') + os.rename(extension_safe_dsym, renamed_dsym) + subprocess.check_call(['zip', '-r', 'extension_safe_Flutter.dSYM.zip', 'Flutter.dSYM'], cwd=dst) + + +def process_framework(args, dst, framework_binary, dsym): + if dsym: + subprocess.check_call([DSYMUTIL, '-o', dsym, framework_binary]) + + if args.strip: + # copy unstripped + unstripped_out = os.path.join(dst, 'Flutter.unstripped') + shutil.copyfile(framework_binary, unstripped_out) + subprocess.check_call(['strip', '-x', '-S', framework_binary]) + + +def generate_gen_snapshot(args, dst, x64_out_dir, arm64_out_dir): + if x64_out_dir: + _generate_gen_snapshot(x64_out_dir, os.path.join(dst, 'gen_snapshot_x64')) + + if arm64_out_dir: + _generate_gen_snapshot( + os.path.join(arm64_out_dir, args.clang_dir), os.path.join(dst, 'gen_snapshot_arm64') + ) + + +def _generate_gen_snapshot(directory, destination): + gen_snapshot_dir = os.path.join(directory, 'gen_snapshot') + if not os.path.isfile(gen_snapshot_dir): + print('Cannot find gen_snapshot at %s' % gen_snapshot_dir) + sys.exit(1) + + subprocess.check_call(['xcrun', 'bitcode_strip', '-r', gen_snapshot_dir, '-o', destination]) + + +def generate_analyze_snapshot(args, dst, x64_out_dir, arm64_out_dir): + if x64_out_dir: + _generate_analyze_snapshot(x64_out_dir, os.path.join(dst, 'analyze_snapshot_x64')) + + if arm64_out_dir: + _generate_analyze_snapshot( + os.path.join(arm64_out_dir, args.clang_dir), os.path.join(dst, 'analyze_snapshot_arm64') + ) + + +def _generate_analyze_snapshot(directory, destination): + analyze_snapshot_dir = os.path.join(directory, 'analyze_snapshot') + if not os.path.isfile(analyze_snapshot_dir): + print('Cannot find analyze_snapshot at %s' % analyze_snapshot_dir) + sys.exit(1) + + subprocess.check_call(['xcrun', 'bitcode_strip', '-r', analyze_snapshot_dir, '-o', destination]) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/engine/src/flutter/sky/tools/create_ios_framework.py b/engine/src/flutter/sky/tools/create_ios_framework.py index 2f90d9816b5c8..9d41c62a8b53a 100644 --- a/engine/src/flutter/sky/tools/create_ios_framework.py +++ b/engine/src/flutter/sky/tools/create_ios_framework.py @@ -19,7 +19,8 @@ def main(): parser = argparse.ArgumentParser( description=( 'Creates Flutter.framework, Flutter.xcframework and ' - 'copies architecture-dependent gen_snapshot binaries to output dir' + 'copies architecture-dependent analyze_snapshot and gen_snapshot ' + 'binaries to output dir' ) ) @@ -82,13 +83,17 @@ def main(): '%s_extension_safe' % simulator_x64_out_dir, '%s_extension_safe' % simulator_arm64_out_dir ) - # Copy gen_snapshot binary to destination directory. + # Copy analyze_snapshot and gen_snapshot binaries to destination directory. if arm64_out_dir: gen_snapshot = os.path.join(arm64_out_dir, 'gen_snapshot_arm64') + analyze_snapshot = os.path.join(arm64_out_dir, 'analyze_snapshot_arm64') sky_utils.copy_binary(gen_snapshot, os.path.join(dst, 'gen_snapshot_arm64')) + sky_utils.copy_binary(analyze_snapshot, os.path.join(dst, 'analyze_snapshot_arm64')) if x64_out_dir: gen_snapshot = os.path.join(x64_out_dir, 'gen_snapshot_x64') + analyze_snapshot = os.path.join(x64_out_dir, 'analyze_snapshot_x64') sky_utils.copy_binary(gen_snapshot, os.path.join(dst, 'gen_snapshot_x64')) + sky_utils.copy_binary(analyze_snapshot, os.path.join(dst, 'analyze_snapshot_x64')) zip_archive(dst, args) return 0 @@ -170,7 +175,7 @@ def zip_archive(dst, args): # See: https://github.com/flutter/flutter/blob/62382c7b83a16b3f48dc06c19a47f6b8667005a5/dev/bots/suite_runners/run_verify_binaries_codesigned_tests.dart#L82-L130 # Binaries that must be codesigned and require entitlements for particular APIs. - with_entitlements = ['gen_snapshot_arm64'] + with_entitlements = ['analyze_snapshot_arm64', 'gen_snapshot_arm64'] with_entitlements_file = os.path.join(dst, 'entitlements.txt') sky_utils.write_codesign_config(with_entitlements_file, with_entitlements) @@ -196,6 +201,7 @@ def zip_archive(dst, args): # pylint: enable=line-too-long zip_contents = [ + 'analyze_snapshot_arm64', 'gen_snapshot_arm64', 'Flutter.xcframework', 'entitlements.txt', diff --git a/engine/src/flutter/testing/run_tests.py b/engine/src/flutter/testing/run_tests.py index f3230d3ba208d..a7937a6455f8f 100755 --- a/engine/src/flutter/testing/run_tests.py +++ b/engine/src/flutter/testing/run_tests.py @@ -446,6 +446,7 @@ def make_test(name, flags=None, extra_env=None): make_test('platform_view_android_delegate_unittests'), # https://github.com/flutter/flutter/issues/36295 make_test('shell_unittests'), + make_test('shorebird_unittests'), ] if is_windows(): diff --git a/shell/common/shorebird/BUILD.gn b/shell/common/shorebird/BUILD.gn new file mode 100644 index 0000000000000..4e31c785af768 --- /dev/null +++ b/shell/common/shorebird/BUILD.gn @@ -0,0 +1,56 @@ +import("//flutter/common/config.gni") +import("//flutter/testing/testing.gni") + +source_set("snapshots_data_handle") { + sources = [ + "snapshots_data_handle.cc", + "snapshots_data_handle.h", + ] + + deps = [ + "//flutter/fml", + "//flutter/runtime", + "//flutter/runtime:libdart", + "//flutter/shell/common", + ] +} + +source_set("shorebird") { + sources = [ + "shorebird.cc", + "shorebird.h", + ] + + deps = [ + ":snapshots_data_handle", + "//flutter/fml", + "//flutter/runtime", + "//flutter/runtime:libdart", + "//flutter/shell/common", + "//flutter/shell/platform/embedder:embedder_headers", + ] + + include_dirs = [ "//flutter/updater" ] +} + +if (enable_unittests) { + test_fixtures("shorebird_fixtures") { + fixtures = [] + } + + executable("shorebird_unittests") { + testonly = true + + sources = [ "snapshots_data_handle_unittests.cc" ] + + # This only includes snapshots_data_handle and not shorebird because + # shorebird fails to link due to a missing updater lib. + deps = [ + ":shorebird_fixtures", + ":snapshots_data_handle", + "//flutter/runtime", + "//flutter/testing", + "//flutter/testing:fixture_test", + ] + } +} diff --git a/shell/common/shorebird/shorebird.cc b/shell/common/shorebird/shorebird.cc new file mode 100644 index 0000000000000..602a0521ae7b0 --- /dev/null +++ b/shell/common/shorebird/shorebird.cc @@ -0,0 +1,299 @@ + +#include "flutter/shell/common/shorebird/shorebird.h" + +#include +#include +#include +#include +#include + +#include "flutter/fml/command_line.h" +#include "flutter/fml/file.h" +#include "flutter/fml/macros.h" +#include "flutter/fml/mapping.h" +#include "flutter/fml/message_loop.h" +#include "flutter/fml/native_library.h" +#include "flutter/fml/paths.h" +#include "flutter/lib/ui/plugins/callback_cache.h" +#include "flutter/runtime/dart_snapshot.h" +#include "flutter/runtime/dart_vm.h" +#include "flutter/shell/common/shell.h" +#include "flutter/shell/common/shorebird/snapshots_data_handle.h" +#include "flutter/shell/common/switches.h" +#include "fml/logging.h" +#include "shell/platform/embedder/embedder.h" +#include "third_party/dart/runtime/include/dart_tools_api.h" + +#include "third_party/updater/library/include/updater.h" + +// Namespaced to avoid Google style warnings. +namespace flutter { + +// Old Android versions (e.g. the v16 ndk Flutter uses) don't always include a +// getauxval symbol, but the Rust ring crate assumes it exists: +// https://github.com/briansmith/ring/blob/fa25bf3a7403c9fe6458cb87bd8427be41225ca2/src/cpu/arm.rs#L22 +// It uses it to determine if the CPU supports AES instructions. +// Making this a weak symbol allows the linker to use a real version instead +// if it can find one. +// BoringSSL just reads from procfs instead, which is what we would do if +// we needed to implement this ourselves. Implementation looks straightforward: +// https://lwn.net/Articles/519085/ +// https://github.com/google/boringssl/blob/6ab4f0ae7f2db96d240eb61a5a8b4724e5a09b2f/crypto/cpu_arm_linux.c +#if defined(__ANDROID__) && defined(__arm__) +extern "C" __attribute__((weak)) unsigned long getauxval(unsigned long type) { + return 0; +} +#endif + +// TODO(eseidel): I believe we need to leak these or we'll sometimes crash +// when using the base snapshot in mixed mode. This likely will not play +// nicely with multi-engine support and will need to be refactored. +static fml::RefPtr vm_snapshot; +static fml::RefPtr isolate_snapshot; + +void SetBaseSnapshot(Settings& settings) { + // These mappings happen to be to static data in the App.framework, but + // we still need to seem to hold onto the DartSnapshot objects to keep + // the mappings alive. + vm_snapshot = DartSnapshot::VMSnapshotFromSettings(settings); + isolate_snapshot = DartSnapshot::IsolateSnapshotFromSettings(settings); + Shorebird_SetBaseSnapshots(isolate_snapshot->GetDataMapping(), + isolate_snapshot->GetInstructionsMapping(), + vm_snapshot->GetDataMapping(), + vm_snapshot->GetInstructionsMapping()); +} + +class FileCallbacksImpl { + public: + static void* Open(); + static uintptr_t Read(void* file, uint8_t* buffer, uintptr_t length); + static int64_t Seek(void* file, int64_t offset, int32_t whence); + static void Close(void* file); +}; + +FileCallbacks ShorebirdFileCallbacks() { + return { + .open = FileCallbacksImpl::Open, + .read = FileCallbacksImpl::Read, + .seek = FileCallbacksImpl::Seek, + .close = FileCallbacksImpl::Close, + }; +} + +// FIXME: consolidate this with the other ConfigureShorebird +bool ConfigureShorebird(const ShorebirdConfigArgs& args, + std::string& patch_path) { + patch_path = args.release_app_library_path; + auto shorebird_updater_dir_name = "shorebird_updater"; + + auto code_cache_dir = fml::paths::JoinPaths( + {std::move(args.code_cache_path), shorebird_updater_dir_name}); + auto app_storage_dir = fml::paths::JoinPaths( + {std::move(args.app_storage_path), shorebird_updater_dir_name}); + + fml::CreateDirectory(fml::paths::GetCachesDirectory(), + {shorebird_updater_dir_name}, + fml::FilePermission::kReadWrite); + + bool init_result; + // Using a block to make AppParameters lifetime explicit. + { + AppParameters app_parameters; + // Combine version and version_code into a single string. + // We could also pass these separately through to the updater if needed. + auto release_version = + args.release_version.version + "+" + args.release_version.build_number; + app_parameters.release_version = release_version.c_str(); + app_parameters.code_cache_dir = code_cache_dir.c_str(); + app_parameters.app_storage_dir = app_storage_dir.c_str(); + + // https://stackoverflow.com/questions/26032039/convert-vectorstring-into-char-c + std::vector c_paths{}; + c_paths.push_back(args.release_app_library_path.c_str()); + // Do not modify application_library_path or c_strings will invalidate. + + app_parameters.original_libapp_paths = c_paths.data(); + app_parameters.original_libapp_paths_size = c_paths.size(); + + // shorebird_init copies from app_parameters and shorebirdYaml. + init_result = shorebird_init(&app_parameters, ShorebirdFileCallbacks(), + args.shorebird_yaml.c_str()); + } + + // We've decided not to support synchronous updates on launch for now. + // It's a terrible user experience (having the app hang on launch) and + // instead we will provide examples of how to build a custom update UI + // within Dart, including updating as part of login, etc. + // https://github.com/shorebirdtech/shorebird/issues/950 + + FML_LOG(INFO) << "Checking for active patch"; + char* c_active_path = shorebird_next_boot_patch_path(); + if (c_active_path != NULL) { + patch_path = c_active_path; + shorebird_free_string(c_active_path); + FML_LOG(INFO) << "Shorebird updater: patch path: " << patch_path; + } else { + FML_LOG(INFO) << "Shorebird updater: no active patch."; + } + + // We are careful only to report a launch start in the case where it's the + // first time we've configured shorebird this process. Otherwise we could end + // up in a case where we report a launch start, but never a completion (e.g. + // from package:flutter_work_manager which sometimes creates a FlutterEngine + // (and thus configures shorebird) but never runs it. The proper fix for this + // is probably to move the launch_start() call to be later in the lifecycle + // (when the snapshot is loaded and run, rather than when FlutterEngine is + // initialized). This "hack" will still have a problem where FlutterEngine is + // initialized but never run before the app is quit, could still cause us to + // suddenly mark-bad a patch that was never actually attempted to launch. + if (!init_result) { + return false; + } + + // Once start_update_thread is called, the next_boot_patch* functions may + // change their return values if the shorebird_report_launch_failed + // function is called. + shorebird_report_launch_start(); + + if (shorebird_should_auto_update()) { + FML_LOG(INFO) << "Starting Shorebird update"; + shorebird_start_update_thread(); + } else { + FML_LOG(INFO) + << "Shorebird auto_update disabled, not checking for updates."; + } + + return true; +} + +void ConfigureShorebird(std::string code_cache_path, + std::string app_storage_path, + Settings& settings, + const std::string& shorebird_yaml, + const std::string& version, + const std::string& version_code) { + // If you are crashing here, you probably are running Shorebird in a Debug + // config, where the AOT snapshot won't be linked into the process, and thus + // lookups will fail. Change your Scheme to Release to fix: + // https://github.com/flutter/flutter/wiki/Debugging-the-engine#debugging-ios-builds-with-xcode + FML_CHECK(DartSnapshot::VMSnapshotFromSettings(settings)) + << "XCode Scheme must be set to Release to use Shorebird"; + + auto shorebird_updater_dir_name = "shorebird_updater"; + + auto code_cache_dir = fml::paths::JoinPaths( + {std::move(code_cache_path), shorebird_updater_dir_name}); + auto app_storage_dir = fml::paths::JoinPaths( + {std::move(app_storage_path), shorebird_updater_dir_name}); + + fml::CreateDirectory(fml::paths::GetCachesDirectory(), + {shorebird_updater_dir_name}, + fml::FilePermission::kReadWrite); + + bool init_result; + // Using a block to make AppParameters lifetime explicit. + { + AppParameters app_parameters; + // Combine version and version_code into a single string. + // We could also pass these separately through to the updater if needed. + auto release_version = version + "+" + version_code; + app_parameters.release_version = release_version.c_str(); + app_parameters.code_cache_dir = code_cache_dir.c_str(); + app_parameters.app_storage_dir = app_storage_dir.c_str(); + + // https://stackoverflow.com/questions/26032039/convert-vectorstring-into-char-c + std::vector c_paths{}; + for (const auto& string : settings.application_library_path) { + c_paths.push_back(string.c_str()); + } + // Do not modify application_library_path or c_strings will invalidate. + + app_parameters.original_libapp_paths = c_paths.data(); + app_parameters.original_libapp_paths_size = c_paths.size(); + + // shorebird_init copies from app_parameters and shorebirdYaml. + init_result = shorebird_init(&app_parameters, ShorebirdFileCallbacks(), + shorebird_yaml.c_str()); + } + + // We've decided not to support synchronous updates on launch for now. + // It's a terrible user experience (having the app hang on launch) and + // instead we will provide examples of how to build a custom update UI + // within Dart, including updating as part of login, etc. + // https://github.com/shorebirdtech/shorebird/issues/950 + + // We only set the base snapshot on iOS for now. +#if SHOREBIRD_USE_INTERPRETER + SetBaseSnapshot(settings); +#endif + + char* c_active_path = shorebird_next_boot_patch_path(); + if (c_active_path != NULL) { + std::string active_path = c_active_path; + shorebird_free_string(c_active_path); + FML_LOG(INFO) << "Shorebird updater: active path: " << active_path; + +#if SHOREBIRD_USE_INTERPRETER + // On iOS we add the patch to the front of the list instead of clearing + // the list, to allow dart_shapshot.cc to still find the base snapshot + // for the vm isolate. + settings.application_library_path.insert( + settings.application_library_path.begin(), active_path); +#else + settings.application_library_path.clear(); + settings.application_library_path.emplace_back(active_path); +#endif + } else { + FML_LOG(INFO) << "Shorebird updater: no active patch."; + } + + // We are careful only to report a launch start in the case where it's the + // first time we've configured shorebird this process. Otherwise we could end + // up in a case where we report a launch start, but never a completion (e.g. + // from package:flutter_work_manager which sometimes creates a FlutterEngine + // (and thus configures shorebird) but never runs it. The proper fix for this + // is probably to move the launch_start() call to be later in the lifecycle + // (when the snapshot is loaded and run, rather than when FlutterEngine is + // initialized). This "hack" will still have a problem where FlutterEngine is + // initialized but never run before the app is quit, could still cause us to + // suddenly mark-bad a patch that was never actually attempted to launch. + if (!init_result) { + return; + } + + // Once start_update_thread is called, the next_boot_patch* functions may + // change their return values if the shorebird_report_launch_failed + // function is called. + shorebird_report_launch_start(); + + if (shorebird_should_auto_update()) { + FML_LOG(INFO) << "Starting Shorebird update"; + shorebird_start_update_thread(); + } else { + FML_LOG(INFO) + << "Shorebird auto_update disabled, not checking for updates."; + } +} + +void* FileCallbacksImpl::Open() { + return SnapshotsDataHandle::createForSnapshots(*vm_snapshot, + *isolate_snapshot) + .release(); +} + +uintptr_t FileCallbacksImpl::Read(void* file, + uint8_t* buffer, + uintptr_t length) { + return reinterpret_cast(file)->Read(buffer, length); +} + +int64_t FileCallbacksImpl::Seek(void* file, int64_t offset, int32_t whence) { + // Currently we only support blob handles. + return reinterpret_cast(file)->Seek(offset, whence); +} + +void FileCallbacksImpl::Close(void* file) { + delete reinterpret_cast(file); +} + +} // namespace flutter \ No newline at end of file diff --git a/shell/common/shorebird/shorebird.h b/shell/common/shorebird/shorebird.h new file mode 100644 index 0000000000000..3e9ccce9ee3f6 --- /dev/null +++ b/shell/common/shorebird/shorebird.h @@ -0,0 +1,45 @@ +#ifndef FLUTTER_SHELL_COMMON_SHOREBIRD_SHOREBIRD_H_ +#define FLUTTER_SHELL_COMMON_SHOREBIRD_SHOREBIRD_H_ + +#include "flutter/common/settings.h" +#include "shell/platform/embedder/embedder.h" + +namespace flutter { + +struct ReleaseVersion { + std::string version; + std::string build_number; +}; + +struct ShorebirdConfigArgs { + std::string code_cache_path; + std::string app_storage_path; + std::string release_app_library_path; + std::string shorebird_yaml; + ReleaseVersion release_version; + + ShorebirdConfigArgs(std::string code_cache_path, + std::string app_storage_path, + std::string release_app_library_path, + std::string shorebird_yaml, + ReleaseVersion release_version) + : code_cache_path(code_cache_path), + app_storage_path(app_storage_path), + release_app_library_path(release_app_library_path), + shorebird_yaml(shorebird_yaml), + release_version(release_version) {} +}; + +bool ConfigureShorebird(const ShorebirdConfigArgs& args, + std::string& patch_path); + +void ConfigureShorebird(std::string code_cache_path, + std::string app_storage_path, + Settings& settings, + const std::string& shorebird_yaml, + const std::string& version, + const std::string& version_code); + +} // namespace flutter + +#endif // FLUTTER_SHELL_COMMON_SHOREBIRD_SHOREBIRD_H_ diff --git a/shell/common/shorebird/snapshots_data_handle.cc b/shell/common/shorebird/snapshots_data_handle.cc new file mode 100644 index 0000000000000..0c6c5a45450a1 --- /dev/null +++ b/shell/common/shorebird/snapshots_data_handle.cc @@ -0,0 +1,144 @@ +#include "flutter/shell/common/shorebird/snapshots_data_handle.h" + +#include "third_party/dart/runtime/include/dart_native_api.h" + +namespace flutter { + +static std::unique_ptr DataMapping(const DartSnapshot& snapshot) { + auto ptr = snapshot.GetDataMapping(); + return std::make_unique(ptr, + Dart_SnapshotDataSize(ptr)); +} + +static std::unique_ptr InstructionsMapping( + const DartSnapshot& snapshot) { + auto ptr = snapshot.GetInstructionsMapping(); + return std::make_unique(ptr, + Dart_SnapshotInstrSize(ptr)); +} + +// The size of the snapshot data is the sum of the sizes of the blobs. +size_t SnapshotsDataHandle::FullSize() const { + size_t size = 0; + for (const auto& blob : blobs_) { + size += blob->GetSize(); + } + return size; +} + +// The offset into the snapshots data blobs as though they were a single +// contiguous buffer. +size_t SnapshotsDataHandle::AbsoluteOffsetForIndex(BlobsIndex index) { + if (index.blob >= blobs_.size()) { + FML_LOG(WARNING) << "Blob index " << index.blob + << " is larger than the number of blobs (" << blobs_.size() + << "). Returning full size (" << FullSize() << ")"; + return FullSize(); + } + if (index.offset > blobs_[index.blob]->GetSize()) { + FML_LOG(WARNING) << "Offset for blob " << index.blob << " (" << index.offset + << ") is larger than the blob size (" + << blobs_[index.blob]->GetSize() + << "). Returning index start of next blob"; + return AbsoluteOffsetForIndex({index.blob + 1, 0}); + } + size_t offset = 0; + for (size_t i = 0; i < index.blob; i++) { + offset += blobs_[i]->GetSize(); + } + offset += index.offset; + return offset; +} + +BlobsIndex SnapshotsDataHandle::IndexForAbsoluteOffset(int64_t offset, + BlobsIndex start_index) { + size_t start_offset = AbsoluteOffsetForIndex(start_index); + if (offset < 0) { + if ((size_t)abs(offset) > start_offset) { + FML_LOG(WARNING) + << "Offset is before the beginning of SnapshotsData. Returning 0, 0"; + return {0, 0}; + } + } else if (offset + start_offset >= FullSize()) { + FML_LOG(WARNING) << "Target offset is past the end of SnapshotsData (" + << offset + start_offset << ", blobs size:" << FullSize() + << "). Returning last blob index and offset"; + return {blobs_.size(), blobs_.back()->GetSize()}; + } + + size_t dest_offset = start_offset + offset; + BlobsIndex index = {0, 0}; + for (const auto& blob : blobs_) { + if (dest_offset < blob->GetSize()) { + // The remaining offset is within this blob. + index.offset = dest_offset; + break; + } + + index.blob++; + dest_offset -= blob->GetSize(); + } + return index; +} + +std::unique_ptr SnapshotsDataHandle::createForSnapshots( + const DartSnapshot& vm_snapshot, + const DartSnapshot& isolate_snapshot) { + // This needs to match the order in which the blobs are written out in + // analyze_snapshot --dump_blobs + std::vector> blobs; + blobs.push_back(DataMapping(vm_snapshot)); + blobs.push_back(DataMapping(isolate_snapshot)); + blobs.push_back(InstructionsMapping(vm_snapshot)); + blobs.push_back(InstructionsMapping(isolate_snapshot)); + return std::make_unique(std::move(blobs)); +} + +uintptr_t SnapshotsDataHandle::Read(uint8_t* buffer, uintptr_t length) { + uintptr_t bytes_read = 0; + // Copy current blob from current offset and possibly into the next blob + // until we have read length bytes. + while (bytes_read < length) { + if (current_index_.blob >= blobs_.size()) { + // We have read all blobs. + break; + } + intptr_t remaining_blob_length = + blobs_[current_index_.blob]->GetSize() - current_index_.offset; + if (remaining_blob_length <= 0) { + // We have read all bytes in this blob. + current_index_.blob++; + current_index_.offset = 0; + continue; + } + intptr_t bytes_to_read = fmin(length - bytes_read, remaining_blob_length); + memcpy(buffer + bytes_read, + blobs_[current_index_.blob]->GetMapping() + current_index_.offset, + bytes_to_read); + bytes_read += bytes_to_read; + current_index_.offset += bytes_to_read; + } + + return bytes_read; +} + +int64_t SnapshotsDataHandle::Seek(int64_t offset, int32_t whence) { + BlobsIndex start_index; + switch (whence) { + case SEEK_CUR: + start_index = current_index_; + break; + case SEEK_SET: + start_index = {0, 0}; + break; + case SEEK_END: + start_index = {blobs_.size(), blobs_.back()->GetSize()}; + break; + default: + FML_CHECK(false) << "Unrecognized whence value in Seek: " << whence; + } + current_index_ = IndexForAbsoluteOffset(offset, start_index); + return current_index_.offset; +} + +} // namespace flutter \ No newline at end of file diff --git a/shell/common/shorebird/snapshots_data_handle.h b/shell/common/shorebird/snapshots_data_handle.h new file mode 100644 index 0000000000000..7a7ec5274d002 --- /dev/null +++ b/shell/common/shorebird/snapshots_data_handle.h @@ -0,0 +1,47 @@ +#ifndef FLUTTER_SHELL_COMMON_SHOREBIRD_SNAPSHOTS_DATA_HANDLE_H_ +#define FLUTTER_SHELL_COMMON_SHOREBIRD_SNAPSHOTS_DATA_HANDLE_H_ + +#include "flutter/fml/file.h" +#include "flutter/runtime/dart_snapshot.h" +#include "third_party/dart/runtime/include/dart_tools_api.h" + +namespace flutter { + +// An offset into an indexed collection of buffers. blob is the index of the +// buffer, and offset is the offset into that buffer. +struct BlobsIndex { + size_t blob; + size_t offset; +}; + +// Implements a POSIX file I/O interface which allows us to provide the four +// data blobs of a Dart snapshot (vm_data, vm_instructions, isolate_data, +// isolate_instructions) to Rust as though it were a single piece of memory. +class SnapshotsDataHandle { + public: + // This would ideally be private, but we need to be able to call it from the + // static createForSnapshots method. + explicit SnapshotsDataHandle(std::vector> blobs) + : blobs_(std::move(blobs)) {} + + static std::unique_ptr createForSnapshots( + const DartSnapshot& vm_snapshot, + const DartSnapshot& isolate_snapshot); + + uintptr_t Read(uint8_t* buffer, uintptr_t length); + int64_t Seek(int64_t offset, int32_t whence); + + // The sum of all the blobs' sizes. + size_t FullSize() const; + + private: + size_t AbsoluteOffsetForIndex(BlobsIndex index); + BlobsIndex IndexForAbsoluteOffset(int64_t offset, BlobsIndex startIndex); + + BlobsIndex current_index_ = {0, 0}; + std::vector> blobs_; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_COMMON_SHOREBIRD_SNAPSHOTS_DATA_HANDLE_H_ diff --git a/shell/common/shorebird/snapshots_data_handle_unittests.cc b/shell/common/shorebird/snapshots_data_handle_unittests.cc new file mode 100644 index 0000000000000..2acf44a7b8973 --- /dev/null +++ b/shell/common/shorebird/snapshots_data_handle_unittests.cc @@ -0,0 +1,177 @@ +#include +#include +#include + +#include "flutter/shell/common/shorebird/snapshots_data_handle.h" + +#include "flutter/fml/mapping.h" +#include "flutter/runtime/dart_snapshot.h" +#include "flutter/testing/testing.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "testing/fixture_test.h" + +namespace flutter { +namespace testing { + +std::unique_ptr MakeHandle( + std::vector& blobs) { + // Map the strings into non-owned mappings: + std::vector> mappings = {}; + for (auto& blob : blobs) { + std::unique_ptr mapping = + std::make_unique( + reinterpret_cast(blob.data()), blob.size()); + mappings.push_back(std::move(mapping)); + } + auto handle = + std::make_unique(std::move(mappings)); + return handle; +} + +TEST(SnapshotsDataHandle, Read) { + std::vector blobs = {"abc", "def", "ghi", "jkl"}; + std::unique_ptr blobs_handle = MakeHandle(blobs); + + const size_t buffer_size = 12; + uint8_t buffer[buffer_size]; + std::fill(buffer, buffer + buffer_size, 0); + blobs_handle->Read(buffer, 6); + + EXPECT_EQ(buffer[0], 'a'); + EXPECT_EQ(buffer[1], 'b'); + EXPECT_EQ(buffer[2], 'c'); + EXPECT_EQ(buffer[3], 'd'); + EXPECT_EQ(buffer[4], 'e'); + EXPECT_EQ(buffer[5], 'f'); + + // Only the first 6 bytes should have been read. + EXPECT_EQ(buffer[6], 0); +} + +TEST(SnapshotsDataHandle, ReadAfterSeekWithPositiveOffset) { + std::vector blobs = {"abc", "def", "ghi", "jkl"}; + std::unique_ptr blobs_handle = MakeHandle(blobs); + + const size_t buffer_size = 20; + uint8_t buffer[buffer_size]; + std::fill(buffer, buffer + buffer_size, 0); + + blobs_handle->Seek(4, SEEK_CUR); + blobs_handle->Read(buffer, 6); + + EXPECT_EQ(buffer[0], 'e'); + EXPECT_EQ(buffer[1], 'f'); + EXPECT_EQ(buffer[2], 'g'); + EXPECT_EQ(buffer[3], 'h'); + EXPECT_EQ(buffer[4], 'i'); + EXPECT_EQ(buffer[5], 'j'); + + // Only the first 6 bytes should have been read. + EXPECT_EQ(buffer[6], 0); +} + +TEST(SnapshotsDataHandle, ReadAfterSeekWithNegativeOffset) { + std::vector blobs = {"abc", "def", "ghi", "jkl"}; + std::unique_ptr blobs_handle = MakeHandle(blobs); + + const size_t buffer_size = 20; + uint8_t buffer[buffer_size]; + std::fill(buffer, buffer + buffer_size, 0); + + blobs_handle->Read(buffer, 5); + EXPECT_EQ(buffer[0], 'a'); + EXPECT_EQ(buffer[1], 'b'); + EXPECT_EQ(buffer[2], 'c'); + EXPECT_EQ(buffer[3], 'd'); + EXPECT_EQ(buffer[4], 'e'); + EXPECT_EQ(buffer[5], 0); + + // Reset buffer + std::fill(buffer, buffer + buffer_size, 0); + + // Read 5, seeked back 4, should start reading at offset 1 ('b') + blobs_handle->Seek(-4, SEEK_CUR); + blobs_handle->Read(buffer, 6); + + EXPECT_EQ(buffer[0], 'b'); + EXPECT_EQ(buffer[1], 'c'); + EXPECT_EQ(buffer[2], 'd'); + EXPECT_EQ(buffer[3], 'e'); + EXPECT_EQ(buffer[4], 'f'); + EXPECT_EQ(buffer[5], 'g'); + EXPECT_EQ(buffer[6], 0); +} + +TEST(SnapshotsDataHandle, SeekPastEnd) { + std::vector blobs = {"abc", "def", "ghi", "jkl"}; + std::unique_ptr blobs_handle = MakeHandle(blobs); + + const size_t buffer_size = 20; + uint8_t buffer[buffer_size]; + std::fill(buffer, buffer + buffer_size, 0); + + // Seek 1 past the end + blobs_handle->Seek(blobs_handle->FullSize() + 1, SEEK_CUR); + + // Seek back 2 bytes and read 2 bytes + blobs_handle->Seek(-2, SEEK_CUR); + blobs_handle->Read(buffer, 2); + + EXPECT_EQ(buffer[0], 'k'); + EXPECT_EQ(buffer[1], 'l'); +} + +TEST(SnapshotsDataHandle, SeekBeforeBeginning) { + std::vector blobs = {"abc", "def", "ghi", "jkl"}; + std::unique_ptr blobs_handle = MakeHandle(blobs); + + const size_t buffer_size = 20; + uint8_t buffer[buffer_size]; + std::fill(buffer, buffer + buffer_size, 0); + + // Seek before the start of the blobs and read the first 2 bytes. + blobs_handle->Seek(-2, SEEK_CUR); + blobs_handle->Read(buffer, 2); + + EXPECT_EQ(buffer[0], 'a'); + EXPECT_EQ(buffer[1], 'b'); +} + +TEST(SnapshotsDataHandle, SeekFromBeginning) { + std::vector blobs = {"abc", "def", "ghi", "jkl"}; + std::unique_ptr blobs_handle = MakeHandle(blobs); + + const size_t buffer_size = 20; + uint8_t buffer[buffer_size]; + std::fill(buffer, buffer + buffer_size, 0); + + // Seek 10 bytes from current (the beginning) + blobs_handle->Seek(10, SEEK_CUR); + + // Seek 2 bytes from the beginning and read 2 bytes + blobs_handle->Seek(2, SEEK_SET); + blobs_handle->Read(buffer, 2); + + EXPECT_EQ(buffer[0], 'c'); + EXPECT_EQ(buffer[1], 'd'); +} + +TEST(SnapshotsDataHandle, SeekFromEnd) { + std::vector blobs = {"abc", "def", "ghi", "jkl"}; + std::unique_ptr blobs_handle = MakeHandle(blobs); + + const size_t buffer_size = 20; + uint8_t buffer[buffer_size]; + std::fill(buffer, buffer + buffer_size, 0); + + // Seek 2 bytes from the end and read 2 bytes + blobs_handle->Seek(-2, SEEK_END); + blobs_handle->Read(buffer, 2); + + EXPECT_EQ(buffer[0], 'k'); + EXPECT_EQ(buffer[1], 'l'); +} + +} // namespace testing +} // namespace flutter From 4d5b9f530f73be18907325cdd4ef29e05440cd01 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Thu, 13 Feb 2025 14:57:19 -0600 Subject: [PATCH 04/14] chore: roll dart to `d3e1bb5620319bca7ef501f147bb7853d9a415ef` (3.7.0) --- DEPS | 2 +- .../flutter/shell}/common/shorebird/BUILD.gn | 0 .../shell}/common/shorebird/shorebird.cc | 0 .../shell}/common/shorebird/shorebird.h | 0 .../common/shorebird/snapshots_data_handle.cc | 0 .../common/shorebird/snapshots_data_handle.h | 0 .../snapshots_data_handle_unittests.cc | 0 .../shell/platform/darwin/ios/BUILD.gn | 50 ++++--------------- 8 files changed, 11 insertions(+), 41 deletions(-) rename {shell => engine/src/flutter/shell}/common/shorebird/BUILD.gn (100%) rename {shell => engine/src/flutter/shell}/common/shorebird/shorebird.cc (100%) rename {shell => engine/src/flutter/shell}/common/shorebird/shorebird.h (100%) rename {shell => engine/src/flutter/shell}/common/shorebird/snapshots_data_handle.cc (100%) rename {shell => engine/src/flutter/shell}/common/shorebird/snapshots_data_handle.h (100%) rename {shell => engine/src/flutter/shell}/common/shorebird/snapshots_data_handle_unittests.cc (100%) diff --git a/DEPS b/DEPS index f115005f6e71c..8400497671749 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'skia_revision': 'e7b8d078851fd505475fe74359e31a421e6968ea', - "dart_sdk_revision": "ddb0f3b6c38b1774a3413c2c159b124be6bd1df7", + "dart_sdk_revision": "d3e1bb5620319bca7ef501f147bb7853d9a415ef", "dart_sdk_git": "git@github.com:shorebirdtech/dart-sdk.git", "updater_git": "https://github.com/shorebirdtech/updater.git", "updater_rev": "78c84e5bf72266da07df536e98d431782cb39a6d", diff --git a/shell/common/shorebird/BUILD.gn b/engine/src/flutter/shell/common/shorebird/BUILD.gn similarity index 100% rename from shell/common/shorebird/BUILD.gn rename to engine/src/flutter/shell/common/shorebird/BUILD.gn diff --git a/shell/common/shorebird/shorebird.cc b/engine/src/flutter/shell/common/shorebird/shorebird.cc similarity index 100% rename from shell/common/shorebird/shorebird.cc rename to engine/src/flutter/shell/common/shorebird/shorebird.cc diff --git a/shell/common/shorebird/shorebird.h b/engine/src/flutter/shell/common/shorebird/shorebird.h similarity index 100% rename from shell/common/shorebird/shorebird.h rename to engine/src/flutter/shell/common/shorebird/shorebird.h diff --git a/shell/common/shorebird/snapshots_data_handle.cc b/engine/src/flutter/shell/common/shorebird/snapshots_data_handle.cc similarity index 100% rename from shell/common/shorebird/snapshots_data_handle.cc rename to engine/src/flutter/shell/common/shorebird/snapshots_data_handle.cc diff --git a/shell/common/shorebird/snapshots_data_handle.h b/engine/src/flutter/shell/common/shorebird/snapshots_data_handle.h similarity index 100% rename from shell/common/shorebird/snapshots_data_handle.h rename to engine/src/flutter/shell/common/shorebird/snapshots_data_handle.h diff --git a/shell/common/shorebird/snapshots_data_handle_unittests.cc b/engine/src/flutter/shell/common/shorebird/snapshots_data_handle_unittests.cc similarity index 100% rename from shell/common/shorebird/snapshots_data_handle_unittests.cc rename to engine/src/flutter/shell/common/shorebird/snapshots_data_handle_unittests.cc diff --git a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn index cbd8073c2e002..e7cfcd2b10674 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn @@ -52,15 +52,14 @@ source_set("flutter_framework_source") { ":ios_gpu_configuration_config", "//flutter:config", ] - allow_circular_includes_from = [ ":flutter_framework_source" ] - deps = [ - ":flutter_framework_source", - "//flutter/fml", - "//flutter/shell/common/shorebird", - "//flutter/shell/platform/common:common_cpp_input", - "//flutter/shell/platform/darwin/common:framework_common", - "//flutter/third_party/icu", - ] + + if (target_cpu == "arm64") { + libs = [ "//flutter/third_party/updater/target/aarch64-apple-ios/release/libupdater.a" ] + } else if (target_cpu == "x64") { + libs = [ "//flutter/third_party/updater/target/x86_64-apple-ios/release/libupdater.a" ] + } else { + assert(false, "Unsupported target_cpu") + } sources = [ "framework/Source/FlutterAppDelegate.mm", @@ -168,37 +167,6 @@ source_set("flutter_framework_source") { ] sources += _flutter_framework_headers - defines = [ "FLUTTER_FRAMEWORK=1" ] - if (darwin_extension_safe) { - defines += [ "APPLICATION_EXTENSION_API_ONLY=1" ] - } - - deps += [ - "$dart_src/runtime/bin:elf_loader", - "//flutter/fml", - "//flutter/runtime", - "//flutter/shell/common", - "//flutter/shell/common/shorebird", - "//flutter/shell/platform/darwin/common", - "//flutter/shell/platform/darwin/common:framework_common", - "//flutter/shell/platform/embedder:embedder_as_internal_library", - "//flutter/shell/profiling:profiling", - "//flutter/third_party/spring_animation", - ] - - public_configs = [ - ":ios_gpu_configuration_config", - "//flutter:config", - ] - - if (target_cpu == "arm64") { - libs = [ "//flutter/third_party/updater/target/aarch64-apple-ios/release/libupdater.a" ] - } else if (target_cpu == "x64") { - libs = [ "//flutter/third_party/updater/target/x86_64-apple-ios/release/libupdater.a" ] - } else { - assert(false, "Unsupported target_cpu") - } - frameworks = [ "AudioToolbox.framework", "CoreMedia.framework", @@ -215,6 +183,7 @@ source_set("flutter_framework_source") { } deps = [ + "$dart_src/runtime/bin:elf_loader", ":ios_gpu_configuration", "//flutter/common:common", "//flutter/common/graphics", @@ -222,6 +191,7 @@ source_set("flutter_framework_source") { "//flutter/lib/ui", "//flutter/runtime", "//flutter/shell/common", + "//flutter/shell/common/shorebird", "//flutter/shell/platform/common:common_cpp_input", "//flutter/shell/platform/darwin/common", "//flutter/shell/platform/darwin/common:framework_common", From 0f660eb0c7c668e30d5ec3a854e009fb0e295bc6 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Thu, 13 Feb 2025 15:02:13 -0600 Subject: [PATCH 05/14] chore: format --- engine/src/flutter/shell/platform/darwin/ios/BUILD.gn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn index e7cfcd2b10674..83dd1eee8eb1f 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn @@ -183,8 +183,8 @@ source_set("flutter_framework_source") { } deps = [ - "$dart_src/runtime/bin:elf_loader", ":ios_gpu_configuration", + "$dart_src/runtime/bin:elf_loader", "//flutter/common:common", "//flutter/common/graphics", "//flutter/fml", From 092f73ea46d8edb3314d14007068837719aeafee Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Fri, 14 Feb 2025 10:02:37 -0600 Subject: [PATCH 06/14] chore: roll engine.version to 0f660eb0c7c668e30d5ec3a854e009fb0e295bc6 (3.29.0) --- bin/internal/engine.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/internal/engine.version b/bin/internal/engine.version index 33084ea9f8372..cb3c6265fc9b0 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -f73bfc4522dd0bc87bbcdb4bb3088082755c5e87 +0f660eb0c7c668e30d5ec3a854e009fb0e295bc6 From 4099421bb727e0b8dff1390c9fe12bea90216483 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Fri, 14 Feb 2025 10:24:44 -0600 Subject: [PATCH 07/14] chore: disable update_engine_version logic --- bin/internal/shared.bat | 3 ++- bin/internal/shared.sh | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/internal/shared.bat b/bin/internal/shared.bat index 3fa3a3be2333b..fe411c7e75648 100644 --- a/bin/internal/shared.bat +++ b/bin/internal/shared.bat @@ -82,7 +82,8 @@ GOTO :after_subroutine REM The following IF conditions are all linked with a logical OR. However, REM there is no OR operator in batch and a GOTO construct is used as replacement. - CALL :do_ensure_engine_version + REM Shorebird manages the engine version separately so we disable this logic. + REM CALL :do_ensure_engine_version IF NOT EXIST "%engine_stamp%" GOTO do_sdk_update_and_snapshot SET /P dart_required_version=<"%engine_version_path%" SET /P dart_installed_version=<"%engine_stamp%" diff --git a/bin/internal/shared.sh b/bin/internal/shared.sh index f97a75d7789fd..5f4e5bc8e6dd7 100644 --- a/bin/internal/shared.sh +++ b/bin/internal/shared.sh @@ -113,8 +113,9 @@ function _wait_for_lock () { function upgrade_flutter () ( mkdir -p "$FLUTTER_ROOT/bin/cache" + # Shorebird manages the engine version separately so we disable this logic. # Ensure the engine.version is populated - "$FLUTTER_ROOT/bin/internal/update_engine_version.sh" + # "$FLUTTER_ROOT/bin/internal/update_engine_version.sh" local revision="$(cd "$FLUTTER_ROOT"; git rev-parse HEAD)" local compilekey="$revision:$FLUTTER_TOOL_ARGS" From ac75ff6df8576a68e545685306011788a54b5cd7 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Tue, 18 Feb 2025 10:09:41 -0600 Subject: [PATCH 08/14] chore: roll updater to `ab23721e35d2e740026def44e1469e17e3440c83` (fix rollbacks) --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 8400497671749..09ea8df721450 100644 --- a/DEPS +++ b/DEPS @@ -18,7 +18,7 @@ vars = { "dart_sdk_revision": "d3e1bb5620319bca7ef501f147bb7853d9a415ef", "dart_sdk_git": "git@github.com:shorebirdtech/dart-sdk.git", "updater_git": "https://github.com/shorebirdtech/updater.git", - "updater_rev": "78c84e5bf72266da07df536e98d431782cb39a6d", + "updater_rev": "ab23721e35d2e740026def44e1469e17e3440c83", # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From 2e1bcd18e2dc3b8251dc0671085c6518d37e16ad Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Tue, 18 Feb 2025 11:53:06 -0600 Subject: [PATCH 09/14] chore: roll engine to `ac75ff6df8576a68e545685306011788a54b5cd7` (fix rollbacks) --- bin/internal/engine.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/internal/engine.version b/bin/internal/engine.version index cb3c6265fc9b0..6907ddf37be57 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -0f660eb0c7c668e30d5ec3a854e009fb0e295bc6 +ac75ff6df8576a68e545685306011788a54b5cd7 From f5d3406bbc924ac26f27e2ddef6d7eef5abfebed Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Tue, 25 Feb 2025 15:37:08 -0600 Subject: [PATCH 10/14] chore: roll Dart to `ac7987ebdca1582a18b2426e8a4f561170f5e530` (fix ffi crash) --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 09ea8df721450..4912ca5c54d30 100644 --- a/DEPS +++ b/DEPS @@ -15,7 +15,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'skia_revision': 'e7b8d078851fd505475fe74359e31a421e6968ea', - "dart_sdk_revision": "d3e1bb5620319bca7ef501f147bb7853d9a415ef", + "dart_sdk_revision": "ac7987ebdca1582a18b2426e8a4f561170f5e530", "dart_sdk_git": "git@github.com:shorebirdtech/dart-sdk.git", "updater_git": "https://github.com/shorebirdtech/updater.git", "updater_rev": "ab23721e35d2e740026def44e1469e17e3440c83", From b28d6477577dc5be88eb2ed632cae3526f326dfc Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Tue, 25 Feb 2025 16:44:58 -0600 Subject: [PATCH 11/14] chore: roll engine to `f5d3406bbc924ac26f27e2ddef6d7eef5abfebed` (fix ffi crash) --- bin/internal/engine.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/internal/engine.version b/bin/internal/engine.version index 6907ddf37be57..191741fe5f727 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -ac75ff6df8576a68e545685306011788a54b5cd7 +f5d3406bbc924ac26f27e2ddef6d7eef5abfebed From 67c0a4eff6b3b3544601cc4a06ef10087e2d9fa3 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Thu, 27 Feb 2025 16:27:57 -0600 Subject: [PATCH 12/14] chore: remove unused `create_full_ios_framework.py` (#75) --- .../sky/tools/create_full_ios_framework.py | 300 ------------------ 1 file changed, 300 deletions(-) delete mode 100644 engine/src/flutter/sky/tools/create_full_ios_framework.py diff --git a/engine/src/flutter/sky/tools/create_full_ios_framework.py b/engine/src/flutter/sky/tools/create_full_ios_framework.py deleted file mode 100644 index 2738b8693302f..0000000000000 --- a/engine/src/flutter/sky/tools/create_full_ios_framework.py +++ /dev/null @@ -1,300 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright 2013 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. - -# Generates and zip the ios flutter framework including the architecture -# dependent snapshot. - -import argparse -import os -import platform -import shutil -import subprocess -import sys - -from create_xcframework import create_xcframework # pylint: disable=import-error - -ARCH_SUBPATH = 'mac-arm64' if platform.processor() == 'arm' else 'mac-x64' -DSYMUTIL = os.path.join( - os.path.dirname(__file__), '..', '..', 'buildtools', ARCH_SUBPATH, 'clang', 'bin', 'dsymutil' -) - -buildroot_dir = os.path.abspath(os.path.join(os.path.realpath(__file__), '..', '..', '..', '..')) - - -def main(): - parser = argparse.ArgumentParser( - description=( - 'Creates Flutter.framework, Flutter.xcframework and ' - 'copies architecture-dependent gen_snapshot binaries to output dir' - ) - ) - - parser.add_argument('--dst', type=str, required=True) - parser.add_argument('--clang-dir', type=str, default='clang_x64') - parser.add_argument('--x64-out-dir', type=str) - parser.add_argument('--arm64-out-dir', type=str, required=True) - parser.add_argument('--simulator-x64-out-dir', type=str, required=True) - parser.add_argument('--simulator-arm64-out-dir', type=str, required=False) - parser.add_argument('--strip', action='store_true', default=False) - parser.add_argument('--dsym', action='store_true', default=False) - - args = parser.parse_args() - - dst = (args.dst if os.path.isabs(args.dst) else os.path.join(buildroot_dir, args.dst)) - - arm64_out_dir = ( - args.arm64_out_dir - if os.path.isabs(args.arm64_out_dir) else os.path.join(buildroot_dir, args.arm64_out_dir) - ) - - x64_out_dir = None - if args.x64_out_dir: - x64_out_dir = ( - args.x64_out_dir - if os.path.isabs(args.x64_out_dir) else os.path.join(buildroot_dir, args.x64_out_dir) - ) - - simulator_x64_out_dir = None - if args.simulator_x64_out_dir: - simulator_x64_out_dir = ( - args.simulator_x64_out_dir if os.path.isabs(args.simulator_x64_out_dir) else - os.path.join(buildroot_dir, args.simulator_x64_out_dir) - ) - - framework = os.path.join(dst, 'Flutter.framework') - simulator_framework = os.path.join(dst, 'sim', 'Flutter.framework') - arm64_framework = os.path.join(arm64_out_dir, 'Flutter.framework') - simulator_x64_framework = os.path.join(simulator_x64_out_dir, 'Flutter.framework') - - simulator_arm64_out_dir = None - if args.simulator_arm64_out_dir: - simulator_arm64_out_dir = ( - args.simulator_arm64_out_dir if os.path.isabs(args.simulator_arm64_out_dir) else - os.path.join(buildroot_dir, args.simulator_arm64_out_dir) - ) - - if args.simulator_arm64_out_dir is not None: - simulator_arm64_framework = os.path.join(simulator_arm64_out_dir, 'Flutter.framework') - - if not os.path.isdir(arm64_framework): - print('Cannot find iOS arm64 Framework at %s' % arm64_framework) - return 1 - - if not os.path.isdir(simulator_x64_framework): - print('Cannot find iOS x64 simulator Framework at %s' % simulator_framework) - return 1 - - if not os.path.isfile(DSYMUTIL): - print('Cannot find dsymutil at %s' % DSYMUTIL) - return 1 - - create_framework( - args, dst, framework, arm64_framework, simulator_framework, simulator_x64_framework, - simulator_arm64_framework - ) - - extension_safe_dst = os.path.join(dst, 'extension_safe') - create_extension_safe_framework( - args, extension_safe_dst, '%s_extension_safe' % arm64_out_dir, - '%s_extension_safe' % simulator_x64_out_dir, '%s_extension_safe' % simulator_arm64_out_dir - ) - - generate_gen_snapshot(args, dst, x64_out_dir, arm64_out_dir) - generate_analyze_snapshot(args, dst, x64_out_dir, arm64_out_dir) - zip_archive(dst) - return 0 - -def create_extension_safe_framework( # pylint: disable=too-many-arguments - args, dst, arm64_out_dir, simulator_x64_out_dir, simulator_arm64_out_dir -): - framework = os.path.join(dst, 'Flutter.framework') - simulator_framework = os.path.join(dst, 'sim', 'Flutter.framework') - arm64_framework = os.path.join(arm64_out_dir, 'Flutter.framework') - simulator_x64_framework = os.path.join(simulator_x64_out_dir, 'Flutter.framework') - simulator_arm64_framework = os.path.join(simulator_arm64_out_dir, 'Flutter.framework') - - if not os.path.isdir(arm64_framework): - print('Cannot find extension safe iOS arm64 Framework at %s' % arm64_framework) - return 1 - - if not os.path.isdir(simulator_x64_framework): - print('Cannot find extension safe iOS x64 simulator Framework at %s' % simulator_x64_framework) - return 1 - - create_framework( - args, dst, framework, arm64_framework, simulator_framework, simulator_x64_framework, - simulator_arm64_framework - ) - return 0 - -def create_framework( # pylint: disable=too-many-arguments - args, dst, framework, arm64_framework, simulator_framework, - simulator_x64_framework, simulator_arm64_framework -): - arm64_dylib = os.path.join(arm64_framework, 'Flutter') - simulator_x64_dylib = os.path.join(simulator_x64_framework, 'Flutter') - simulator_arm64_dylib = os.path.join(simulator_arm64_framework, 'Flutter') - if not os.path.isfile(arm64_dylib): - print('Cannot find iOS arm64 dylib at %s' % arm64_dylib) - return 1 - - if not os.path.isfile(simulator_x64_dylib): - print('Cannot find iOS simulator dylib at %s' % simulator_x64_dylib) - return 1 - - # Compute dsym output paths, if enabled. - framework_dsym = None - simulator_dsym = None - if args.dsym: - framework_dsym = framework + '.dSYM' - simulator_dsym = simulator_framework + '.dSYM' - - # Emit the framework for physical devices. - shutil.rmtree(framework, True) - shutil.copytree(arm64_framework, framework) - framework_binary = os.path.join(framework, 'Flutter') - process_framework(args, dst, framework_binary, framework_dsym) - - # Emit the framework for simulators. - if args.simulator_arm64_out_dir is not None: - shutil.rmtree(simulator_framework, True) - shutil.copytree(simulator_arm64_framework, simulator_framework) - - simulator_framework_binary = os.path.join(simulator_framework, 'Flutter') - - # Create the arm64/x64 simulator fat framework. - subprocess.check_call([ - 'lipo', simulator_x64_dylib, simulator_arm64_dylib, '-create', '-output', - simulator_framework_binary - ]) - process_framework(args, dst, simulator_framework_binary, simulator_dsym) - else: - simulator_framework = simulator_x64_framework - - # Create XCFramework from the arm-only fat framework and the arm64/x64 - # simulator frameworks, or just the x64 simulator framework if only that one - # exists. - xcframeworks = [simulator_framework, framework] - dsyms = [simulator_dsym, framework_dsym] if args.dsym else None - create_xcframework(location=dst, name='Flutter', frameworks=xcframeworks, dsyms=dsyms) - - # Add the x64 simulator into the fat framework. - subprocess.check_call([ - 'lipo', arm64_dylib, simulator_x64_dylib, '-create', '-output', framework_binary - ]) - - process_framework(args, dst, framework_binary, framework_dsym) - return 0 - - -def embed_codesign_configuration(config_path, contents): - with open(config_path, 'w') as file: - file.write('\n'.join(contents) + '\n') - - -def zip_archive(dst): - ios_file_with_entitlements = ['gen_snapshot_arm64'] - ios_file_without_entitlements = [ - 'Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', - 'Flutter.xcframework/ios-arm64/dSYMs/Flutter.framework.dSYM/Contents/Resources/DWARF/Flutter', - 'Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', - 'Flutter.xcframework/ios-arm64_x86_64-simulator/dSYMs/Flutter.framework.dSYM/Contents/Resources/DWARF/Flutter', # pylint: disable=line-too-long - 'extension_safe/Flutter.xcframework/ios-arm64/Flutter.framework/Flutter', - 'extension_safe/Flutter.xcframework/ios-arm64/dSYMs/Flutter.framework.dSYM/Contents/Resources/DWARF/Flutter', # pylint: disable=line-too-long - 'extension_safe/Flutter.xcframework/ios-arm64_x86_64-simulator/Flutter.framework/Flutter', - 'extension_safe/Flutter.xcframework/ios-arm64_x86_64-simulator/dSYMs/Flutter.framework.dSYM/Contents/Resources/DWARF/Flutter' # pylint: disable=line-too-long - ] - embed_codesign_configuration(os.path.join(dst, 'entitlements.txt'), ios_file_with_entitlements) - - embed_codesign_configuration( - os.path.join(dst, 'without_entitlements.txt'), ios_file_without_entitlements - ) - - subprocess.check_call([ - 'zip', - '-r', - 'artifacts.zip', - 'analyze_snapshot_arm64', - 'gen_snapshot_arm64', - 'Flutter.xcframework', - 'entitlements.txt', - 'without_entitlements.txt', - 'extension_safe/Flutter.xcframework', - ], - cwd=dst) - - # Generate Flutter.dSYM.zip for manual symbolification. - # - # Historically, the framework dSYM was named Flutter.dSYM, so in order to - # remain backward-compatible with existing instructions in docs/Crashes.md - # and existing tooling such as dart-lang/dart_ci, we rename back to that name - # - # TODO(cbracken): remove these archives and the upload steps once we bundle - # dSYMs in app archives. https://github.com/flutter/flutter/issues/116493 - framework_dsym = os.path.join(dst, 'Flutter.framework.dSYM') - if os.path.exists(framework_dsym): - renamed_dsym = framework_dsym.replace('Flutter.framework.dSYM', 'Flutter.dSYM') - os.rename(framework_dsym, renamed_dsym) - subprocess.check_call(['zip', '-r', 'Flutter.dSYM.zip', 'Flutter.dSYM'], cwd=dst) - - extension_safe_dsym = os.path.join(dst, 'extension_safe', 'Flutter.framework.dSYM') - if os.path.exists(extension_safe_dsym): - renamed_dsym = extension_safe_dsym.replace('Flutter.framework.dSYM', 'Flutter.dSYM') - os.rename(extension_safe_dsym, renamed_dsym) - subprocess.check_call(['zip', '-r', 'extension_safe_Flutter.dSYM.zip', 'Flutter.dSYM'], cwd=dst) - - -def process_framework(args, dst, framework_binary, dsym): - if dsym: - subprocess.check_call([DSYMUTIL, '-o', dsym, framework_binary]) - - if args.strip: - # copy unstripped - unstripped_out = os.path.join(dst, 'Flutter.unstripped') - shutil.copyfile(framework_binary, unstripped_out) - subprocess.check_call(['strip', '-x', '-S', framework_binary]) - - -def generate_gen_snapshot(args, dst, x64_out_dir, arm64_out_dir): - if x64_out_dir: - _generate_gen_snapshot(x64_out_dir, os.path.join(dst, 'gen_snapshot_x64')) - - if arm64_out_dir: - _generate_gen_snapshot( - os.path.join(arm64_out_dir, args.clang_dir), os.path.join(dst, 'gen_snapshot_arm64') - ) - - -def _generate_gen_snapshot(directory, destination): - gen_snapshot_dir = os.path.join(directory, 'gen_snapshot') - if not os.path.isfile(gen_snapshot_dir): - print('Cannot find gen_snapshot at %s' % gen_snapshot_dir) - sys.exit(1) - - subprocess.check_call(['xcrun', 'bitcode_strip', '-r', gen_snapshot_dir, '-o', destination]) - - -def generate_analyze_snapshot(args, dst, x64_out_dir, arm64_out_dir): - if x64_out_dir: - _generate_analyze_snapshot(x64_out_dir, os.path.join(dst, 'analyze_snapshot_x64')) - - if arm64_out_dir: - _generate_analyze_snapshot( - os.path.join(arm64_out_dir, args.clang_dir), os.path.join(dst, 'analyze_snapshot_arm64') - ) - - -def _generate_analyze_snapshot(directory, destination): - analyze_snapshot_dir = os.path.join(directory, 'analyze_snapshot') - if not os.path.isfile(analyze_snapshot_dir): - print('Cannot find analyze_snapshot at %s' % analyze_snapshot_dir) - sys.exit(1) - - subprocess.check_call(['xcrun', 'bitcode_strip', '-r', analyze_snapshot_dir, '-o', destination]) - - -if __name__ == '__main__': - sys.exit(main()) From bfa145af1dea624d0568c44e0debb08de888cb3a Mon Sep 17 00:00:00 2001 From: Eric Seidel Date: Thu, 27 Feb 2025 14:36:34 -0800 Subject: [PATCH 13/14] chore: revert spurious changes --- packages/flutter_tools/lib/src/ios/mac.dart | 4 +--- packages/flutter_tools/lib/src/windows/build_windows.dart | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 03a560812527e..dba3560a09c39 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:io'; import 'package:meta/meta.dart'; import 'package:process/process.dart'; @@ -11,6 +10,7 @@ import 'package:unified_analytics/unified_analytics.dart'; import '../artifacts.dart'; import '../base/file_system.dart'; +import '../base/io.dart'; import '../base/logger.dart'; import '../base/process.dart'; import '../base/project_migrator.dart'; @@ -32,7 +32,6 @@ import '../migrations/xcode_script_build_phase_migration.dart'; import '../migrations/xcode_thin_binary_build_phase_input_paths_migration.dart'; import '../plugins.dart'; import '../project.dart'; -import '../reporting/reporting.dart'; import 'application_package.dart'; import 'code_signing.dart'; import 'migrations/host_app_info_plist_migration.dart'; @@ -570,7 +569,6 @@ Future buildXcodeProject({ globals.printError('Archive succeeded but the expected xcarchive at $outputDir not found'); } } - return XcodeBuildResult( success: true, output: outputDir, diff --git a/packages/flutter_tools/lib/src/windows/build_windows.dart b/packages/flutter_tools/lib/src/windows/build_windows.dart index 74be94dc5856a..dd415628e9806 100644 --- a/packages/flutter_tools/lib/src/windows/build_windows.dart +++ b/packages/flutter_tools/lib/src/windows/build_windows.dart @@ -21,7 +21,6 @@ import '../flutter_plugins.dart'; import '../globals.dart' as globals; import '../migrations/cmake_custom_command_migration.dart'; import '../migrations/cmake_native_assets_migration.dart'; -import '../shorebird/shorebird_yaml.dart'; import 'migrations/build_architecture_migration.dart'; import 'migrations/show_window_migration.dart'; import 'migrations/version_migration.dart'; From bdc9f96d46385ef289a68491081d47ba3c9c7579 Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Thu, 27 Feb 2025 17:24:17 -0600 Subject: [PATCH 14/14] fix: shorebird android tests (#77) --- .github/workflows/shorebird_ci.yml | 5 ++++- .../shorebird_tests/test/shorebird_tests.dart | 20 +++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/shorebird_ci.yml b/.github/workflows/shorebird_ci.yml index 69397c4cad0d6..ec147b4489255 100644 --- a/.github/workflows/shorebird_ci.yml +++ b/.github/workflows/shorebird_ci.yml @@ -45,7 +45,7 @@ jobs: - name: ๐Ÿฆ Run Flutter Tools Tests # TODO(eseidel): Find a nice way to run this on windows. - if: matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' + if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' }} # TODO(eseidel): We can't run all flutter_tools tests until we make # our changes not throw exceptions on missing shorebird.yaml. # https://github.com/shorebirdtech/shorebird/issues/2392 @@ -53,5 +53,8 @@ jobs: working-directory: packages/flutter_tools - name: ๐Ÿฆ Run Shorebird Tests + # TODO(felangel): These tests have a dependency on pkg:flutter_flavorizr which + # requires XCode -- therefore they don't work on Windows. + if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' }} run: dart test working-directory: packages/shorebird_tests diff --git a/packages/shorebird_tests/test/shorebird_tests.dart b/packages/shorebird_tests/test/shorebird_tests.dart index 847b319e6e6df..4bead2de4503e 100644 --- a/packages/shorebird_tests/test/shorebird_tests.dart +++ b/packages/shorebird_tests/test/shorebird_tests.dart @@ -110,15 +110,23 @@ extension ShorebirdProjectDirectoryOnDirectory on Directory { path.join(this.path, 'android', 'app', 'build.gradle'), ); - Future addPubDependency(String name, {bool dev = false}) { - return _runFlutterCommand( + Future addPubDependency(String name, {bool dev = false}) async { + final result = await _runFlutterCommand( ['pub', 'add', if (dev) '--dev', name], workingDirectory: this, ); + if (result.exitCode != 0) { + throw Exception( + 'Failed to run `flutter pub add $name`: ${result.stderr}'); + } } Future addProjectFlavors() async { - await addPubDependency('flutter_flavorizr', dev: true); + await addPubDependency( + // TODO(felangel): revert to using published version once 3.29.0 support is released. + // https://github.com/AngeloAvv/flutter_flavorizr/pull/291 + 'dev:flutter_flavorizr:{"git":{"url":"https://github.com/wjlee611/flutter_flavorizr.git","ref":"chore/temp-migrate-3-29","path":"."}}', + ); await File( path.join( @@ -153,10 +161,14 @@ flavors: bundleId: "com.example.shorebird_test.global" '''); - await _runFlutterCommand( + final result = await _runFlutterCommand( ['pub', 'run', 'flutter_flavorizr'], workingDirectory: this, ); + if (result.exitCode != 0) { + throw Exception( + 'Failed to run `flutter pub run flutter_flavorizr`: ${result.stderr}'); + } } void addShorebirdFlavors() {