diff --git a/DEPS b/DEPS index acb41ff28fa7a..02939d351f16c 100644 --- a/DEPS +++ b/DEPS @@ -56,7 +56,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': 'd70b123c77d4115f332652c9bb21ac46a2456889', + 'dart_revision': '05fa9dd6af23dd3f74fd402f8ba917fe7180112a', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py diff --git a/dev/icon_treeshaker/.gitignore b/dev/icon_treeshaker/.gitignore new file mode 100644 index 0000000000000..3cceda5578968 --- /dev/null +++ b/dev/icon_treeshaker/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/dev/icon_treeshaker/CHANGELOG.md b/dev/icon_treeshaker/CHANGELOG.md new file mode 100644 index 0000000000000..effe43c82c8a7 --- /dev/null +++ b/dev/icon_treeshaker/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/dev/icon_treeshaker/README.md b/dev/icon_treeshaker/README.md new file mode 100644 index 0000000000000..8831761b897a1 --- /dev/null +++ b/dev/icon_treeshaker/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/dev/icon_treeshaker/analysis_options.yaml b/dev/icon_treeshaker/analysis_options.yaml new file mode 100644 index 0000000000000..dee8927aafeb5 --- /dev/null +++ b/dev/icon_treeshaker/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/dev/icon_treeshaker/example/icon_treeshaker_example.dart b/dev/icon_treeshaker/example/icon_treeshaker_example.dart new file mode 100644 index 0000000000000..3c909885a7914 --- /dev/null +++ b/dev/icon_treeshaker/example/icon_treeshaker_example.dart @@ -0,0 +1,6 @@ +import 'package:icon_treeshaker/icon_treeshaker.dart'; + +void main() { + var awesome = Awesome(); + print('awesome: ${awesome.isAwesome}'); +} diff --git a/dev/icon_treeshaker/example_flutter/.gitignore b/dev/icon_treeshaker/example_flutter/.gitignore new file mode 100644 index 0000000000000..79c113f9b5017 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/dev/icon_treeshaker/example_flutter/.metadata b/dev/icon_treeshaker/example_flutter/.metadata new file mode 100644 index 0000000000000..dac569ea96e9a --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "4048d66cc5089164c175f64d45e82a81d8aa43f9" + channel: "[user-branch]" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 4048d66cc5089164c175f64d45e82a81d8aa43f9 + base_revision: 4048d66cc5089164c175f64d45e82a81d8aa43f9 + - platform: android + create_revision: 4048d66cc5089164c175f64d45e82a81d8aa43f9 + base_revision: 4048d66cc5089164c175f64d45e82a81d8aa43f9 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/dev/icon_treeshaker/example_flutter/README.md b/dev/icon_treeshaker/example_flutter/README.md new file mode 100644 index 0000000000000..9b7df9da3cd6f --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/README.md @@ -0,0 +1,3 @@ +# example_flutter + +A new Flutter project. diff --git a/dev/icon_treeshaker/example_flutter/analysis_options.yaml b/dev/icon_treeshaker/example_flutter/analysis_options.yaml new file mode 100644 index 0000000000000..f9b303465f19b --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/dev/icon_treeshaker/example_flutter/android/.gitignore b/dev/icon_treeshaker/example_flutter/android/.gitignore new file mode 100644 index 0000000000000..be3943c96d8ee --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/dev/icon_treeshaker/example_flutter/android/app/build.gradle.kts b/dev/icon_treeshaker/example_flutter/android/app/build.gradle.kts new file mode 100644 index 0000000000000..e302716d68682 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.example_flutter" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.example_flutter" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/dev/icon_treeshaker/example_flutter/android/app/src/debug/AndroidManifest.xml b/dev/icon_treeshaker/example_flutter/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000000000..399f6981d5d35 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/dev/icon_treeshaker/example_flutter/android/app/src/main/AndroidManifest.xml b/dev/icon_treeshaker/example_flutter/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000000..d5d47c0312691 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/icon_treeshaker/example_flutter/android/app/src/main/kotlin/com/example/example_flutter/MainActivity.kt b/dev/icon_treeshaker/example_flutter/android/app/src/main/kotlin/com/example/example_flutter/MainActivity.kt new file mode 100644 index 0000000000000..c4cea59baea63 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/android/app/src/main/kotlin/com/example/example_flutter/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.example_flutter + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/dev/icon_treeshaker/example_flutter/android/app/src/main/res/drawable-v21/launch_background.xml b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000000000..f74085f3f6a2b --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/dev/icon_treeshaker/example_flutter/android/app/src/main/res/drawable/launch_background.xml b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000000000..304732f884201 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/dev/icon_treeshaker/example_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000..db77bb4b7b090 Binary files /dev/null and b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/dev/icon_treeshaker/example_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000..17987b79bb8a3 Binary files /dev/null and b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/dev/icon_treeshaker/example_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000..09d4391482be6 Binary files /dev/null and b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/dev/icon_treeshaker/example_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000..d5f1c8d34e7a8 Binary files /dev/null and b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/dev/icon_treeshaker/example_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000..4d6372eebdb28 Binary files /dev/null and b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/dev/icon_treeshaker/example_flutter/android/app/src/main/res/values-night/styles.xml b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000000000..06952be745f9f --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/dev/icon_treeshaker/example_flutter/android/app/src/main/res/values/styles.xml b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000000..cb1ef88056edd --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/dev/icon_treeshaker/example_flutter/android/app/src/profile/AndroidManifest.xml b/dev/icon_treeshaker/example_flutter/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000000000..399f6981d5d35 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/dev/icon_treeshaker/example_flutter/android/build.gradle.kts b/dev/icon_treeshaker/example_flutter/android/build.gradle.kts new file mode 100644 index 0000000000000..89176ef44e8c7 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/dev/icon_treeshaker/example_flutter/android/gradle.properties b/dev/icon_treeshaker/example_flutter/android/gradle.properties new file mode 100644 index 0000000000000..f018a61817f55 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/dev/icon_treeshaker/example_flutter/android/gradle/wrapper/gradle-wrapper.properties b/dev/icon_treeshaker/example_flutter/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000000..ac3b47926ee56 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/dev/icon_treeshaker/example_flutter/android/settings.gradle.kts b/dev/icon_treeshaker/example_flutter/android/settings.gradle.kts new file mode 100644 index 0000000000000..ab39a10a29baf --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/android/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/dev/icon_treeshaker/example_flutter/fonts/TwoIcons.ttf b/dev/icon_treeshaker/example_flutter/fonts/TwoIcons.ttf new file mode 100644 index 0000000000000..788e667c17220 Binary files /dev/null and b/dev/icon_treeshaker/example_flutter/fonts/TwoIcons.ttf differ diff --git a/dev/icon_treeshaker/example_flutter/hook/build.dart b/dev/icon_treeshaker/example_flutter/hook/build.dart new file mode 100644 index 0000000000000..652ebf2d4c171 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/hook/build.dart @@ -0,0 +1,14 @@ +import 'package:icon_treeshaker/hook_helper.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +void main(List arguments) { + build(arguments, (input, output) async { + output.assets.font.add( + FontAsset( + family: 'TwoIcons', + file: input.packageRoot.resolve('fonts/TwoIcons.ttf'), + package: input.packageName, + ), + ); + }); +} diff --git a/dev/icon_treeshaker/example_flutter/lib/main.dart b/dev/icon_treeshaker/example_flutter/lib/main.dart new file mode 100644 index 0000000000000..bf5ed1ef5d197 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/lib/main.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: Scaffold( + body: Center( + child: Column(children: [Icon(Icons.abc), Icon(TwoIcons.acUnit)]), + ), + ), + ); + } +} + +@staticIconProvider +sealed class TwoIcons { + static const IconData acUnit = IconData(0xe800, fontFamily: 'TwoIcons'); + static const IconData whatshot = IconData(0xe801, fontFamily: 'TwoIcons'); +} diff --git a/dev/icon_treeshaker/example_flutter/linux/.gitignore b/dev/icon_treeshaker/example_flutter/linux/.gitignore new file mode 100644 index 0000000000000..d3896c98444fb --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/dev/icon_treeshaker/example_flutter/linux/CMakeLists.txt b/dev/icon_treeshaker/example_flutter/linux/CMakeLists.txt new file mode 100644 index 0000000000000..9d187535bdb72 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example_flutter") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.example_flutter") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/dev/icon_treeshaker/example_flutter/linux/flutter/CMakeLists.txt b/dev/icon_treeshaker/example_flutter/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000000000..d5bd01648a96d --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/dev/icon_treeshaker/example_flutter/linux/runner/CMakeLists.txt b/dev/icon_treeshaker/example_flutter/linux/runner/CMakeLists.txt new file mode 100644 index 0000000000000..e97dabc7028e1 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/dev/icon_treeshaker/example_flutter/linux/runner/main.cc b/dev/icon_treeshaker/example_flutter/linux/runner/main.cc new file mode 100644 index 0000000000000..e7c5c54370372 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/dev/icon_treeshaker/example_flutter/linux/runner/my_application.cc b/dev/icon_treeshaker/example_flutter/linux/runner/my_application.cc new file mode 100644 index 0000000000000..ada38713ef335 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/linux/runner/my_application.cc @@ -0,0 +1,130 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example_flutter"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example_flutter"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/dev/icon_treeshaker/example_flutter/linux/runner/my_application.h b/dev/icon_treeshaker/example_flutter/linux/runner/my_application.h new file mode 100644 index 0000000000000..72271d5e41701 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/dev/icon_treeshaker/example_flutter/pubspec.yaml b/dev/icon_treeshaker/example_flutter/pubspec.yaml new file mode 100644 index 0000000000000..e81cadd989640 --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/pubspec.yaml @@ -0,0 +1,27 @@ +name: example_flutter +description: "A new Flutter project." +publish_to: 'none' +version: 0.1.0 + +environment: + sdk: ^3.8.0-133.0.dev + +dependencies: + flutter: + sdk: flutter + native_assets_cli: ^0.11.0 + icon_treeshaker: + path: ../ + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: + uses-material-design: true + + fonts: + - family: TwoIcons + fonts: + - asset: fonts/TwoIcons.ttf diff --git a/dev/icon_treeshaker/example_flutter/test/widget_test.dart b/dev/icon_treeshaker/example_flutter/test/widget_test.dart new file mode 100644 index 0000000000000..a95133eed5ffa --- /dev/null +++ b/dev/icon_treeshaker/example_flutter/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:example_flutter/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/dev/icon_treeshaker/hook/link.dart b/dev/icon_treeshaker/hook/link.dart new file mode 100644 index 0000000000000..37ff27b3e8fc4 --- /dev/null +++ b/dev/icon_treeshaker/hook/link.dart @@ -0,0 +1,136 @@ +import 'dart:convert' show jsonDecode; +import 'dart:io' as io; + +import 'package:collection/collection.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:icon_treeshaker/flutter_config.dart'; +import 'package:icon_treeshaker/hook_helper.dart'; +import 'package:icon_treeshaker/icon_treeshaker.dart'; +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/data_assets.dart'; +import 'package:native_assets_cli/src/config.dart'; +import 'package:path/path.dart' as p; +import 'package:record_use/record_use.dart' as record_use; + +Future main(List arguments) async => await link(arguments, ( + input, + output, +) async { + final logger = Logger('link'); + final fontAssets = input.assets.fonts.toList(); + FileSystem fs = LocalFileSystem(); + var usages = input.usages; + //TODO(mosum): Get path to subset from flutter + final fontSubset = input.config.flutter.fontSubsetBinary; + if (fontSubset == null) { + throw ArgumentError( + 'The font subset binary was not provided by $input - icon tree shaking is not possible', + ); + } + logger.info('Getting tree-shaker data for icons.'); + final Map pathToTreeshakeData = getIconData( + usages, + fontAssets, + ); + for (final fontAsset in fontAssets) { + logger.info('Checking if $fontAsset should be tree-shaken.'); + final IconTreeShakerData? iconData = pathToTreeshakeData[fontAsset]; + if (iconData == null) { + logger.info('No IconData usages of $fontAsset found.'); + continue; + } + final fontFile = fs.file(fontAsset.file); + logger.info('Running $fontSubset on $fontFile.'); + var pathOfSubsetFont = input.outputDirectoryShared.resolve( + //TODO(mosum): Handle duplicates + p.basename(fontAsset.file.path), + ); + final success = await subsetFont( + fontFile: fontFile, + outputPath: pathOfSubsetFont.toFilePath(), + iconData: iconData, + fs: fs, + pathToSubsetBinary: fontSubset, + logger: logger, + ); + output.assets.font.add(fontAsset.copyWith(file: pathOfSubsetFont)); + if (!success) { + throw io.ProcessException( + fontSubset, + [], + 'Failure when trying to treeshake $fontAsset. Check the log for more details.', + ); + } + logger.info('Success, shrunk $fontFile!.'); + } +}); + +/// The key is the family, or might be prefixed +/// [result[familyKey] = asset;](https://github.com/flutter/flutter/blob/08b1bdecd6a632e7b30901efaaf19d0f759bd0f5/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart#L278) +/// +/// The [ids] should +Map getIconData( + record_use.RecordedUsages usages, + List fonts, +) { + final groupedInstances = (usages.instancesOf( + record_use.Identifier( + importUri: 'package:flutter/widgets.dart', + name: 'IconData', + ), + ) ?? + []) + .map((e) => e.instanceConstant) + .map((e) => e.fields) + .map( + (e) => ( + codePoint: (e['codePoint'] as record_use.IntConstant).value, + family: (e['fontFamily'] as record_use.StringConstant?)?.value, + fontPackage: (e['fontPackage'] as record_use.StringConstant?)?.value, + matchTextDirection: + (e['matchTextDirection'] as record_use.StringConstant?)?.value, + ), + ) + .groupListsBy( + (instance) => (family: instance.family, package: instance.fontPackage), + ); + final map = {}; + for (final MapEntry(key: key, value: instances) in groupedInstances.entries) { + var font = fonts.firstWhereOrNull( + (font) => font.family == key.family && font.package == key.package, + ); + if (font != null) { + // Add space as an optional code point, as web uses it to measure the font height. + //TODO(mosum): add web as OS, and pass it to the data config (TBC) + bool isWeb = 1 == 0; + const int kSpacePoint = 32; + final List optionalCodePoints = isWeb ? [kSpacePoint] : []; + map[font] = IconTreeShakerData( + family: font.family, + codePoints: instances.map((instance) => instance.codePoint).toList(), + optionalCodePoints: optionalCodePoints, + ); + } + } + + // As in https://github.com/flutter/flutter/blob/08b1bdecd6a632e7b30901efaaf19d0f759bd0f5/packages/flutter_tools/lib/src/build_system/targets/icon_tree_shaker.dart#L358 + // Use 'packages/$package/$family' as keys + + return map; +} + +extension on LinkInput { + record_use.RecordedUsages get usages { + final usagesFile = recordedUsagesFile; + final usagesContent = io.File.fromUri(usagesFile!).readAsStringSync(); + final usagesJson = jsonDecode(usagesContent) as Map; + return record_use.RecordedUsages.fromJson(usagesJson); + } +} + +extension on LinkInputAssets { + Iterable get fonts => encodedAssets + .where((e) => e.type == FontAsset.type) + .map(FontAsset.fromEncoded); +} diff --git a/dev/icon_treeshaker/lib/flutter_config.dart b/dev/icon_treeshaker/lib/flutter_config.dart new file mode 100644 index 0000000000000..0f02e2bc19dfd --- /dev/null +++ b/dev/icon_treeshaker/lib/flutter_config.dart @@ -0,0 +1,71 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. 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:native_assets_cli/native_assets_cli_builder.dart'; + +/// Extension to the [HookConfig] providing access to configuration specific +/// to code assets (only available if code assets are supported). +extension FlutterHookConfig on HookConfig { + /// Code asset specific configuration. + FlutterConfig get flutter => FlutterConfig.fromJson(json); +} + +/// Configuration for hook writers if code assets are supported. +class FlutterConfig { + factory FlutterConfig.fromJson(Map json) { + return FlutterConfig._( + fontSubsetBinary: json.getNested([ + _configKey, + _flutterKey, + _fontSubsetBinaryKey, + ]), + ); + } + // Should not be made public, class will be replaced as a view on `json`. + FlutterConfig._({String? fontSubsetBinary}) + : _fontSubsetBinary = fontSubsetBinary; + + final String? _fontSubsetBinary; + + String? get fontSubsetBinary => _fontSubsetBinary; +} + +/// Extension to initialize code specific configuration on link/build inputs. +extension CodeAssetBuildInputBuilder on HookConfigBuilder { + void setupFlutter({String? fontSubsetBinary}) { + json.setNested([ + _configKey, + _flutterKey, + _fontSubsetBinaryKey, + ], fontSubsetBinary); + } +} + +const String _configKey = 'config'; +const String _flutterKey = 'flutter'; +const String _fontSubsetBinaryKey = 'fontSubsetBinary'; + +extension MapJsonUtils on Map { + void setNested(List nestedMapKeys, Object? value) { + Map map = this; + for (final String key in nestedMapKeys.sublist( + 0, + nestedMapKeys.length - 1, + )) { + map = (map[key] ??= {}) as Map; + } + map[nestedMapKeys.last] = value; + } + + T? getNested(List nestedMapKeys) { + Map map = this; + for (final String key in nestedMapKeys.sublist( + 0, + nestedMapKeys.length - 1, + )) { + map = (map[key] ??= {}) as Map; + } + return map[nestedMapKeys.last] as T?; + } +} diff --git a/dev/icon_treeshaker/lib/hook_helper.dart b/dev/icon_treeshaker/lib/hook_helper.dart new file mode 100644 index 0000000000000..607bdb810659e --- /dev/null +++ b/dev/icon_treeshaker/lib/hook_helper.dart @@ -0,0 +1,146 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. 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:collection'; + +import 'package:native_assets_cli/data_assets.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +/// Data bundled with a Dart or Flutter application. +/// +/// A data asset is accessible in a Dart or Flutter application. To retrieve an +/// asset at runtime, the [id] is used. This enables access to the asset +/// irrespective of how and where the application is run. +/// +/// An data asset must provide a [DataAsset.file]. The Dart and Flutter SDK will +/// bundle this code in the final application. +final class FontAsset { + final String family; + + /// https://api.flutter.dev/flutter/dart-ui/FontStyle.html values - italic or normal + final String? style; + + /// integer multiple of 100, between 100 and 900 + final int? weight; + + /// relative path in the package + final Uri file; + + /// The package which contains this asset. + final String package; + + /// The identifier for this data asset. + /// TODO(mosum): don't use family to allow multiple fonts per family + String get id => 'package:$package/$family'; + + /// Constructs a [DataAsset] from an [EncodedAsset]. + factory FontAsset.fromEncoded(EncodedAsset asset) { + assert(asset.type == DataAsset.type); + final jsonMap = asset.encoding; + return FontAsset( + family: jsonMap[_familyKey] as String, + style: jsonMap[_styleKey] as String?, + weight: jsonMap[_weightKey] as int?, + package: jsonMap[_packageKey] as String, + file: Uri.file(jsonMap[_fileKey] as String), + ); + } + + @override + bool operator ==(Object other) { + if (other is! FontAsset) { + return false; + } + return other.package == package && + other.file == file && + other.family == family && + other.style == style && + other.weight == weight; + } + + @override + int get hashCode => Object.hash(package, file, family, style, weight); + + EncodedAsset encode() => EncodedAsset( + FontAsset.type, + SplayTreeMap.from({ + _packageKey: package, + _fileKey: file.toFilePath(), + _familyKey: family, + if (style != null) _styleKey: style, + if (weight != null) _weightKey: weight, + }), + ); + + @override + String toString() => 'FontAsset(${encode().encoding})'; + + static const String type = 'font'; + + FontAsset({ + required this.file, + required this.package, + required this.family, + this.style, + this.weight, + }); + + FontAsset copyWith({ + String? family, + String? style, + int? weight, + Uri? file, + String? package, + }) => FontAsset( + family: family ?? this.family, + style: style ?? this.style, + weight: weight ?? this.weight, + file: file ?? this.file, + package: package ?? this.package, + ); +} + +const _familyKey = 'family'; +const _styleKey = 'style'; +const _weightKey = 'weight'; +const _packageKey = 'package'; +const _fileKey = 'file'; + +/// Extension to the [LinkOutputBuilder] providing access to emitting data +/// assets (only available if data assets are supported). +extension FontAssetLinkOutputBuilder on EncodedAssetLinkOutputBuilder { + /// Provides access to emitting data assets. + FontAssetLinkOutputBuilderAdd get font => FontAssetLinkOutputBuilderAdd(this); +} + +/// Extension on [LinkOutputBuilder] to emit data assets. +extension type FontAssetLinkOutputBuilderAdd( + EncodedAssetLinkOutputBuilder _output +) { + /// Adds the given [asset] to the link hook output. + void add(FontAsset asset) => _output.addEncodedAsset(asset.encode()); + + /// Adds the given [assets] to the link hook output. + void addAll(Iterable assets) => assets.forEach(add); +} + +/// Extension to the [LinkOutputBuilder] providing access to emitting data +/// assets (only available if data assets are supported). +extension FontAssetBuildOutputBuilder on EncodedAssetBuildOutputBuilder { + /// Provides access to emitting data assets. + FontAssetBuildOutputBuilderAdd get font => + FontAssetBuildOutputBuilderAdd(this); +} + +/// Extension on [LinkOutputBuilder] to emit data assets. +extension type FontAssetBuildOutputBuilderAdd( + EncodedAssetBuildOutputBuilder _output +) { + /// Adds the given [asset] to the link hook output. + void add(FontAsset asset) => + _output.addEncodedAsset(asset.encode(), linkInPackage: 'icon_treeshaker'); + + /// Adds the given [assets] to the link hook output. + void addAll(Iterable assets) => assets.forEach(add); +} diff --git a/dev/icon_treeshaker/lib/icon_treeshaker.dart b/dev/icon_treeshaker/lib/icon_treeshaker.dart new file mode 100644 index 0000000000000..6aac066d47d7b --- /dev/null +++ b/dev/icon_treeshaker/lib/icon_treeshaker.dart @@ -0,0 +1,8 @@ +/// Support for doing something awesome. +/// +/// More dartdocs go here. +library; + +export 'src/icon_treeshaker_base.dart'; + +// TODO: Export any libraries intended for clients of this package. diff --git a/dev/icon_treeshaker/lib/src/icon_treeshaker_base.dart b/dev/icon_treeshaker/lib/src/icon_treeshaker_base.dart new file mode 100644 index 0000000000000..12091caf9a5b8 --- /dev/null +++ b/dev/icon_treeshaker/lib/src/icon_treeshaker_base.dart @@ -0,0 +1,140 @@ +// TODO: Put public facing types in this file. + +import 'dart:convert' show utf8; +import 'dart:io' show Process; + +import 'package:file/file.dart' show File, FileSystem; +import 'package:logging/logging.dart' show Logger; +import 'package:mime/mime.dart' as mime; + +/// Checks if you are awesome. Spoiler: you are. +class Awesome { + bool get isAwesome => true; +} + +/// The MIME types for supported font sets. +const Set kTtfMimeTypes = { + 'font/ttf', // based on internet search + 'font/opentype', + 'font/otf', + 'application/x-font-opentype', + 'application/x-font-otf', + 'application/x-font-ttf', // based on running locally. +}; + +/// Calls font-subset, which transforms the [fontFile] font file to a +/// subsetted version at [outputPath]. +/// +/// If [enabled] is false, or the relative path is not recognized as an icon +/// font used in the Flutter application, this returns false. +/// If the font-subset subprocess fails, it will [throwToolExit]. +/// Otherwise, it will return true. +Future subsetFont({ + required File fontFile, + required String outputPath, + required IconTreeShakerData iconData, + required FileSystem fs, + required String pathToSubsetBinary, + required Logger logger, +}) async { + if (fontFile.lengthSync() < 12) { + return false; + } + final String? mimeType = mime.lookupMimeType( + fontFile.path, + headerBytes: await fontFile.openRead(0, 12).first, + ); + if (!kTtfMimeTypes.contains(mimeType)) { + return false; + } + + final File fontSubset = fs.file(pathToSubsetBinary); + + if (!fontSubset.existsSync()) { + throw IconTreeShakerException._( + 'The font-subset utility is missing. Run "flutter doctor".', + ); + } + + final List args = [outputPath, fontFile.path]; + final Iterable requiredCodePointStrings = iconData.codePoints.map( + (int codePoint) => codePoint.toString(), + ); + final Iterable optionalCodePointStrings = iconData.optionalCodePoints + .map((int codePoint) => 'optional:$codePoint'); + final String codePointsString = requiredCodePointStrings + .followedBy(optionalCodePointStrings) + .join(' '); + logger.info( + 'Running font-subset: ${args.join(' ')}, ' + 'using codepoints $codePointsString', + ); + final Process fontSubsetProcess = await Process.start(fontSubset.path, args); + try { + fontSubsetProcess.stdin.write(codePointsString); + await fontSubsetProcess.stdin.flush(); + await fontSubsetProcess.stdin.close(); + } on Exception { + // handled by checking the exit code. + } + + final int code = await fontSubsetProcess.exitCode; + if (code != 0) { + logger.severe(await utf8.decodeStream(fontSubsetProcess.stdout)); + logger.severe(await utf8.decodeStream(fontSubsetProcess.stderr)); + throw IconTreeShakerException._( + 'Font subsetting failed with exit code $code.', + ); + } + logger.info(getSubsetSummaryMessage(fontFile, fs.file(outputPath))); + return true; +} + +/// The font family name, relative path to font file, and list of code points +/// the application is using. +class IconTreeShakerData { + /// All parameters are required. + const IconTreeShakerData({ + required this.family, + required this.codePoints, + required this.optionalCodePoints, + }); + + /// The font family name, e.g. "MaterialIcons". + final String family; + + /// The list of code points for the font. + final List codePoints; + + /// The list of code points to be optionally added, if they exist in the + /// input font. Otherwise, the tool will silently omit them. + final List optionalCodePoints; + + @override + String toString() => 'FontSubsetData($family, $codePoints)'; +} + +class IconTreeShakerException implements Exception { + IconTreeShakerException._(this.message); + + final String message; + + @override + String toString() => + 'IconTreeShakerException: $message\n\n' + 'To disable icon tree shaking, pass --no-tree-shake-icons to the requested ' + 'flutter build command'; +} + +String getSubsetSummaryMessage(File inputFont, File outputFont) { + final String fontName = inputFont.basename; + final double inputSize = inputFont.lengthSync().toDouble(); + final double outputSize = outputFont.lengthSync().toDouble(); + final double reductionBytes = inputSize - outputSize; + final String reductionPercentage = (reductionBytes / inputSize * 100) + .toStringAsFixed(1); + return 'Font asset "$fontName" was tree-shaken, reducing it from ' + '${inputSize.ceil()} to ${outputSize.ceil()} bytes ' + '($reductionPercentage% reduction). Tree-shaking can be disabled ' + 'by providing the --no-tree-shake-icons flag when building your app.'; +} diff --git a/dev/icon_treeshaker/pubspec.yaml b/dev/icon_treeshaker/pubspec.yaml new file mode 100644 index 0000000000000..926606eb07598 --- /dev/null +++ b/dev/icon_treeshaker/pubspec.yaml @@ -0,0 +1,22 @@ +name: icon_treeshaker +description: A starting point for Dart libraries or applications. +version: 1.0.0 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.8.0-133.0.dev + +# Add regular dependencies here. +dependencies: + collection: ^1.19.1 + file: ^7.0.1 + logging: ^1.3.0 + mime: ^2.0.0 + native_assets_cli: ^0.11.0 + path: ^1.9.1 + record_use: ^0.3.0 + # path: ^1.8.0 + +dev_dependencies: + lints: ^5.0.0 + test: ^1.24.0 diff --git a/dev/icon_treeshaker/test/icon_treeshaker_test.dart b/dev/icon_treeshaker/test/icon_treeshaker_test.dart new file mode 100644 index 0000000000000..f703c426ce4d7 --- /dev/null +++ b/dev/icon_treeshaker/test/icon_treeshaker_test.dart @@ -0,0 +1,16 @@ +import 'package:icon_treeshaker/icon_treeshaker.dart'; +import 'package:test/test.dart'; + +void main() { + group('A group of tests', () { + final awesome = Awesome(); + + setUp(() { + // Additional setup goes here. + }); + + test('First Test', () { + expect(awesome.isAwesome, isTrue); + }); + }); +} diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart index 138eb2725a20e..47a5ac7c95803 100644 --- a/packages/flutter_tools/lib/src/asset.dart +++ b/packages/flutter_tools/lib/src/asset.dart @@ -16,6 +16,7 @@ import 'base/logger.dart'; import 'base/platform.dart'; import 'base/utils.dart'; import 'build_info.dart'; +import 'build_system/targets/native_assets.dart'; import 'cache.dart'; import 'convert.dart'; import 'dart/package_map.dart'; @@ -112,6 +113,7 @@ abstract class AssetBundle { /// Returns 0 for success; non-zero for failure. Future build({ + DartHookResult? dartHookResult, String manifestPath = defaultManifestPath, required String packageConfigPath, bool deferredComponentsEnabled = false, @@ -187,6 +189,8 @@ class ManifestAssetBundle implements AssetBundle { DateTime? _lastBuildTimestamp; + DartHookResult? _lastHookResult; + // We assume the main asset is designed for a device pixel ratio of 1.0. static const String _kAssetManifestJsonFilename = 'AssetManifest.json'; static const String _kAssetManifestBinFilename = 'AssetManifest.bin'; @@ -206,13 +210,21 @@ class ManifestAssetBundle implements AssetBundle { @override bool needsBuild({String manifestPath = defaultManifestPath}) { - final DateTime? lastBuildTimestamp = _lastBuildTimestamp; - if (lastBuildTimestamp == null) { + if (!wasBuiltOnce() || + _lastHookResult == null || + // We need to re-run the dart build. + !_lastHookResult!.isUpToDate(_fileSystem) || + // We don't have to re-run the dart build, but some files the dart build + // wants us to bundle have changed contents. + _lastHookResult!.isOutputDirty(_fileSystem)) { return true; } + final DateTime lastBuildTimestamp = _lastBuildTimestamp!; + final FileStat manifestStat = _fileSystem.file(manifestPath).statSync(); - if (manifestStat.type == FileSystemEntityType.notFound) { + if (manifestStat.type == FileSystemEntityType.notFound || + manifestStat.modified.isAfter(lastBuildTimestamp)) { return true; } @@ -221,18 +233,19 @@ class ManifestAssetBundle implements AssetBundle { return true; // directory was deleted. } for (final File file in directory.listSync().whereType()) { - final DateTime dateTime = file.statSync().modified; - if (dateTime.isAfter(lastBuildTimestamp)) { + final DateTime lastModified = file.statSync().modified; + if (lastModified.isAfter(lastBuildTimestamp)) { return true; } } } - return manifestStat.modified.isAfter(lastBuildTimestamp); + return false; } @override Future build({ + DartHookResult? dartHookResult, String manifestPath = defaultManifestPath, FlutterProject? flutterProject, required String packageConfigPath, @@ -255,6 +268,7 @@ class ManifestAssetBundle implements AssetBundle { // hang on hot reload, as the incremental dill files will never be copied to the // device. _lastBuildTimestamp = DateTime.now(); + _lastHookResult = dartHookResult ?? DartHookResult.empty(); if (flutterManifest.isEmpty) { entries[_kAssetManifestJsonFilename] = AssetBundleEntry( DevFSStringContent('{}'), @@ -400,8 +414,39 @@ class ManifestAssetBundle implements AssetBundle { } } + for (final DataAsset dataAsset in dartHookResult?.dataAssets ?? []) { + final Package package = packageConfig[dataAsset.package]!; + final Uri fileUri = dataAsset.file; + + final String baseDir; + final Uri relativeUri; + if (fileUri.isAbsolute) { + final List pathSegments = fileUri.pathSegments; + baseDir = + fileUri.replace(pathSegments: pathSegments.take(pathSegments.length - 1)).toFilePath(); + relativeUri = Uri(pathSegments: [pathSegments.last]); + } else { + baseDir = package.root.toFilePath(); + relativeUri = fileUri; + } + + final _Asset asset = _Asset( + baseDir: baseDir, + relativeUri: relativeUri, + entryUri: Uri.parse('packages/${dataAsset.package}/${dataAsset.name}'), + package: package, + ); + if (assetVariants.containsKey(asset)) { + _logger.printError( + 'Conflicting assets: The asset "$asset" was declared in the pubspec and the hook.', + ); + return 1; + } + assetVariants[asset] = <_Asset>[asset]; + } + // Save the contents of each image, image variant, and font - // asset in entries. + // asset in [entries]. for (final _Asset asset in assetVariants.keys) { final File assetFile = asset.lookupAssetFile(_fileSystem); final List<_Asset> variants = assetVariants[asset]!; diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index ac6ce0e8ff149..dde3d231cfcd0 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -930,20 +930,6 @@ const String kSdkRoot = 'SdkRoot'; /// Whether to enable Dart obfuscation and where to save the symbol map. const String kDartObfuscation = 'DartObfuscation'; -/// Whether to enable Native Assets. -/// -/// If true, native assets are built and the mapping for native assets lookup -/// at runtime is embedded in the kernel file. -/// -/// If false, native assets are not built, and an empty mapping is embedded in -/// the kernel file. Used for targets that trigger kernel builds but -/// are not OS/architecture specific. -/// -/// Supported values are 'true' and 'false'. -/// -/// Defaults to 'true'. -const String kNativeAssets = 'NativeAssets'; - /// An output directory where one or more code-size measurements may be written. const String kCodeSizeDirectory = 'CodeSizeDirectory'; diff --git a/packages/flutter_tools/lib/src/build_system/targets/android.dart b/packages/flutter_tools/lib/src/build_system/targets/android.dart index da89922d3e86e..9fa7039d7303c 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/android.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/android.dart @@ -70,9 +70,11 @@ abstract class AndroidAssetBundle extends Target { .file(isolateSnapshotData) .copySync(outputDirectory.childFile('isolate_snapshot_data').path); } + final DartHookResult dartHookResult = await DartBuild.loadHookResult(environment); final Depfile assetDepfile = await copyAssets( environment, outputDirectory, + dartHookResult: dartHookResult, targetPlatform: TargetPlatform.android, buildMode: buildMode, flavor: environment.defines[kFlavor], @@ -89,7 +91,11 @@ abstract class AndroidAssetBundle extends Target { } @override - List get dependencies => const [KernelSnapshot(), InstallCodeAssets()]; + List get dependencies => const [ + DartBuildForNative(), + KernelSnapshot(), + InstallCodeAssets(), + ]; } /// An implementation of [AndroidAssetBundle] that includes dependencies on vm 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 5072f10ef51fa..9f6eacff9515c 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/assets.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/assets.dart @@ -33,6 +33,7 @@ import 'native_assets.dart'; Future copyAssets( Environment environment, Directory outputDirectory, { + required DartHookResult dartHookResult, Map additionalContent = const {}, required TargetPlatform targetPlatform, required BuildMode buildMode, @@ -49,6 +50,7 @@ Future copyAssets( splitDeferredAssets: buildMode != BuildMode.debug && buildMode != BuildMode.jitRelease, ).createBundle(); final int resultCode = await assetBundle.build( + dartHookResult: dartHookResult, manifestPath: pubspecFile.path, packageConfigPath: findPackageConfigFileOrDefault(environment.projectDir).path, deferredComponentsEnabled: environment.defines[kDeferredComponents] == 'true', @@ -241,7 +243,11 @@ class CopyAssets extends Target { String get name => 'copy_assets'; @override - List get dependencies => const [KernelSnapshot(), InstallCodeAssets()]; + List get dependencies => const [ + DartBuildForNative(), + KernelSnapshot(), + InstallCodeAssets(), + ]; @override List get inputs => const [ @@ -267,9 +273,11 @@ class CopyAssets extends Target { final BuildMode buildMode = BuildMode.fromCliName(buildModeEnvironment); final Directory output = environment.buildDir.childDirectory('flutter_assets'); output.createSync(recursive: true); + final DartHookResult dartHookResult = await DartBuild.loadHookResult(environment); final Depfile depfile = await copyAssets( environment, output, + dartHookResult: dartHookResult, targetPlatform: TargetPlatform.android, buildMode: buildMode, flavor: environment.defines[kFlavor], diff --git a/packages/flutter_tools/lib/src/build_system/targets/common.dart b/packages/flutter_tools/lib/src/build_system/targets/common.dart index b1f60007c23d4..77f0cdd4c38e1 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/common.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/common.dart @@ -84,9 +84,11 @@ class CopyFlutterBundle extends Target { .file(isolateSnapshotData) .copySync(environment.outputDir.childFile('isolate_snapshot_data').path); } + final DartHookResult dartHookResult = await DartBuild.loadHookResult(environment); final Depfile assetDepfile = await copyAssets( environment, environment.outputDir, + dartHookResult: dartHookResult, targetPlatform: TargetPlatform.android, buildMode: buildMode, flavor: flavor, @@ -103,7 +105,11 @@ class CopyFlutterBundle extends Target { } @override - List get dependencies => const [KernelSnapshot(), InstallCodeAssets()]; + List get dependencies => const [ + DartBuildForNative(), + KernelSnapshot(), + InstallCodeAssets(), + ]; } /// Copies the pre-built flutter bundle for release mode. @@ -153,7 +159,7 @@ class KernelSnapshot extends Target { @override List get outputs => const [ Source.pattern('{BUILD_DIR}/${KernelSnapshot.dillName}'), - // TODO(mosuem): Should output resources.json. https://github.com/flutter/flutter/issues/146263 + Source.pattern('{BUILD_DIR}/${KernelSnapshot.recordedUsagesName}'), ]; static const String depfile = 'kernel_snapshot_program.d'; @@ -168,6 +174,7 @@ class KernelSnapshot extends Target { ]; static const String dillName = 'app.dill'; + static const String recordedUsagesName = 'recorded_usages.json'; @override Future build(Environment environment) async { @@ -282,6 +289,7 @@ class KernelSnapshot extends Target { buildDir: environment.buildDir, targetOS: targetOS, checkDartPluginRegistry: environment.generateDartPluginRegistry, + recordedUsages: environment.buildDir.childFile(KernelSnapshot.recordedUsagesName).path, ); if (output == null || output.errorCount != 0) { throw Exception(); 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 37331f279a8d8..fc983e0a94931 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -443,7 +443,11 @@ abstract class IosAssetBundle extends Target { const IosAssetBundle(); @override - List get dependencies => const [KernelSnapshot(), InstallCodeAssets()]; + List get dependencies => const [ + DartBuildForNative(), + KernelSnapshot(), + InstallCodeAssets(), + ]; @override List get inputs => const [ @@ -529,9 +533,11 @@ abstract class IosAssetBundle extends Target { final String? flavor = await flutterProject.ios.parseFlavorFromConfiguration(environment); // Copy the assets. + final DartHookResult dartHookResult = await DartBuild.loadHookResult(environment); final Depfile assetDepfile = await copyAssets( environment, assetDirectory, + dartHookResult: dartHookResult, targetPlatform: TargetPlatform.ios, buildMode: buildMode, additionalInputs: [ diff --git a/packages/flutter_tools/lib/src/build_system/targets/linux.dart b/packages/flutter_tools/lib/src/build_system/targets/linux.dart index 8be8524307d09..0d03a40cb7e98 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/linux.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/linux.dart @@ -93,6 +93,7 @@ abstract class BundleLinuxAssets extends Target { @override List get dependencies => [ + const DartBuildForNative(), const KernelSnapshot(), const InstallCodeAssets(), UnpackLinux(targetPlatform), @@ -127,9 +128,11 @@ abstract class BundleLinuxAssets extends Target { .copySync(outputDirectory.childFile('kernel_blob.bin').path); } final String versionInfo = getVersionInfo(environment.defines); + final DartHookResult dartHookResult = await DartBuild.loadHookResult(environment); final Depfile depfile = await copyAssets( environment, outputDirectory, + dartHookResult: dartHookResult, targetPlatform: targetPlatform, buildMode: buildMode, additionalContent: { 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 8ba45dd5786fe..74cbc24701576 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -430,6 +430,9 @@ class CompileMacOSFramework extends Target { abstract class MacOSBundleFlutterAssets extends Target { const MacOSBundleFlutterAssets(); + @override + List get dependencies => const [DartBuildForNative()]; + @override List get inputs => const [ Source.pattern('{BUILD_DIR}/App.framework/App'), @@ -491,9 +494,11 @@ abstract class MacOSBundleFlutterAssets extends Target { final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir); final String? flavor = await flutterProject.macos.parseFlavorFromConfiguration(environment); + final DartHookResult dartHookResult = await DartBuild.loadHookResult(environment); final Depfile assetDepfile = await copyAssets( environment, assetDirectory, + dartHookResult: dartHookResult, targetPlatform: TargetPlatform.darwin, buildMode: buildMode, flavor: flavor, @@ -613,11 +618,12 @@ class DebugMacOSBundleFlutterAssets extends MacOSBundleFlutterAssets { String get name => 'debug_macos_bundle_flutter_assets'; @override - List get dependencies => const [ - KernelSnapshot(), - DebugMacOSFramework(), - DebugUnpackMacOS(), - InstallCodeAssets(), + List get dependencies => [ + ...super.dependencies, + const KernelSnapshot(), + const DebugMacOSFramework(), + const DebugUnpackMacOS(), + const InstallCodeAssets(), ]; @override @@ -659,10 +665,11 @@ class ProfileMacOSBundleFlutterAssets extends MacOSBundleFlutterAssets { String get name => 'profile_macos_bundle_flutter_assets'; @override - List get dependencies => const [ - CompileMacOSFramework(), - InstallCodeAssets(), - ProfileUnpackMacOS(), + List get dependencies => [ + ...super.dependencies, + const CompileMacOSFramework(), + const ProfileUnpackMacOS(), + const InstallCodeAssets(), ]; @override diff --git a/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart b/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart index c98e33d372a5e..206240aaaaf44 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/native_assets.dart @@ -5,6 +5,7 @@ import 'package:meta/meta.dart'; import 'package:package_config/package_config_types.dart'; +import '../../artifacts.dart'; import '../../base/common.dart'; import '../../base/file_system.dart'; import '../../build_info.dart'; @@ -13,61 +14,69 @@ import '../../dart/package_map.dart'; import '../../isolated/native_assets/native_assets.dart'; import '../build_system.dart'; import '../depfile.dart'; -import '../exceptions.dart'; +import '../exceptions.dart' show MissingDefineException; import 'common.dart'; +export '../../isolated/native_assets/native_assets.dart' + show CodeAsset, DartHookResult, DataAsset, DynamicLoadingBundled; + /// Runs the dart build of the app. -abstract class DartBuild extends Target { - const DartBuild({@visibleForTesting FlutterNativeAssetsBuildRunner? buildRunner}) - : _buildRunner = buildRunner; +class DartBuild extends Target { + const DartBuild({ + @visibleForTesting FlutterNativeAssetsBuildRunner? buildRunner, + this.specifiedTargetPlatform, + }) : _buildRunner = buildRunner; final FlutterNativeAssetsBuildRunner? _buildRunner; + final TargetPlatform? specifiedTargetPlatform; + @override Future build(Environment environment) async { final FileSystem fileSystem = environment.fileSystem; - final String? nativeAssetsEnvironment = environment.defines[kNativeAssets]; - - final DartBuildResult result; - if (nativeAssetsEnvironment == 'false') { - result = const DartBuildResult.empty(); - } else { - final TargetPlatform targetPlatform = _getTargetPlatformFromEnvironment(environment, name); - - final PackageConfig packageConfig = await loadPackageConfigWithLogging( - fileSystem.file(environment.packageConfigPath), - logger: environment.logger, - ); - final Uri projectUri = environment.projectDir.uri; - final String? runPackageName = - packageConfig.packages.where((Package p) => p.root == projectUri).firstOrNull?.name; - final FlutterNativeAssetsBuildRunner buildRunner = - _buildRunner ?? - FlutterNativeAssetsBuildRunnerImpl( - environment.packageConfigPath, - packageConfig, - fileSystem, - environment.logger, - runPackageName!, - ); - result = await runFlutterSpecificDartBuild( - environmentDefines: environment.defines, - buildRunner: buildRunner, - targetPlatform: targetPlatform, - projectUri: projectUri, - fileSystem: fileSystem, - ); - } + final DartHookResult result; + + final TargetPlatform targetPlatform = + specifiedTargetPlatform ?? _getTargetPlatformFromEnvironment(environment, name); - final File dartBuildResultJsonFile = environment.buildDir.childFile(dartBuildResultFilename); - if (!dartBuildResultJsonFile.parent.existsSync()) { - dartBuildResultJsonFile.parent.createSync(recursive: true); + final PackageConfig packageConfig = await loadPackageConfigWithLogging( + fileSystem.file(environment.packageConfigPath), + logger: environment.logger, + ); + final Uri projectUri = environment.projectDir.uri; + final String? runPackageName = + packageConfig.packages.where((Package p) => p.root == projectUri).firstOrNull?.name; + final FlutterNativeAssetsBuildRunner buildRunner = + _buildRunner ?? + FlutterNativeAssetsBuildRunnerImpl( + environment.packageConfigPath, + packageConfig, + fileSystem, + environment.logger, + runPackageName!, + ); + result = await runFlutterSpecificHooks( + environmentDefines: { + ...environment.defines, + Artifact.fontSubset.name: environment.artifacts.getArtifactPath(Artifact.fontSubset), + KernelSnapshot.recordedUsagesName: + environment.buildDir.childFile(KernelSnapshot.recordedUsagesName).path, + }, + buildRunner: buildRunner, + targetPlatform: targetPlatform, + projectUri: projectUri, + fileSystem: fileSystem, + ); + + final File dartHookResultJsonFile = environment.buildDir.childFile(dartHookResultFilename); + if (!dartHookResultJsonFile.parent.existsSync()) { + dartHookResultJsonFile.parent.createSync(recursive: true); } - dartBuildResultJsonFile.writeAsStringSync(json.encode(result.toJson())); + dartHookResultJsonFile.writeAsStringSync(json.encode(result.toJson())); final Depfile depfile = Depfile( [for (final Uri dependency in result.dependencies) fileSystem.file(dependency)], - [fileSystem.file(dartBuildResultJsonFile)], + [fileSystem.file(dartHookResultJsonFile)], ); final File outputDepfile = environment.buildDir.childFile(depFilename); if (!outputDepfile.parent.existsSync()) { @@ -89,29 +98,33 @@ abstract class DartBuild extends Target { ), // If different packages are resolved, different native assets might need to be built. Source.pattern('{WORKSPACE_DIR}/.dart_tool/package_config_subset'), - // TODO(mosuem): Should consume resources.json. https://github.com/flutter/flutter/issues/146263 + Source.pattern('{BUILD_DIR}/${KernelSnapshot.recordedUsagesName}'), ]; @override String get name => 'dart_build'; @override - List get outputs => const [ - Source.pattern('{BUILD_DIR}/$dartBuildResultFilename'), - ]; + List get outputs => const [Source.pattern('{BUILD_DIR}/$dartHookResultFilename')]; /// Dependent build [Target]s can use this to consume the result of the /// [DartBuild] target. - static Future loadBuildResult(Environment environment) async { - final File dartBuildResultJsonFile = environment.buildDir.childFile( - DartBuild.dartBuildResultFilename, + static Future loadHookResult(Environment environment) async { + final File dartHookResultJsonFile = environment.buildDir.childFile( + DartBuild.dartHookResultFilename, ); - return DartBuildResult.fromJson( - json.decode(dartBuildResultJsonFile.readAsStringSync()) as Map, + if (!dartHookResultJsonFile.existsSync()) { + return DartHookResult.empty(); + } + return DartHookResult.fromJson( + json.decode(dartHookResultJsonFile.readAsStringSync()) as Map, ); } - static const String dartBuildResultFilename = 'dart_build_result.json'; + @override + List get dependencies => []; + + static const String dartHookResultFilename = 'dart_build_result.json'; static const String depFilename = 'dart_build.d'; } @@ -137,13 +150,13 @@ class InstallCodeAssets extends Target { final TargetPlatform targetPlatform = _getTargetPlatformFromEnvironment(environment, name); // We fetch the result from the [DartBuild]. - final DartBuildResult dartBuildResult = await DartBuild.loadBuildResult(environment); + final DartHookResult dartHookResult = await DartBuild.loadHookResult(environment); // And install/copy the code assets to the right place and create a // native_asset.yaml that can be used by the final AOT compilation. final Uri nativeAssetsFileUri = environment.buildDir.childFile(nativeAssetsFilename).uri; await installCodeAssets( - dartBuildResult: dartBuildResult, + dartHookResult: dartHookResult, environmentDefines: environment.defines, targetPlatform: targetPlatform, projectUri: projectUri, @@ -153,7 +166,7 @@ class InstallCodeAssets extends Target { assert(await fileSystem.file(nativeAssetsFileUri).exists()); final Depfile depfile = Depfile( - [for (final Uri file in dartBuildResult.filesToBeBundled) fileSystem.file(file)], + [for (final Uri file in dartHookResult.filesToBeBundled) fileSystem.file(file)], [fileSystem.file(nativeAssetsFileUri)], ); final File outputDepfile = environment.buildDir.childFile(depFilename); @@ -176,6 +189,7 @@ class InstallCodeAssets extends Target { ), // If different packages are resolved, different native assets might need to be built. Source.pattern('{WORKSPACE_DIR}/.dart_tool/package_config_subset'), + Source.artifact(Artifact.fontSubset), ]; @override diff --git a/packages/flutter_tools/lib/src/build_system/targets/web.dart b/packages/flutter_tools/lib/src/build_system/targets/web.dart index 130caa7d4bfe8..67bafd481cb28 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/web.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/web.dart @@ -28,6 +28,7 @@ import '../depfile.dart'; import '../exceptions.dart'; import 'assets.dart'; import 'localizations.dart'; +import 'native_assets.dart'; /// Generates an entry point for a web target. // Keep this in sync with build_runner/resident_web_runner.dart @@ -408,7 +409,11 @@ class WebReleaseBundle extends Target { String get name => 'web_release_bundle'; @override - List get dependencies => [...compileTargets, templatedFilesTarget]; + List get dependencies => [ + ...compileTargets, + templatedFilesTarget, + const DartBuild(specifiedTargetPlatform: TargetPlatform.web_javascript), + ]; Iterable get buildPatternStems => compileTargets.expand((Dart2WebTarget target) => target.buildPatternStems); @@ -448,9 +453,11 @@ class WebReleaseBundle extends Target { final Directory outputDirectory = environment.outputDir.childDirectory('assets'); outputDirectory.createSync(recursive: true); + final DartHookResult dartHookResult = await DartBuild.loadHookResult(environment); final Depfile depfile = await copyAssets( environment, environment.outputDir.childDirectory('assets'), + dartHookResult: dartHookResult, targetPlatform: TargetPlatform.web_javascript, buildMode: buildMode, ); diff --git a/packages/flutter_tools/lib/src/build_system/targets/windows.dart b/packages/flutter_tools/lib/src/build_system/targets/windows.dart index 807fcd7c90055..bc0394e81b05f 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/windows.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/windows.dart @@ -106,6 +106,7 @@ abstract class BundleWindowsAssets extends Target { @override List get dependencies => [ + const DartBuildForNative(), const KernelSnapshot(), const InstallCodeAssets(), UnpackWindows(targetPlatform), @@ -141,9 +142,11 @@ abstract class BundleWindowsAssets extends Target { .childFile('app.dill') .copySync(outputDirectory.childFile('kernel_blob.bin').path); } + final DartHookResult dartHookResult = await DartBuild.loadHookResult(environment); final Depfile depfile = await copyAssets( environment, outputDirectory, + dartHookResult: dartHookResult, targetPlatform: targetPlatform, buildMode: buildMode, additionalContent: { diff --git a/packages/flutter_tools/lib/src/bundle_builder.dart b/packages/flutter_tools/lib/src/bundle_builder.dart index 22c455f5a2f13..6dd1160cc4b4e 100644 --- a/packages/flutter_tools/lib/src/bundle_builder.dart +++ b/packages/flutter_tools/lib/src/bundle_builder.dart @@ -29,10 +29,6 @@ class BundleBuilder { /// /// The default `mainPath` is `lib/main.dart`. /// The default `manifestPath` is `pubspec.yaml`. - /// - /// If [buildNativeAssets], native assets are built and the mapping for native - /// assets lookup at runtime is embedded in the kernel file, otherwise an - /// empty native assets mapping is embedded in the kernel file. Future build({ required TargetPlatform platform, required BuildInfo buildInfo, @@ -42,7 +38,6 @@ class BundleBuilder { String? applicationKernelFilePath, String? depfilePath, String? assetDirPath, - bool buildNativeAssets = true, @visibleForTesting BuildSystem? buildSystem, }) async { project ??= FlutterProject.current(); @@ -67,7 +62,6 @@ class BundleBuilder { kTargetFile: mainPath, kDeferredComponents: 'false', ...buildInfo.toBuildSystemEnvironment(), - if (!buildNativeAssets) kNativeAssets: 'false', }, artifacts: globals.artifacts!, fileSystem: globals.fs, diff --git a/packages/flutter_tools/lib/src/commands/build_bundle.dart b/packages/flutter_tools/lib/src/commands/build_bundle.dart index ec06e8dcbeb61..262878ca00c2a 100644 --- a/packages/flutter_tools/lib/src/commands/build_bundle.dart +++ b/packages/flutter_tools/lib/src/commands/build_bundle.dart @@ -147,7 +147,6 @@ class BuildBundleCommand extends BuildSubCommand { mainPath: targetFile, depfilePath: stringArg('depfile'), assetDirPath: stringArg('asset-dir'), - buildNativeAssets: false, ); return FlutterCommandResult.success(); } diff --git a/packages/flutter_tools/lib/src/compile.dart b/packages/flutter_tools/lib/src/compile.dart index dbf60e73a6e4d..3acc744124f6d 100644 --- a/packages/flutter_tools/lib/src/compile.dart +++ b/packages/flutter_tools/lib/src/compile.dart @@ -254,6 +254,7 @@ class KernelCompiler { required List dartDefines, required PackageConfig packageConfig, String? nativeAssets, + String? recordedUsages, }) async { final TargetPlatform? platform = targetModel == TargetModel.dartdevc ? TargetPlatform.web_javascript : null; @@ -369,6 +370,7 @@ class KernelCompiler { '--verbosity=error', ...?extraFrontEndOptions, if (mainUri != null) mainUri else '--native-assets-only', + if (recordedUsages != null) '--recorded-usages=$recordedUsages', ]; _logger.printTrace(command.join(' ')); diff --git a/packages/flutter_tools/lib/src/features.dart b/packages/flutter_tools/lib/src/features.dart index 3b9b21cfe0b96..9c14c15427aa6 100644 --- a/packages/flutter_tools/lib/src/features.dart +++ b/packages/flutter_tools/lib/src/features.dart @@ -48,6 +48,9 @@ abstract class FeatureFlags { /// Whether native assets compilation and bundling is enabled. bool get isNativeAssetsEnabled => false; + /// Whether dart data assets building and bundling is enabled. + bool get isDartDataAssetsEnabled => false; + /// Whether Swift Package Manager dependency management is enabled. bool get isSwiftPackageManagerEnabled => false; @@ -72,6 +75,7 @@ const List allFeatures = [ flutterCustomDevicesFeature, cliAnimation, nativeAssets, + dartDataAssets, swiftPackageManager, explicitPackageDependencies, ]; @@ -155,6 +159,14 @@ const Feature nativeAssets = Feature( master: FeatureChannelSetting(available: true), ); +/// Enable dart data assets building and bundling. +const Feature dartDataAssets = Feature( + name: 'dart data assets building and bundling', + configSetting: 'enable-dart-data-assets', + environmentOverride: 'FLUTTER_DART_DATA_ASSETS', + master: FeatureChannelSetting(available: true), +); + /// Enable Swift Package Manager as a darwin dependency manager. const Feature swiftPackageManager = Feature( name: 'support for Swift Package Manager for iOS and macOS', diff --git a/packages/flutter_tools/lib/src/flutter_features.dart b/packages/flutter_tools/lib/src/flutter_features.dart index ff83f64008945..527a4f36bdca4 100644 --- a/packages/flutter_tools/lib/src/flutter_features.dart +++ b/packages/flutter_tools/lib/src/flutter_features.dart @@ -55,6 +55,9 @@ class FlutterFeatureFlags implements FeatureFlags { @override bool get isNativeAssetsEnabled => isEnabled(nativeAssets); + @override + bool get isDartDataAssetsEnabled => isEnabled(dartDataAssets); + @override bool get isSwiftPackageManagerEnabled => isEnabled(swiftPackageManager); diff --git a/packages/flutter_tools/lib/src/isolated/native_assets/native_assets.dart b/packages/flutter_tools/lib/src/isolated/native_assets/native_assets.dart index 6a8d5f75406e5..625fecac8c7e3 100644 --- a/packages/flutter_tools/lib/src/isolated/native_assets/native_assets.dart +++ b/packages/flutter_tools/lib/src/isolated/native_assets/native_assets.dart @@ -4,17 +4,22 @@ // Logic for native assets shared between all host OSes. +import 'package:icon_treeshaker/flutter_config.dart'; +import 'package:icon_treeshaker/hook_helper.dart' show FontAsset; import 'package:logging/logging.dart' as logging; import 'package:native_assets_builder/native_assets_builder.dart'; import 'package:native_assets_cli/code_assets_builder.dart'; +import 'package:native_assets_cli/data_assets_builder.dart'; import 'package:package_config/package_config_types.dart'; +import '../../artifacts.dart' show Artifact; import '../../base/common.dart'; import '../../base/file_system.dart'; import '../../base/logger.dart'; import '../../base/platform.dart'; import '../../build_info.dart'; import '../../build_system/exceptions.dart'; +import '../../build_system/targets/common.dart'; import '../../cache.dart'; import '../../convert.dart'; import '../../features.dart'; @@ -27,53 +32,120 @@ import 'macos/native_assets.dart'; import 'macos/native_assets_host.dart'; import 'windows/native_assets.dart'; -/// The assets produced by a Dart build and the dependencies of those assets. +export 'package:native_assets_cli/code_assets_builder.dart' show CodeAsset, DynamicLoadingBundled; +export 'package:native_assets_cli/data_assets_builder.dart' show DataAsset; + +/// The assets produced by a Dart hook run and the dependencies of those assets. /// /// If any of the dependencies change, then the Dart build should be performed /// again. -final class DartBuildResult { - const DartBuildResult(this.codeAssets, this.dependencies); - - const DartBuildResult.empty() : codeAssets = const [], dependencies = const []; +final class DartHookResult { + const DartHookResult( + this.buildStart, + this.buildEnd, + this.codeAssets, + this.dataAssets, + this.fontAssets, + this.dependencies, + ); - factory DartBuildResult.fromJson(Map json) { + DartHookResult.empty() + : buildStart = DateTime.now(), + buildEnd = DateTime.now(), + codeAssets = const [], + dataAssets = const [], + fontAssets = const [], + dependencies = const []; + + factory DartHookResult.fromJson(Map json) { + final DateTime buildStart = DateTime.parse((json['build_start'] as String?)!); + final DateTime buildEnd = DateTime.parse((json['build_end'] as String?)!); final List dependencies = [ for (final Object? encodedUri in json['dependencies']! as List) Uri.parse(encodedUri! as String), ]; final List codeAssets = [ - for (final Object? json in json['code_assets']! as List) + for (final Object? json in json['code_assets'] as List? ?? const []) CodeAsset.fromEncoded(EncodedAsset.fromJson(json! as Map)), ]; - return DartBuildResult(codeAssets, dependencies); + final List dataAssets = [ + for (final Object? json in json['data_assets'] as List? ?? const []) + DataAsset.fromEncoded(EncodedAsset.fromJson(json! as Map)), + ]; + final List fontAssets = [ + for (final Object? json in json['data_assets'] as List? ?? const []) + FontAsset.fromEncoded(EncodedAsset.fromJson(json! as Map)), + ]; + return DartHookResult(buildStart, buildEnd, codeAssets, dataAssets, fontAssets, dependencies); } + final DateTime buildStart; + final DateTime buildEnd; final List codeAssets; + final List dataAssets; + final List fontAssets; final List dependencies; Map toJson() => { + 'build_start': buildStart.toIso8601String(), + 'build_end': buildEnd.toIso8601String(), 'dependencies': [for (final Uri dep in dependencies) dep.toString()], 'code_assets': [for (final CodeAsset code in codeAssets) code.encode().toJson()], + 'data_assets': [for (final DataAsset asset in dataAssets) asset.encode().toJson()], + 'font_assets': [for (final FontAsset asset in fontAssets) asset.encode().toJson()], }; /// The files that eventually should be bundled with the app. List get filesToBeBundled => [ for (final CodeAsset code in codeAssets) if (code.linkMode is DynamicLoadingBundled) code.file!, + for (final DataAsset asset in dataAssets) asset.file, + for (final FontAsset asset in fontAssets) asset.file, ]; + + /// Whether caller may need to re-run the dart build. + bool isUpToDate(FileSystem fileSystem) { + return !_wasAnyFileModifiedSince(fileSystem, buildStart, dependencies); + } + + /// Whether the files produced by the build are up-to-date. + /// + /// NOTICE: The build itself may be up-to-date but the output may not be (as + /// the output may be existing on disc and not be produced by the build + /// itself - in which case we may not need to re-build if the file changes, + /// but we may need to make a new asset bundle with the modified file). + bool isOutputDirty(FileSystem fileSystem) { + return _wasAnyFileModifiedSince(fileSystem, buildEnd, filesToBeBundled); + } + + static bool _wasAnyFileModifiedSince(FileSystem fileSystem, DateTime since, List uris) { + for (final Uri uri in uris) { + final DateTime modified = fileSystem.statSync(uri.toFilePath()).modified; + if (modified.isAfter(since)) { + return true; + } + } + return false; + } } -/// Invokes the build of all transitive Dart packages and prepares code assets +/// Invokes the build of all transitive Dart package hooks and prepares assets /// to be included in the native build. -Future runFlutterSpecificDartBuild({ +Future runFlutterSpecificHooks({ required Map environmentDefines, required FlutterNativeAssetsBuildRunner buildRunner, - required TargetPlatform targetPlatform, + required TargetPlatform? targetPlatform, required Uri projectUri, required FileSystem fileSystem, }) async { - final OS targetOS = getNativeOSFromTargetPlatform(targetPlatform); - final Uri buildUri = nativeAssetsBuildUri(projectUri, targetOS); + final bool isWeb = targetPlatform == TargetPlatform.web_javascript; + final OS? targetOS = isWeb ? null : getNativeOSFromTargetPlatform(targetPlatform!); + final Uri buildUri = nativeAssetsBuildUri(projectUri, isWeb ? 'web' : targetOS!.name); + + // Sanity check. + final String? codesignIdentity = environmentDefines[kCodesignIdentity]; + assert(codesignIdentity == null || targetOS == OS.iOS || targetOS == OS.macOS); + final Directory buildDir = fileSystem.directory(buildUri); final bool flutterTester = targetPlatform == TargetPlatform.tester; @@ -84,21 +156,25 @@ Future runFlutterSpecificDartBuild({ await buildDir.create(recursive: true); } - if (!await _nativeBuildRequired(buildRunner)) { - return const DartBuildResult.empty(); + if (!await _hookRunRequired(buildRunner)) { + return DartHookResult.empty(); } final BuildMode buildMode = _getBuildMode(environmentDefines, flutterTester); - final List architectures = - flutterTester - ? [Architecture.current] - : _architecturesForOS(targetPlatform, targetOS, environmentDefines); - final DartBuildResult result = - architectures.isEmpty - ? const DartBuildResult.empty() - : await _runDartBuild( + final List? architectures = + isWeb || targetPlatform == null + ? null + : (flutterTester + ? [Architecture.current] + : _architecturesForOS(targetPlatform, targetOS!, environmentDefines)); + final DartHookResult result = + architectures?.isEmpty ?? false + ? DartHookResult.empty() + : await _runDartHooks( environmentDefines: environmentDefines, buildRunner: buildRunner, + codeAssetSupport: !isWeb && featureFlags.isNativeAssetsEnabled, + dataAssetSupport: featureFlags.isDartDataAssetsEnabled, architectures: architectures, projectUri: projectUri, linkingEnabled: _nativeAssetsLinkingEnabled(buildMode), @@ -109,7 +185,7 @@ Future runFlutterSpecificDartBuild({ } Future installCodeAssets({ - required DartBuildResult dartBuildResult, + required DartHookResult dartHookResult, required Map environmentDefines, required TargetPlatform targetPlatform, required Uri projectUri, @@ -117,14 +193,15 @@ Future installCodeAssets({ required Uri nativeAssetsFileUri, }) async { final OS targetOS = getNativeOSFromTargetPlatform(targetPlatform); - final Uri buildUri = nativeAssetsBuildUri(projectUri, targetOS); + + final Uri buildUri = nativeAssetsBuildUri(projectUri, targetOS.name); final bool flutterTester = targetPlatform == TargetPlatform.tester; final BuildMode buildMode = _getBuildMode(environmentDefines, flutterTester); final String? codesignIdentity = environmentDefines[kCodesignIdentity]; final Map assetTargetLocations = assetTargetLocationsForOS( targetOS, - dartBuildResult.codeAssets, + dartHookResult.codeAssets, flutterTester, buildUri, ); @@ -362,7 +439,7 @@ bool _nativeAssetsLinkingEnabled(BuildMode buildMode) { } } -Future _nativeBuildRequired(FlutterNativeAssetsBuildRunner buildRunner) async { +Future _hookRunRequired(FlutterNativeAssetsBuildRunner buildRunner) async { final List packagesWithNativeAssets = await buildRunner.packagesWithNativeAssets(); if (packagesWithNativeAssets.isEmpty) { globals.logger.printTrace( @@ -371,11 +448,12 @@ Future _nativeBuildRequired(FlutterNativeAssetsBuildRunner buildRunner) as return false; } - if (!featureFlags.isNativeAssetsEnabled) { + if (!featureFlags.isNativeAssetsEnabled && !featureFlags.isDartDataAssetsEnabled) { final String packageNames = packagesWithNativeAssets.join(' '); throwToolExit( - 'Package(s) $packageNames require the native assets feature to be enabled. ' - 'Enable using `flutter config --enable-native-assets`.', + 'Package(s) $packageNames require the dart assets feature to be enabled.\n' + ' Enable code assets using `flutter config --enable-native-assets`.' + ' Enable data assets using `flutter config --enable-dart-data-assets`.', ); } return true; @@ -408,9 +486,9 @@ Future ensureNoNativeAssetsOrOsIsSupported( /// This should be the same for different archs, debug/release, etc. /// It should work for all macOS. -Uri nativeAssetsBuildUri(Uri projectUri, OS os) { +Uri nativeAssetsBuildUri(Uri projectUri, String osName) { final String buildDir = getBuildDirectory(); - return projectUri.resolve('$buildDir/native_assets/$os/'); + return projectUri.resolve('$buildDir/native_assets/$osName/'); } Map _assetTargetLocationsWindowsLinux( @@ -552,21 +630,29 @@ Future _copyNativeCodeAssetsForOS( /// /// This will invoke `hook/build.dart` and `hook/link.dart` (if applicable) for /// all transitive dart packages that define such hooks. -Future _runDartBuild({ +Future _runDartHooks({ required Map environmentDefines, required FlutterNativeAssetsBuildRunner buildRunner, - required List architectures, + required List? architectures, required Uri projectUri, required FileSystem fileSystem, required OS? targetOS, required bool linkingEnabled, + required bool codeAssetSupport, + required bool dataAssetSupport, }) async { - final String architectureString = - architectures.length == 1 - ? architectures.single.toString() - : architectures.toList().toString(); + final DateTime buildStart = DateTime.now(); + + // For native builds we have valid architectures. + // For web builds we use `null` as the single architecture. + final bool isWeb = architectures == null; + + final String targetString = + isWeb + ? 'web' + : '$targetOS ${architectures.length == 1 ? architectures.single : architectures}'; - globals.logger.printTrace('Building native assets for $targetOS $architectureString.'); + globals.logger.printTrace('Building native assets for $targetString.'); final List assets = []; final Set dependencies = {}; @@ -599,30 +685,47 @@ Future _runDartBuild({ : null; final MacOSCodeConfig? macOSConfig = targetOS == OS.macOS ? MacOSCodeConfig(targetVersion: targetMacOSVersion) : null; - for (final Architecture architecture in architectures) { + for (final Architecture? architecture in architectures ?? [null]) { + assert(!(codeAssetSupport && architecture == null)); final BuildResult? buildResult = await buildRunner.build( - buildAssetTypes: [CodeAsset.type], - inputCreator: - () => - BuildInputBuilder() - ..config.setupCode( - targetArchitecture: architecture, - linkModePreference: LinkModePreference.dynamic, - cCompiler: cCompilerConfig, - targetOS: targetOS!, - android: androidConfig, - iOS: iosConfig, - macOS: macOSConfig, - ), + buildAssetTypes: [ + if (codeAssetSupport) CodeAsset.type, + if (dataAssetSupport) DataAsset.type, + FontAsset.type, + ], + inputCreator: () { + final BuildInputBuilder buildInputBuilder = BuildInputBuilder(); + if (targetOS != null) { + buildInputBuilder.config + ..setupCode( + targetArchitecture: architecture, + linkModePreference: LinkModePreference.dynamic, + cCompiler: cCompilerConfig, + targetOS: targetOS, + android: androidConfig, + iOS: iosConfig, + macOS: macOSConfig, + ) + ..setupFlutter( + //TODO(mosum): In the future, decouple the font subset tool from Flutter by shipping it with the icon treeshaking package. + fontSubsetBinary: environmentDefines[Artifact.fontSubset.name], + ); + } + return buildInputBuilder; + }, inputValidator: - (BuildInput config) async => [...await validateCodeAssetBuildInput(config)], + (BuildInput config) async => [ + if (codeAssetSupport) ...await validateCodeAssetBuildInput(config), + if (dataAssetSupport) ...await validateDataAssetBuildInput(config), + ], buildValidator: (BuildInput config, BuildOutput output) async => [ - ...await validateCodeAssetBuildOutput(config, output), + if (codeAssetSupport) ...await validateCodeAssetBuildOutput(config, output), + if (dataAssetSupport) ...await validateDataAssetBuildOutput(config, output), ], applicationAssetValidator: (List assets) async => [ - ...await validateCodeAssetInApplication(assets), + if (codeAssetSupport) ...await validateCodeAssetInApplication(assets), ], workingDirectory: projectUri, linkingEnabled: linkingEnabled, @@ -633,40 +736,55 @@ Future _runDartBuild({ dependencies.addAll(buildResult.dependencies); if (!linkingEnabled) { assets.addAll(buildResult.encodedAssets); - } else { - final LinkResult? linkResult = await buildRunner.link( - buildAssetTypes: [CodeAsset.type], - inputCreator: - () => - LinkInputBuilder() - ..config.setupCode( - targetArchitecture: architecture, - linkModePreference: LinkModePreference.dynamic, - cCompiler: cCompilerConfig, - targetOS: targetOS!, - android: androidConfig, - iOS: iosConfig, - macOS: macOSConfig, - ), - inputValidator: - (LinkInput config) async => [...await validateCodeAssetLinkInput(config)], - linkValidator: - (LinkInput config, LinkOutput output) async => [ - ...await validateCodeAssetLinkOutput(config, output), - ], - applicationAssetValidator: - (List assets) async => [ - ...await validateCodeAssetInApplication(assets), - ], - workingDirectory: projectUri, - buildResult: buildResult, - ); - if (linkResult == null) { - _throwNativeAssetsLinkFailed(); - } - assets.addAll(linkResult.encodedAssets); - dependencies.addAll(linkResult.dependencies); + continue; + } + final LinkResult? linkResult = await buildRunner.link( + buildAssetTypes: [ + if (codeAssetSupport) CodeAsset.type, + if (dataAssetSupport) DataAsset.type, + FontAsset.type, + ], + inputCreator: () { + final LinkInputBuilder linkInputBuilder = LinkInputBuilder(); + if (targetOS != null) { + linkInputBuilder.config.setupCode( + targetArchitecture: architecture, + linkModePreference: LinkModePreference.dynamic, + cCompiler: cCompilerConfig, + targetOS: targetOS, + android: androidConfig, + iOS: iosConfig, + macOS: macOSConfig, + ); + } + linkInputBuilder.setupLink( + assets: assets, + recordedUsesFile: Uri.file(environmentDefines[KernelSnapshot.recordedUsagesName]!), + ); + return linkInputBuilder; + }, + inputValidator: + (LinkInput config) async => [ + if (codeAssetSupport) ...await validateCodeAssetLinkInput(config), + if (dataAssetSupport) ...await validateDataAssetLinkInput(config), + ], + linkValidator: + (LinkInput config, LinkOutput output) async => [ + if (codeAssetSupport) ...await validateCodeAssetLinkOutput(config, output), + if (dataAssetSupport) ...await validateDataAssetLinkOutput(config, output), + ], + applicationAssetValidator: + (List assets) async => [ + if (codeAssetSupport) ...await validateCodeAssetInApplication(assets), + ], + workingDirectory: projectUri, + buildResult: buildResult, + ); + if (linkResult == null) { + _throwNativeAssetsLinkFailed(); } + assets.addAll(linkResult.encodedAssets); + dependencies.addAll(linkResult.dependencies); } final List codeAssets = @@ -674,8 +792,28 @@ Future _runDartBuild({ .where((EncodedAsset asset) => asset.type == CodeAsset.type) .map(CodeAsset.fromEncoded) .toList(); - globals.logger.printTrace('Building native assets for $targetOS $architectureString done.'); - return DartBuildResult(codeAssets, dependencies.toList()); + final List dataAssets = + assets + .where((EncodedAsset asset) => asset.type == DataAsset.type) + .map(DataAsset.fromEncoded) + .toList(); + + final List fontAssets = + assets + .where((EncodedAsset asset) => asset.type == FontAsset.type) + .map(FontAsset.fromEncoded) + .toList(); + globals.logger.printTrace('Building native assets for $targetString done.'); + + final DateTime buildEnd = DateTime.now(); + return DartHookResult( + buildStart, + buildEnd, + codeAssets, + dataAssets, + fontAssets, + dependencies.toList(), + ); } List _architecturesForOS( diff --git a/packages/flutter_tools/lib/src/isolated/native_assets/test/native_assets.dart b/packages/flutter_tools/lib/src/isolated/native_assets/test/native_assets.dart index 832492b68474b..458ab53ba09eb 100644 --- a/packages/flutter_tools/lib/src/isolated/native_assets/test/native_assets.dart +++ b/packages/flutter_tools/lib/src/isolated/native_assets/test/native_assets.dart @@ -7,6 +7,7 @@ import 'package:native_assets_cli/code_assets.dart' show OS; import 'package:package_config/package_config_types.dart'; +import '../../../artifacts.dart' show Artifact; import '../../../base/platform.dart'; import '../../../build_info.dart'; import '../../../globals.dart' as globals; @@ -22,7 +23,7 @@ class TestCompilerNativeAssetsBuilderImpl implements TestCompilerNativeAssetsBui @override String windowsBuildDirectory(FlutterProject project) => - nativeAssetsBuildUri(project.directory.uri, OS.windows).toFilePath(); + nativeAssetsBuildUri(project.directory.uri, OS.windows.name).toFilePath(); } Future testCompilerBuildNativeAssets(BuildInfo buildInfo) async { @@ -54,15 +55,16 @@ Future testCompilerBuildNativeAssets(BuildInfo buildInfo) async { // `build/native_assets//native_assets.json` file which uses absolute // paths to the shared libraries. final OS targetOS = getNativeOSFromTargetPlatform(TargetPlatform.tester); - final Uri buildUri = nativeAssetsBuildUri(projectUri, targetOS); + final Uri buildUri = nativeAssetsBuildUri(projectUri, targetOS.name); final Uri nativeAssetsFileUri = buildUri.resolve('native_assets.json'); final Map environmentDefines = { kBuildMode: buildInfo.mode.cliName, + Artifact.fontSubset.name: '', }; // First perform the dart build. - final DartBuildResult dartBuildResult = await runFlutterSpecificDartBuild( + final DartHookResult dartHookResult = await runFlutterSpecificHooks( environmentDefines: environmentDefines, buildRunner: buildRunner, targetPlatform: TargetPlatform.tester, @@ -72,7 +74,7 @@ Future testCompilerBuildNativeAssets(BuildInfo buildInfo) async { // Then "install" the code assets so they can be used at runtime. await installCodeAssets( - dartBuildResult: dartBuildResult, + dartHookResult: dartHookResult, environmentDefines: environmentDefines, targetPlatform: TargetPlatform.tester, projectUri: projectUri, diff --git a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart index 48075324ca19f..73d22f5b7fe7d 100644 --- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart @@ -643,6 +643,7 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). if (rebuildBundle) { _logger.printTrace('Updating assets'); final int result = await assetBundle.build( + dartHookResult: await runDartBuild(targetPlatform: TargetPlatform.web_javascript), packageConfigPath: debuggingOptions.buildInfo.packageConfigPath, targetPlatform: TargetPlatform.web_javascript, ); diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 22b32458208d0..406fb62ce89a1 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -24,6 +24,7 @@ import 'base/terminal.dart'; import 'base/utils.dart'; import 'build_info.dart'; import 'build_system/build_system.dart'; +import 'build_system/targets/native_assets.dart'; import 'build_system/tools/shader_compiler.dart'; import 'bundle.dart'; import 'cache.dart'; @@ -1050,7 +1051,34 @@ abstract class ResidentRunner extends ResidentHandlers { bool _exited = false; Completer _finished = Completer(); BuildResult? _lastBuild; - Environment? _environment; + + late final Environment _environment = Environment( + artifacts: globals.artifacts!, + logger: globals.logger, + cacheDir: globals.cache.getRoot(), + engineVersion: globals.flutterVersion.engineRevision, + fileSystem: globals.fs, + flutterRootDir: globals.fs.directory(Cache.flutterRoot), + outputDir: globals.fs.directory(getBuildDirectory()), + processManager: globals.processManager, + platform: globals.platform, + analytics: globals.analytics, + projectDir: globals.fs.currentDirectory, + packageConfigPath: debuggingOptions.buildInfo.packageConfigPath, + generateDartPluginRegistry: generateDartPluginRegistry, + defines: { + // Needed for Dart plugin registry generation. + kTargetFile: mainPath, + kBuildMode: debuggingOptions.buildInfo.mode.cliName, + }, + ); + + /// The result of the last dart build. Will be populated in + /// [runDartBuild]. + DartHookResult? _dartHookResult; + + /// The last dart build's result. + DartHookResult? get dartHookResult => _dartHookResult; @override bool hotMode; @@ -1148,26 +1176,6 @@ abstract class ResidentRunner extends ResidentHandlers { @override Future runSourceGenerators() async { - _environment ??= Environment( - artifacts: globals.artifacts!, - logger: globals.logger, - cacheDir: globals.cache.getRoot(), - engineVersion: globals.flutterVersion.engineRevision, - fileSystem: globals.fs, - flutterRootDir: globals.fs.directory(Cache.flutterRoot), - outputDir: globals.fs.directory(getBuildDirectory()), - processManager: globals.processManager, - platform: globals.platform, - analytics: globals.analytics, - projectDir: globals.fs.currentDirectory, - packageConfigPath: debuggingOptions.buildInfo.packageConfigPath, - generateDartPluginRegistry: generateDartPluginRegistry, - defines: { - // Needed for Dart plugin registry generation. - kTargetFile: mainPath, - }, - ); - final CompositeTarget compositeTarget = CompositeTarget([ globals.buildTargets.generateLocalizationsTarget, globals.buildTargets.dartPluginRegistrantTarget, @@ -1175,7 +1183,7 @@ abstract class ResidentRunner extends ResidentHandlers { _lastBuild = await globals.buildSystem.buildIncremental( compositeTarget, - _environment!, + _environment, _lastBuild, ); if (!_lastBuild!.success) { @@ -1189,6 +1197,32 @@ abstract class ResidentRunner extends ResidentHandlers { globals.printTrace('complete'); } + Future runDartBuild({required TargetPlatform targetPlatform}) async { + globals.printTrace('runDartBuild() with ${_environment.defines} and $targetPlatform'); + if (_dartHookResult?.isUpToDate(globals.fs) ?? false) { + globals.printTrace('runDartBuild() - up-to-date already'); + return _dartHookResult!; + } + globals.printTrace('runDartBuild() - will perform dart build'); + + final BuildResult lastBuild = await globals.buildSystem.build( + DartBuild(specifiedTargetPlatform: targetPlatform), + _environment, + ); + if (!lastBuild.success) { + for (final ExceptionMeasurement exceptionMeasurement in lastBuild.exceptions.values) { + globals.printError( + exceptionMeasurement.exception.toString(), + stackTrace: globals.logger.isVerbose ? exceptionMeasurement.stackTrace : null, + ); + } + } + + _dartHookResult = await DartBuild.loadHookResult(_environment); + globals.printTrace('runDartBuild() - done'); + return _dartHookResult!; + } + @protected void writeVmServiceFile() { if (debuggingOptions.vmserviceOutFile != null) { diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 8e26c310875de..633c76efcb8b6 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -125,7 +125,13 @@ class HotRunner extends ResidentRunner { final Map> benchmarkData = >{}; - String? _targetPlatform; + String? _targetPlatformName; + TargetPlatform get _targetPlatform => + _targetPlatformName != null + ? getTargetPlatformForName(_targetPlatformName!) + : throw ArgumentError( + 'Access to the target platform needs a call to _calculateTargetPlatform first', + ); String? _sdkName; bool? _emulator; @@ -137,22 +143,22 @@ class HotRunner extends ResidentRunner { bool get supportsDetach => stopAppDuringCleanup; Future _calculateTargetPlatform() async { - if (_targetPlatform != null) { + if (_targetPlatformName != null) { return; } switch (flutterDevices.length) { case 1: final Device device = flutterDevices.first.device!; - _targetPlatform = getNameForTargetPlatform(await device.targetPlatform); + _targetPlatformName = getNameForTargetPlatform(await device.targetPlatform); _sdkName = await device.sdkNameAndVersion; _emulator = await device.isLocalEmulator; case > 1: - _targetPlatform = 'multiple'; + _targetPlatformName = 'multiple'; _sdkName = 'multiple'; _emulator = false; default: - _targetPlatform = 'unknown'; + _targetPlatformName = 'unknown'; _sdkName = 'unknown'; _emulator = false; } @@ -303,8 +309,13 @@ class HotRunner extends ResidentRunner { return 3; } + await _calculateTargetPlatform(); + final Stopwatch initialUpdateDevFSsTimer = Stopwatch()..start(); - final UpdateFSReport devfsResult = await _updateDevFS(fullRestart: needsFullRestart); + final UpdateFSReport devfsResult = await _updateDevFS( + fullRestart: needsFullRestart, + targetPlatform: _targetPlatform, + ); _addBenchmarkData( 'hotReloadInitialDevFSSyncMilliseconds', initialUpdateDevFSsTimer.elapsed.inMilliseconds, @@ -437,7 +448,7 @@ class HotRunner extends ResidentRunner { appStartedCompleter?.future.then((_) { HotEvent( 'reload-ready', - targetPlatform: _targetPlatform!, + targetPlatform: _targetPlatformName!, sdkName: _sdkName!, emulator: _emulator!, fullRestart: false, @@ -449,7 +460,7 @@ class HotRunner extends ResidentRunner { _analytics.send( Event.hotRunnerInfo( label: 'reload-ready', - targetPlatform: _targetPlatform!, + targetPlatform: _targetPlatformName!, sdkName: _sdkName!, emulator: _emulator!, fullRestart: false, @@ -489,12 +500,16 @@ class HotRunner extends ResidentRunner { ]; } - Future _updateDevFS({bool fullRestart = false}) async { + Future _updateDevFS({ + bool fullRestart = false, + required TargetPlatform targetPlatform, + }) async { final bool isFirstUpload = !assetBundle.wasBuiltOnce(); final bool rebuildBundle = assetBundle.needsBuild(); if (rebuildBundle) { globals.printTrace('Updating assets'); final int result = await assetBundle.build( + dartHookResult: await runDartBuild(targetPlatform: targetPlatform), packageConfigPath: debuggingOptions.buildInfo.packageConfigPath, flavor: debuggingOptions.buildInfo.flavor, ); @@ -609,7 +624,7 @@ class HotRunner extends ResidentRunner { final Stopwatch restartTimer = Stopwatch()..start(); UpdateFSReport updatedDevFS; try { - updatedDevFS = await _updateDevFS(fullRestart: true); + updatedDevFS = await _updateDevFS(fullRestart: true, targetPlatform: _targetPlatform); } finally { hotRunnerConfig!.updateDevFSComplete(); } @@ -777,7 +792,7 @@ class HotRunner extends ResidentRunner { if (fullRestart) { final OperationResult result = await _fullRestartHelper( - targetPlatform: _targetPlatform, + targetPlatform: _targetPlatformName, sdkName: _sdkName, emulator: _emulator, reason: reason, @@ -794,7 +809,7 @@ class HotRunner extends ResidentRunner { return result; } final OperationResult result = await _hotReloadHelper( - targetPlatform: _targetPlatform, + targetPlatform: _targetPlatformName, sdkName: _sdkName, emulator: _emulator, reason: reason, @@ -1016,7 +1031,7 @@ class HotRunner extends ResidentRunner { final Stopwatch devFSTimer = Stopwatch()..start(); UpdateFSReport updatedDevFS; try { - updatedDevFS = await _updateDevFS(); + updatedDevFS = await _updateDevFS(targetPlatform: _targetPlatform); } finally { hotRunnerConfig!.updateDevFSComplete(); } diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index ec243584e9913..e29dabb3e1b12 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -57,6 +57,8 @@ dependencies: graphs: 2.3.2 native_assets_builder: 0.11.1 native_assets_cli: 0.11.0 + icon_treeshaker: + path: ../../dev/icon_treeshaker # We depend on very specific internal implementation details of the # 'test' package, which change between versions, so when upgrading diff --git a/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart index 12155e369892f..9a6114b9cb3e6 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/build_bundle_test.dart @@ -308,7 +308,6 @@ void main() { kIconTreeShakerFlag: 'false', kDeferredComponents: 'false', kDartObfuscation: 'false', - kNativeAssets: 'false', }); }), FileSystem: fsFactory, @@ -350,7 +349,6 @@ void main() { kIconTreeShakerFlag: 'false', kDeferredComponents: 'false', kDartObfuscation: 'false', - kNativeAssets: 'false', }); }), FileSystem: fsFactory, @@ -392,7 +390,6 @@ void main() { kIconTreeShakerFlag: 'false', kDeferredComponents: 'false', kDartObfuscation: 'false', - kNativeAssets: 'false', }); }), FileSystem: fsFactory, @@ -435,7 +432,6 @@ void main() { kIconTreeShakerFlag: 'false', kDeferredComponents: 'false', kDartObfuscation: 'false', - kNativeAssets: 'false', }); }), FileSystem: fsFactory, @@ -478,7 +474,6 @@ void main() { kIconTreeShakerFlag: 'false', kDeferredComponents: 'false', kDartObfuscation: 'false', - kNativeAssets: 'false', }); }), FileSystem: fsFactory, @@ -521,7 +516,6 @@ void main() { kIconTreeShakerFlag: 'false', kDeferredComponents: 'false', kDartObfuscation: 'false', - kNativeAssets: 'false', }); }), FileSystem: fsFactory, @@ -571,7 +565,6 @@ void main() { kIconTreeShakerFlag: 'false', kDeferredComponents: 'false', kDartObfuscation: 'false', - kNativeAssets: 'false', }); }), FileSystem: fsFactory, @@ -621,7 +614,6 @@ void main() { kIconTreeShakerFlag: 'false', kDeferredComponents: 'false', kDartObfuscation: 'false', - kNativeAssets: 'false', }); }), FileSystem: fsFactory, diff --git a/packages/flutter_tools/test/general.shard/devfs_test.dart b/packages/flutter_tools/test/general.shard/devfs_test.dart index b99e49d1ad800..ce085e3666be5 100644 --- a/packages/flutter_tools/test/general.shard/devfs_test.dart +++ b/packages/flutter_tools/test/general.shard/devfs_test.dart @@ -24,6 +24,7 @@ import 'package:flutter_tools/src/build_system/tools/shader_compiler.dart'; import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/flutter_manifest.dart'; +import 'package:flutter_tools/src/isolated/native_assets/native_assets.dart'; import 'package:package_config/package_config.dart'; import 'package:test/fake.dart'; import 'package:vm_service/vm_service.dart' as vm_service; @@ -990,6 +991,7 @@ class FakeBundle extends AssetBundle { @override Future build({ + DartHookResult? dartHookResult, String manifestPath = defaultManifestPath, String? assetDirPath, String? packageConfigPath, diff --git a/packages/flutter_tools/test/general.shard/isolated/android/native_assets_test.dart b/packages/flutter_tools/test/general.shard/isolated/android/native_assets_test.dart index 3d188e9233867..b69056cc16e61 100644 --- a/packages/flutter_tools/test/general.shard/isolated/android/native_assets_test.dart +++ b/packages/flutter_tools/test/general.shard/isolated/android/native_assets_test.dart @@ -89,7 +89,7 @@ void main() { kBuildMode: buildMode.cliName, kMinSdkVersion: minSdkVersion, }; - final DartBuildResult result = await runFlutterSpecificDartBuild( + final DartHookResult result = await runFlutterSpecificHooks( environmentDefines: environmentDefines, targetPlatform: TargetPlatform.android_arm64, projectUri: projectUri, @@ -97,7 +97,7 @@ void main() { buildRunner: buildRunner, ); await installCodeAssets( - dartBuildResult: result, + dartHookResult: result, environmentDefines: environmentDefines, targetPlatform: TargetPlatform.android_arm64, projectUri: projectUri, @@ -130,7 +130,7 @@ void main() { () async { final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); await packageConfig.create(recursive: true); - await runFlutterSpecificDartBuild( + await runFlutterSpecificHooks( environmentDefines: { kBuildMode: BuildMode.debug.cliName, kMinSdkVersion: minSdkVersion, @@ -155,7 +155,7 @@ void main() { await packageConfig.parent.create(); await packageConfig.create(); expect( - () => runFlutterSpecificDartBuild( + () => runFlutterSpecificHooks( environmentDefines: { kBuildMode: BuildMode.debug.cliName, kMinSdkVersion: minSdkVersion, diff --git a/packages/flutter_tools/test/general.shard/isolated/build_system/targets/native_assets_test.dart b/packages/flutter_tools/test/general.shard/isolated/build_system/targets/native_assets_test.dart index 5c2f6b6dda028..d7790a7aaee21 100644 --- a/packages/flutter_tools/test/general.shard/isolated/build_system/targets/native_assets_test.dart +++ b/packages/flutter_tools/test/general.shard/isolated/build_system/targets/native_assets_test.dart @@ -64,7 +64,6 @@ void main() { iosEnvironment.buildDir.createSync(recursive: true); androidEnvironment.buildDir.createSync(recursive: true); }); - testWithoutContext('NativeAssets throws error if missing target platform', () async { iosEnvironment.defines.remove(kTargetPlatform); expect( @@ -214,14 +213,14 @@ void main() { // * dart build output should depend on C source // * installation output should depend on shared library from dart build - final File dartBuildResult = iosEnvironment.buildDir.childFile( - DartBuild.dartBuildResultFilename, + final File dartHookResult = iosEnvironment.buildDir.childFile( + DartBuild.dartHookResultFilename, ); final File buildDepsFile = iosEnvironment.buildDir.childFile(DartBuild.depFilename); expect(buildDepsFile, exists); expect( buildDepsFile.readAsStringSync(), - stringContainsInOrder([dartBuildResult.path, ':', 'src/foo.c']), + stringContainsInOrder([dartHookResult.path, ':', 'src/foo.c']), ); final File nativeAssetsYaml = iosEnvironment.buildDir.childFile( diff --git a/packages/flutter_tools/test/general.shard/isolated/ios/native_assets_test.dart b/packages/flutter_tools/test/general.shard/isolated/ios/native_assets_test.dart index 5d0c43c992f7d..4750a9fe23b7f 100644 --- a/packages/flutter_tools/test/general.shard/isolated/ios/native_assets_test.dart +++ b/packages/flutter_tools/test/general.shard/isolated/ios/native_assets_test.dart @@ -199,7 +199,7 @@ void main() { kSdkRoot: '.../iPhone Simulator', kIosArchs: 'arm64 x86_64', }; - final DartBuildResult dartBuildResult = await runFlutterSpecificDartBuild( + final DartHookResult dartHookResult = await runFlutterSpecificHooks( environmentDefines: environmentDefines, targetPlatform: TargetPlatform.ios, projectUri: projectUri, @@ -207,7 +207,7 @@ void main() { buildRunner: buildRunner, ); await installCodeAssets( - dartBuildResult: dartBuildResult, + dartHookResult: dartHookResult, environmentDefines: environmentDefines, targetPlatform: TargetPlatform.ios, projectUri: projectUri, diff --git a/packages/flutter_tools/test/general.shard/isolated/linux/native_assets_test.dart b/packages/flutter_tools/test/general.shard/isolated/linux/native_assets_test.dart index f1a2796c9945d..5508644f30b5c 100644 --- a/packages/flutter_tools/test/general.shard/isolated/linux/native_assets_test.dart +++ b/packages/flutter_tools/test/general.shard/isolated/linux/native_assets_test.dart @@ -61,7 +61,7 @@ void main() { final File packageConfig = environment.projectDir.childFile('.dart_tool/package_config.json'); await packageConfig.create(recursive: true); - await runFlutterSpecificDartBuild( + await runFlutterSpecificHooks( environmentDefines: {kBuildMode: BuildMode.debug.cliName}, targetPlatform: TargetPlatform.linux_x64, projectUri: projectUri, diff --git a/packages/flutter_tools/test/general.shard/isolated/macos/native_assets_test.dart b/packages/flutter_tools/test/general.shard/isolated/macos/native_assets_test.dart index e03f65fba30e4..8984ebbc62007 100644 --- a/packages/flutter_tools/test/general.shard/isolated/macos/native_assets_test.dart +++ b/packages/flutter_tools/test/general.shard/isolated/macos/native_assets_test.dart @@ -300,7 +300,7 @@ void main() { }; final TargetPlatform targetPlatform = flutterTester ? TargetPlatform.tester : TargetPlatform.darwin; - final DartBuildResult dartBuildResult = await runFlutterSpecificDartBuild( + final DartHookResult dartHookResult = await runFlutterSpecificHooks( environmentDefines: environmentDefines, targetPlatform: targetPlatform, projectUri: projectUri, @@ -315,7 +315,7 @@ void main() { : nonFlutterTesterAssetUri; await installCodeAssets( - dartBuildResult: dartBuildResult, + dartHookResult: dartHookResult, environmentDefines: environmentDefines, targetPlatform: targetPlatform, projectUri: projectUri, diff --git a/packages/flutter_tools/test/general.shard/isolated/native_assets_test.dart b/packages/flutter_tools/test/general.shard/isolated/native_assets_test.dart index b14befe960bd2..7f55f507966eb 100644 --- a/packages/flutter_tools/test/general.shard/isolated/native_assets_test.dart +++ b/packages/flutter_tools/test/general.shard/isolated/native_assets_test.dart @@ -77,7 +77,7 @@ void main() { makeCodeAsset('free', LookupInExecutable()), makeCodeAsset('draw', DynamicLoadingSystem(Uri.file('/usr/lib/skia.so'))), ]; - final DartBuildResult dartBuildResult = await runFlutterSpecificDartBuild( + final DartHookResult dartHookResult = await runFlutterSpecificHooks( environmentDefines: environmentDefines, targetPlatform: TargetPlatform.linux_x64, projectUri: projectUri, @@ -89,7 +89,7 @@ void main() { ), ); await installCodeAssets( - dartBuildResult: dartBuildResult, + dartHookResult: dartHookResult, environmentDefines: environmentDefines, targetPlatform: TargetPlatform.windows_x64, projectUri: projectUri, @@ -108,7 +108,7 @@ void main() { await packageConfig.parent.create(); await packageConfig.create(); expect( - () => runFlutterSpecificDartBuild( + () => runFlutterSpecificHooks( environmentDefines: {kBuildMode: BuildMode.debug.cliName}, targetPlatform: TargetPlatform.windows_x64, projectUri: projectUri, @@ -117,11 +117,7 @@ void main() { packagesWithNativeAssetsResult: ['bar'], ), ), - throwsToolExit( - message: - 'Package(s) bar require the native assets feature to be enabled. ' - 'Enable using `flutter config --enable-native-assets`.', - ), + throwsToolExit(message: 'Enable code assets using `flutter config --enable-native-assets`'), ); }, ); @@ -142,7 +138,7 @@ void main() { final Map environmentDefines = { kBuildMode: BuildMode.debug.cliName, }; - final DartBuildResult dartBuildResult = await runFlutterSpecificDartBuild( + final DartHookResult dartHookResult = await runFlutterSpecificHooks( environmentDefines: environmentDefines, targetPlatform: TargetPlatform.windows_x64, projectUri: projectUri, @@ -152,7 +148,7 @@ void main() { ), ); await installCodeAssets( - dartBuildResult: dartBuildResult, + dartHookResult: dartHookResult, environmentDefines: environmentDefines, targetPlatform: TargetPlatform.windows_x64, projectUri: projectUri, @@ -184,7 +180,7 @@ void main() { await packageConfig.parent.create(); await packageConfig.create(); expect( - () => runFlutterSpecificDartBuild( + () => runFlutterSpecificHooks( environmentDefines: {kBuildMode: BuildMode.debug.cliName}, targetPlatform: TargetPlatform.linux_x64, projectUri: projectUri, @@ -226,7 +222,7 @@ void main() { file: file, ); - final DartBuildResult result = await runFlutterSpecificDartBuild( + final DartHookResult result = await runFlutterSpecificHooks( environmentDefines: { // Release mode means the dart build has linking enabled. kBuildMode: BuildMode.release.cliName, diff --git a/packages/flutter_tools/test/general.shard/isolated/windows/native_assets_test.dart b/packages/flutter_tools/test/general.shard/isolated/windows/native_assets_test.dart index 36ab9f068f046..a5ee8b7c7dc02 100644 --- a/packages/flutter_tools/test/general.shard/isolated/windows/native_assets_test.dart +++ b/packages/flutter_tools/test/general.shard/isolated/windows/native_assets_test.dart @@ -107,7 +107,7 @@ void main() { }; final TargetPlatform targetPlatform = flutterTester ? TargetPlatform.tester : TargetPlatform.windows_x64; - final DartBuildResult dartBuildResult = await runFlutterSpecificDartBuild( + final DartHookResult dartHookResult = await runFlutterSpecificHooks( environmentDefines: environmentDefines, targetPlatform: targetPlatform, projectUri: projectUri, @@ -123,7 +123,7 @@ void main() { ) : nonFlutterTesterAssetUri; await installCodeAssets( - dartBuildResult: dartBuildResult, + dartHookResult: dartHookResult, environmentDefines: environmentDefines, targetPlatform: targetPlatform, projectUri: projectUri, diff --git a/packages/flutter_tools/test/integration.shard/isolated/dart_data_asset_test.dart b/packages/flutter_tools/test/integration.shard/isolated/dart_data_asset_test.dart new file mode 100644 index 0000000000000..fd088be9fa8da --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/isolated/dart_data_asset_test.dart @@ -0,0 +1,433 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This test exercises the embedding of the native assets mapping in dill files. +// An initial dill file is created by `flutter assemble` and used for running +// the application. This dill must contain the mapping. +// When doing hot reload, this mapping must stay in place. +// When doing a hot restart, a new dill file is pushed. This dill file must also +// contain the native assets mapping. +// When doing a hot reload, this mapping must stay in place. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:yaml_edit/yaml_edit.dart' show YamlEditor; + +import '../../src/common.dart'; +import '../test_utils.dart' show ProcessResultMatcher, fileSystem, flutterBin, platform; +import '../transition_test_utils.dart'; +import 'native_assets_test_utils.dart'; + +final String hostOs = platform.operatingSystem; +const String packageName = 'data_asset_example'; +const String packageNameDependency = 'data_asset_dependency'; + +void main() { + if (!platform.isMacOS && !platform.isLinux && !platform.isWindows) { + return; + } + setUpAll(() async { + processManager.runSync([flutterBin, 'config', '--enable-native-assets']); + processManager.runSync([flutterBin, 'config', '--enable-dart-data-assets']); + }); + + late Directory tempDirectory; + late Directory root; + late Directory rootDependency; + + setUp(() async { + // Do not reuse project structure to be able to make local changes + tempDirectory = fileSystem.directory( + fileSystem.systemTempDirectory.createTempSync().resolveSymbolicLinksSync(), + ); + root = createAppWithName(packageName, tempDirectory); + await createDataAssetApp(packageName, root); + + rootDependency = createAppWithName(packageNameDependency, tempDirectory); + }); + + tearDown(() { + tryToDelete(tempDirectory); + }); + + group('dart data assets', () { + // NOTE: flutter-tester doesn't support profile/release mode. + // NOTE: flutter web doesn't allow cpaturing print()s in profile/release + // nOTE: flutter web doens't allow adding assets on hot-restart + final List devices = [hostOs, 'chrome', 'flutter-tester']; + final List modes = ['debug', 'release']; + + for (final String mode in modes) { + for (final String device in devices) { + final bool isFlutterTester = device == 'flutter-tester'; + final bool isWeb = device == 'chrome'; + final bool isDebug = mode == 'debug'; + + // This test relies on running the flutter app and capturing `print()`s + // the app prints to determine if the test succeeded. + // `flutter run --profile/release` on the web doesn't support capturing + // prints + // -> See https://github.com/flutter/flutter/issues/159668 + if (isWeb && !isDebug) { + continue; + } + + // Flutter tester only supports debug mode. + if (isFlutterTester && !isDebug) { + continue; + } + + testWithoutContext('flutter run on $device --$mode', () async { + final bool performRestart = isDebug; + final bool performReload = isDebug; + + final Map assets = {'id1': 'content1', 'id2': 'content2'}; + writeAssets(assets, root); + writeHookLibrary(root, assets, available: ['id1']); + writeHelperLibrary(root, 'version1', assets.keys.toList()); + + final ProcessTestResult result = await runFlutter( + ['run', '-v', '-d', device, '--$mode'], + root.path, + [ + Barrier.contains('Launching lib/main.dart on'), + Multiple.contains( + [ + // The flutter tool will print it's ready to accept keys (e.g. + // q=quit, ...) + // (This can be racy with app already running and printing) + 'Flutter run key command', + + // Once the app runs it will print whether it found assets. + 'VERSION: version1', + 'FOUND "packages/data_asset_example/id1": "content1".', + 'NOT-FOUND "packages/data_asset_example/id2".', + ], + handler: (_) { + if (!performRestart) { + return 'q'; + } + // Now we trigger a hot-restart with new assets & new + // application code, we make the build hook now emit also the + // `id2` data asset. + writeAssets(assets, root); + writeHookLibrary(root, assets, available: ['id1', 'id2']); + writeHelperLibrary(root, 'version2', assets.keys.toList()); + return 'R'; + }, + ), + if (performRestart) + Multiple.contains( + [ + // Once the app runs it will print whether it found assets. + // We expect it to having found the new `id2` now. + 'VERSION: version2', + 'FOUND "packages/data_asset_example/id1": "content1".', + + // Flutter web doesn't support new assets on hot-restart atm + // -> See https://github.com/flutter/flutter/issues/137265 + if (isWeb) + 'NOT-FOUND "packages/data_asset_example/id2".' + else + 'FOUND "packages/data_asset_example/id2": "content2".', + if (isWeb) 'Successful hot restart' else 'Hot restart performed', + ], + handler: (_) { + if (!performReload) { + return 'q'; + } + // Now we trigger a hot-reload with new assets & new + // application code, we make the build hook now emit also the + // `id3` data asset (but not `id4`). + assets['id3'] = 'content3'; + assets['id4'] = 'content4'; + writeAssets(assets, root); + writeHookLibrary(root, assets, available: ['id1', 'id2', 'id3']); + writeHelperLibrary(root, 'version3', assets.keys.toList()); + return 'r'; + }, + ), + if (performReload) + Multiple.contains( + [ + // Once the app runs it will print whether it found assets. + 'VERSION: version3', + 'FOUND "packages/data_asset_example/id1": "content1".', + // Flutter web doesn't support new assets on hot-reload atm + // -> See https://github.com/flutter/flutter/issues/137265 + if (isWeb) ...[ + 'NOT-FOUND "packages/data_asset_example/id2".', + 'NOT-FOUND "packages/data_asset_example/id3".', + ] else ...[ + 'FOUND "packages/data_asset_example/id2": "content2".', + 'FOUND "packages/data_asset_example/id3": "content3".', + ], + 'NOT-FOUND "packages/data_asset_example/id4".', + if (isWeb) 'Successful hot restart' else 'Hot reload performed', + ], + handler: (_) { + return 'q'; // quit + }, + ), + Barrier.contains('Application finished.'), + ], + debug: true, + ); + if (result.exitCode != 0) { + throw Exception( + 'flutter run failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}', + ); + } + }); + } + } + + for (final String target in [hostOs, 'web']) { + testWithoutContext('flutter build $target', () async { + final Map assets = {'id1': 'content1', 'id2': 'content2'}; + final List available = ['id1']; + writeAssets(assets, root); + writeHookLibrary(root, assets, available: available); + writeHelperLibrary(root, 'version1', assets.keys.toList()); + + final ProcessTestResult result = await runFlutter( + ['build', '-v', target], + root.path, + [Barrier.contains('Built build/$target')], + debug: true, + ); + if (result.exitCode != 0) { + throw Exception( + 'flutter build failed: ${result.exitCode}\n${result.stderr}\n${result.stdout}', + ); + } + final Directory buildTargetDir = root.childDirectory('build').childDirectory(target); + + final List manifestFiles = + buildTargetDir + .listSync(recursive: true) + .whereType() + .where((File file) => file.path.endsWith('AssetManifest.json')) + .toList(); + + if (manifestFiles.isEmpty) { + throw Exception('Expected a `AssetManifest.json` to be avilable in the $buildTargetDir.'); + } + for (final File manifestFile in manifestFiles) { + final Map manifest = + json.decode(manifestFile.readAsStringSync()) as Map; + for (final String id in available) { + final String key = 'packages/$packageName/$id'; + final List entry = manifest[key]! as List; + expect(entry, equals([key])); + + final File file = manifestFile.parent.childFile(key); + expect(file.readAsStringSync(), assets[id]); + } + } + }); + } + + for (final String target in [hostOs, 'web']) { + testWithoutContext('flutter build $target with conflicting assets', () async { + final Map assets = { + 'id1.txt': 'content1', + 'id2.txt': 'content2', + }; + final List available = ['id1.txt']; + writeAssets(assets, root); + writeAssets(assets, rootDependency); + writeHookLibrary(root, assets, available: available); + writeHookLibrary(rootDependency, assets, available: available); + writeHelperLibrary(root, 'version1', assets.keys.toList()); + + await modifyPubspec(root, (YamlEditor editor) { + editor.update( + ['dependencies', packageNameDependency], + {'path': '../$packageNameDependency'}, + ); + }); + + await modifyPubspec(rootDependency, (YamlEditor editor) { + editor + ..update(['flutter', 'assets'], [assets.keys.first]) + ..update(['dependencies'], {'native_assets_cli': '^0.11.0'}); + }); + + final ProcessTestResult result = await runFlutter( + ['build', '-v', target], + root.path, + [ + Barrier.contains( + 'Conflicting assets: The asset "asset: packages/data_asset_dependency/id1.txt" was declared in the pubspec and the hook', + ), + ], + debug: true, + ); + expect(result.exitCode, isNonZero); + }); + } + }); +} + +Future modifyPubspec(Directory dir, void Function(YamlEditor editor) modify) async { + final File pubspecFile = dir.childFile('pubspec.yaml'); + final String content = await pubspecFile.readAsString(); + final YamlEditor yamlEditor = YamlEditor(content); + modify(yamlEditor); + pubspecFile.writeAsStringSync(yamlEditor.toString()); +} + +Future createDataAssetApp(String packageName, Directory root) async { + await modifyPubspec( + root, + (YamlEditor editor) => + editor.update(['dependencies'], {'native_assets_cli': '^0.11.0'}), + ); + + final File pubspecFile = root.childFile('pubspec.yaml'); + await pinDependencies(pubspecFile); + + final File mainFile = root.childDirectory('lib').childFile('main.dart'); + writeFile(mainFile, ''' + import 'dart:async'; + + import 'package:flutter/material.dart'; + + import 'helper.dart'; + + void main() { + runApp(const MyApp()); + } + + class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + bool first = true; + Timer.periodic(const Duration(seconds: 1), (_) async { + // Delay to give the `flutter run` command time to connect and + // setup `print()` capturing logic (especially on web it won't be + // able to intercept prints until it has connected to DevTools). + if (first) { + await Future.delayed(const Duration(seconds: 5)); + } + dumpAssets(); + }); + + return MaterialApp( + title: 'Flutter Demo', + home: Scaffold( + body: Text('Hello world'), + ), + ); + } + } + '''); + + expect( + await processManager.run([flutterBin, 'pub', 'get'], workingDirectory: root.path), + const ProcessResultMatcher(), + ); +} + +Directory createAppWithName(String packageName, Directory tempDirectory) { + final ProcessResult result = processManager.runSync([ + flutterBin, + 'create', + '--no-pub', + packageName, + ], workingDirectory: tempDirectory.path); + expect(result, const ProcessResultMatcher()); + final Directory packageDirectory = tempDirectory.childDirectory(packageName); + + expect( + processManager.runSync([ + flutterBin, + 'pub', + 'get', + ], workingDirectory: packageDirectory.path), + const ProcessResultMatcher(), + ); + return packageDirectory; +} + +void writeHookLibrary( + Directory root, + Map dataAssets, { + required List available, +}) { + final File hookFile = root.childDirectory('hook').childFile('build.dart'); + available = [for (final String id in available) '"$id"']; + writeFile(hookFile, ''' + import 'package:native_assets_cli/data_assets.dart'; + + void main(List args) async { + await build(args, (BuildInput input, BuildOutputBuilder output) async { + for (final id in $available) { + output.assets.data.add( + DataAsset( + package: input.packageName, + name: id, + file: input.packageRoot.resolve(id), + ), + ); + } + + // This is a workaround for an issue in the + // `package:native_assets_builder` package: + // -> See https://github.com/dart-lang/native/issues/1770 + output.addDependency(input.packageRoot.resolve('hook/build.dart')); + }); + } + '''); +} + +void writeAssets(Map dataAssets, Directory root) { + dataAssets.forEach((String id, String content) { + writeFile(root.childFile(id), content); + }); +} + +void writeHelperLibrary(Directory root, String version, List assetIds) { + assetIds = [for (final String id in assetIds) '"packages/$packageName/$id"']; + final File helperFile = root.childDirectory('lib').childFile('helper.dart'); + writeFile(helperFile, ''' + import 'package:flutter/services.dart' show rootBundle; + + // Only run the code once, but after hot-restart & hot-reload we want to + // run it again. + bool $version = false; + void dumpAssets() async { + if ($version) return; + $version = true; + + final found = {}; + final notFound = []; + for (final String assetId in $assetIds) { + try { + found[assetId] = await rootBundle.loadString(assetId); + } catch (e, s) { + print('EXCEPTION \$e'); + notFound.add(assetId); + } + } + print('VERSION: $version'); + for (final MapEntry(:key, :value) in found.entries) { + print('FOUND "\$key": "\$value".'); + } + for (final id in notFound) { + print('NOT-FOUND "\$id".'); + } + } + '''); +} + +void writeFile(File file, String content) => + file + ..createSync(recursive: true) + ..writeAsStringSync(content); diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart index 2d6499e71b9be..64ad74dd2a654 100644 --- a/packages/flutter_tools/test/src/fakes.dart +++ b/packages/flutter_tools/test/src/fakes.dart @@ -492,6 +492,7 @@ class TestFeatureFlags implements FeatureFlags { this.areCustomDevicesEnabled = false, this.isCliAnimationEnabled = true, this.isNativeAssetsEnabled = false, + this.isDartDataAssetsEnabled = false, this.isSwiftPackageManagerEnabled = false, this.isExplicitPackageDependenciesEnabled = false, }); @@ -526,6 +527,9 @@ class TestFeatureFlags implements FeatureFlags { @override final bool isNativeAssetsEnabled; + @override + final bool isDartDataAssetsEnabled; + @override final bool isSwiftPackageManagerEnabled;