diff --git a/.ci.yaml b/.ci.yaml index 1abbcdcf8d9a3..68e911ea33029 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -164,13 +164,13 @@ platform_properties: ] dependencies: >- [ - {"dependency": "apple_signing", "version": "version:to_2026"} + {"dependency": "apple_signing", "version": "version:to_2027"} ] - os: Mac-15.5|Mac-15.7 + os: Mac-15.7 device_type: none $flutter/osx_sdk : >- { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } mac_arm64: @@ -181,14 +181,14 @@ platform_properties: ] dependencies: >- [ - {"dependency": "apple_signing", "version": "version:to_2026"} + {"dependency": "apple_signing", "version": "version:to_2027"} ] - os: Mac-15.5|Mac-15.7 + os: Mac-15.7 device_type: none cpu: arm64 $flutter/osx_sdk : >- { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } mac_benchmark: @@ -199,16 +199,16 @@ platform_properties: ] dependencies: >- [ - {"dependency": "apple_signing", "version": "version:to_2026"} + {"dependency": "apple_signing", "version": "version:to_2027"} ] device_type: none mac_model: "Macmini8,1" - os: Mac-15.5|Mac-15.7 + os: Mac-15.7 tags: > ["devicelab", "hostonly", "mac"] $flutter/osx_sdk : >- { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } mac_x64: @@ -219,14 +219,14 @@ platform_properties: ] dependencies: >- [ - {"dependency": "apple_signing", "version": "version:to_2026"} + {"dependency": "apple_signing", "version": "version:to_2027"} ] - os: Mac-15.5|Mac-15.7 + os: Mac-15.7 device_type: none cpu: x86 $flutter/osx_sdk : >- { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } mac_build_test: @@ -238,14 +238,14 @@ platform_properties: dependencies: >- [ {"dependency": "ruby", "version": "ruby_3.1-pod_1.13"}, - {"dependency": "apple_signing", "version": "version:to_2026"} + {"dependency": "apple_signing", "version": "version:to_2027"} ] - os: Mac-15.5|Mac-15.7 + os: Mac-15.7 device_type: none cpu: x86 $flutter/osx_sdk : >- { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } mac_mokey: @@ -256,7 +256,7 @@ platform_properties: {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"}, {"dependency": "open_jdk", "version": "version:21"} ] - os: Mac-15.1|Mac-15.5|Mac-15.6|Mac-15.7 + os: Mac-15.7 cpu: x86 device_type: "mokey" @@ -267,7 +267,7 @@ platform_properties: {"dependency": "android_sdk", "version": "version:36v8unmodified"}, {"dependency": "open_jdk", "version": "version:21"} ] - os: Mac-15.1|Mac-15.5|Mac-15.6|Mac-15.7 + os: Mac-15.7 cpu: arm64 device_type: "mokey" @@ -280,13 +280,13 @@ platform_properties: dependencies: >- [ {"dependency": "ruby", "version": "ruby_3.1-pod_1.13"}, - {"dependency": "apple_signing", "version": "version:to_2026"} + {"dependency": "apple_signing", "version": "version:to_2027"} ] - os: Mac-15.1|Mac-15.5|Mac-15.6|Mac-15.7 - device_os: iOS-18 + os: Mac-15.7 + device_os: iOS-18|iOS-26 $flutter/osx_sdk : >- { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } mac_x64_ios: @@ -298,14 +298,14 @@ platform_properties: dependencies: >- [ {"dependency": "ruby", "version": "ruby_3.1-pod_1.13"}, - {"dependency": "apple_signing", "version": "version:to_2026"} + {"dependency": "apple_signing", "version": "version:to_2027"} ] - os: Mac-15.1|Mac-15.5|Mac-15.6|Mac-15.7 + os: Mac-15.7 cpu: x86 - device_os: iOS-18 + device_os: iOS-18|iOS-26 $flutter/osx_sdk : >- { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } mac_arm64_ios: @@ -319,12 +319,12 @@ platform_properties: {"dependency": "ruby", "version": "ruby_3.1-pod_1.13"}, {"dependency": "apple_signing", "version": "none"} ] - os: Mac-15.1|Mac-15.5|Mac-15.6|Mac-15.7 + os: Mac-15.7 cpu: arm64 - device_os: iOS-18 + device_os: iOS-18|iOS-26 $flutter/osx_sdk : >- { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } windows: @@ -1548,13 +1548,13 @@ targets: timeout: 60 properties: add_recipes_cq: "true" - # This makes use of UpsideDownCake, a preview version of android. Preview versions eventually + # This makes use of Baklava, a preview version of android. Preview versions eventually # get removed from the sdk manager, so it is hosted on CIPD to ensure integration testing # doesn't flake when that happens. - # https://chrome-infra-packages.appspot.com/p/flutter/android/sdk/all/linux-amd64/+/version:udcv1 + # https://chrome-infra-packages.appspot.com/p/flutter/android/sdk/all/linux-amd64/+/version:baklava_v1 dependencies: >- [ - {"dependency": "android_sdk", "version": "version:udcv1"}, + {"dependency": "android_sdk", "version": "version:baklava_v1"}, {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, {"dependency": "cmake", "version": "build_id:8787856497187628321"}, {"dependency": "goldctl", "version": "git_revision:031e93819017b95c3f2dfe463189c7b8d02f2f83"}, @@ -3679,6 +3679,7 @@ targets: timeout: 60 properties: add_recipes_cq: "true" + test_timeout_secs: "2700" dependencies: >- [ {"dependency": "android_sdk", "version": "version:36v8unmodified"}, @@ -3697,6 +3698,7 @@ targets: timeout: 60 properties: add_recipes_cq: "true" + test_timeout_secs: "2700" dependencies: >- [ {"dependency": "android_sdk", "version": "version:36v8unmodified"}, @@ -3715,6 +3717,7 @@ targets: timeout: 60 properties: add_recipes_cq: "true" + test_timeout_secs: "2700" dependencies: >- [ {"dependency": "android_sdk", "version": "version:36v8unmodified"}, @@ -3733,6 +3736,7 @@ targets: timeout: 60 properties: add_recipes_cq: "true" + test_timeout_secs: "2700" dependencies: >- [ {"dependency": "android_sdk", "version": "version:36v8unmodified"}, @@ -3751,6 +3755,7 @@ targets: timeout: 60 properties: add_recipes_cq: "true" + test_timeout_secs: "2700" dependencies: >- [ {"dependency": "android_sdk", "version": "version:36v8unmodified"}, @@ -4158,6 +4163,16 @@ targets: properties: task_name: hello_world_macos__compile + - name: Mac hello_world_impeller_macos_sdfs + recipe: devicelab/devicelab_drone + presubmit: false + timeout: 60 + bringup: true + properties: + tags: > + ["devicelab", "mac"] + task_name: hello_world_impeller_macos_sdfs + - name: Mac integration_ui_test_test_macos recipe: devicelab/devicelab_drone presubmit: false @@ -4498,8 +4513,9 @@ targets: - engine/** - DEPS - - name: Mac tool_integration_tests_1_5 + - name: Mac tool_integration_tests_1 recipe: flutter/flutter_drone + bringup: true timeout: 60 properties: cpu: arm64 @@ -4513,10 +4529,9 @@ targets: {"dependency": "goldctl", "version": "git_revision:031e93819017b95c3f2dfe463189c7b8d02f2f83"} ] shard: tool_integration_tests - subshard: "1_5" + subshard: "1_10" tags: > ["framework", "hostonly", "shard", "mac"] - test_timeout_secs: "2700" runIf: - dev/** - packages/flutter_tools/** @@ -4525,8 +4540,9 @@ targets: - engine/** - DEPS - - name: Mac tool_integration_tests_2_5 + - name: Mac tool_integration_tests_2 recipe: flutter/flutter_drone + bringup: true timeout: 60 properties: cpu: arm64 @@ -4540,10 +4556,9 @@ targets: {"dependency": "goldctl", "version": "git_revision:031e93819017b95c3f2dfe463189c7b8d02f2f83"} ] shard: tool_integration_tests - subshard: "2_5" + subshard: "2_10" tags: > ["framework", "hostonly", "shard", "mac"] - test_timeout_secs: "2700" runIf: - dev/** - packages/flutter_tools/** @@ -4552,8 +4567,9 @@ targets: - engine/** - DEPS - - name: Mac tool_integration_tests_3_5 + - name: Mac tool_integration_tests_3 recipe: flutter/flutter_drone + bringup: true timeout: 60 properties: cpu: arm64 @@ -4567,10 +4583,9 @@ targets: {"dependency": "goldctl", "version": "git_revision:031e93819017b95c3f2dfe463189c7b8d02f2f83"} ] shard: tool_integration_tests - subshard: "3_5" + subshard: "3_10" tags: > ["framework", "hostonly", "shard", "mac"] - test_timeout_secs: "2700" runIf: - dev/** - packages/flutter_tools/** @@ -4579,8 +4594,9 @@ targets: - engine/** - DEPS - - name: Mac tool_integration_tests_4_5 + - name: Mac tool_integration_tests_4 recipe: flutter/flutter_drone + bringup: true timeout: 60 properties: cpu: arm64 @@ -4594,10 +4610,9 @@ targets: {"dependency": "goldctl", "version": "git_revision:031e93819017b95c3f2dfe463189c7b8d02f2f83"} ] shard: tool_integration_tests - subshard: "4_5" + subshard: "4_10" tags: > ["framework", "hostonly", "shard", "mac"] - test_timeout_secs: "2700" runIf: - dev/** - packages/flutter_tools/** @@ -4606,8 +4621,9 @@ targets: - engine/** - DEPS - - name: Mac tool_integration_tests_5_5 + - name: Mac tool_integration_tests_5 recipe: flutter/flutter_drone + bringup: true timeout: 60 properties: cpu: arm64 @@ -4621,10 +4637,144 @@ targets: {"dependency": "goldctl", "version": "git_revision:031e93819017b95c3f2dfe463189c7b8d02f2f83"} ] shard: tool_integration_tests - subshard: "5_5" + subshard: "5_10" + tags: > + ["framework", "hostonly", "shard", "mac"] + runIf: + - dev/** + - packages/flutter_tools/** + - bin/** + - .ci.yaml + - engine/** + - DEPS + + - name: Mac tool_integration_tests_6 + recipe: flutter/flutter_drone + bringup: true + timeout: 60 + properties: + cpu: arm64 + add_recipes_cq: "true" + dependencies: >- + [ + {"dependency": "android_sdk", "version": "version:36v9unmodified"}, + {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"}, + {"dependency": "open_jdk", "version": "version:21"}, + {"dependency": "ruby", "version": "ruby_3.1-pod_1.13"}, + {"dependency": "goldctl", "version": "git_revision:c845c41b9b81bfcb11f2f0ab17b5b2386d634c31"} + ] + shard: tool_integration_tests + subshard: "6_10" + tags: > + ["framework", "hostonly", "shard", "mac"] + runIf: + - dev/** + - packages/flutter_tools/** + - bin/** + - .ci.yaml + - engine/** + - DEPS + + - name: Mac tool_integration_tests_7 + recipe: flutter/flutter_drone + bringup: true + timeout: 60 + properties: + cpu: arm64 + add_recipes_cq: "true" + dependencies: >- + [ + {"dependency": "android_sdk", "version": "version:36v9unmodified"}, + {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"}, + {"dependency": "open_jdk", "version": "version:21"}, + {"dependency": "ruby", "version": "ruby_3.1-pod_1.13"}, + {"dependency": "goldctl", "version": "git_revision:c845c41b9b81bfcb11f2f0ab17b5b2386d634c31"} + ] + shard: tool_integration_tests + subshard: "7_10" + tags: > + ["framework", "hostonly", "shard", "mac"] + runIf: + - dev/** + - packages/flutter_tools/** + - bin/** + - .ci.yaml + - engine/** + - DEPS + + - name: Mac tool_integration_tests_8 + recipe: flutter/flutter_drone + bringup: true + timeout: 60 + properties: + cpu: arm64 + add_recipes_cq: "true" + dependencies: >- + [ + {"dependency": "android_sdk", "version": "version:36v9unmodified"}, + {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"}, + {"dependency": "open_jdk", "version": "version:21"}, + {"dependency": "ruby", "version": "ruby_3.1-pod_1.13"}, + {"dependency": "goldctl", "version": "git_revision:c845c41b9b81bfcb11f2f0ab17b5b2386d634c31"} + ] + shard: tool_integration_tests + subshard: "8_10" + tags: > + ["framework", "hostonly", "shard", "mac"] + runIf: + - dev/** + - packages/flutter_tools/** + - bin/** + - .ci.yaml + - engine/** + - DEPS + + - name: Mac tool_integration_tests_9 + recipe: flutter/flutter_drone + bringup: true + timeout: 60 + properties: + cpu: arm64 + add_recipes_cq: "true" + dependencies: >- + [ + {"dependency": "android_sdk", "version": "version:36v9unmodified"}, + {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"}, + {"dependency": "open_jdk", "version": "version:21"}, + {"dependency": "ruby", "version": "ruby_3.1-pod_1.13"}, + {"dependency": "goldctl", "version": "git_revision:c845c41b9b81bfcb11f2f0ab17b5b2386d634c31"} + ] + shard: tool_integration_tests + subshard: "9_10" + tags: > + ["framework", "hostonly", "shard", "mac"] + runIf: + - dev/** + - packages/flutter_tools/** + - bin/** + - .ci.yaml + - engine/** + - DEPS + + - name: Mac tool_integration_tests_10 + recipe: flutter/flutter_drone + bringup: true + timeout: 60 + properties: + cpu: arm64 + add_recipes_cq: "true" + dependencies: >- + [ + {"dependency": "android_sdk", "version": "version:36v9unmodified"}, + {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"}, + {"dependency": "open_jdk", "version": "version:21"}, + {"dependency": "ruby", "version": "ruby_3.1-pod_1.13"}, + {"dependency": "goldctl", "version": "git_revision:c845c41b9b81bfcb11f2f0ab17b5b2386d634c31"} + ] + shard: tool_integration_tests + subshard: "10_10" tags: > ["framework", "hostonly", "shard", "mac"] - test_timeout_secs: "2700" runIf: - dev/** - packages/flutter_tools/** @@ -4793,9 +4943,10 @@ targets: task_name: hello_world_android__compile # mac mokey benchmark - - name: Mac_mokey hot_mode_dev_cycle__benchmark + - name: Mac_arm64_mokey hot_mode_dev_cycle__benchmark recipe: devicelab/devicelab_drone presubmit: false + bringup: true timeout: 60 properties: tags: > @@ -4833,9 +4984,10 @@ targets: task_name: integration_ui_frame_number # mac mokey benchmark - - name: Mac_mokey microbenchmarks + - name: Mac_arm64_mokey microbenchmarks recipe: devicelab/devicelab_drone presubmit: false + bringup: true timeout: 60 properties: tags: > @@ -5452,9 +5604,10 @@ targets: ["devicelab", "ios", "mac"] task_name: hot_mode_dev_cycle_ios__benchmark - - name: Mac_x64 hot_mode_dev_cycle_ios_simulator + - name: Mac_arm64 hot_mode_dev_cycle_ios_simulator recipe: devicelab/devicelab_drone presubmit: false + bringup: true timeout: 60 properties: dependencies: >- @@ -5473,10 +5626,10 @@ targets: tags: > ["devicelab", "ios", "mac"] task_name: ios_debug_workflow - os: Mac-15.5|Mac-15.7 + os: Mac-15.7 $flutter/osx_sdk : >- { - "sdk_version": "17a5295f" + "sdk_version": "17c52" } - name: Mac_ios fullscreen_textfield_perf_ios__e2e_summary @@ -5515,7 +5668,7 @@ targets: ["devicelab", "ios", "mac"] task_name: flutter_gallery__transition_perf_e2e_ios drone_dimensions: > - ["device_os=iOS-18","os=Mac-15.1|Mac-15.5|Mac-15.6|Mac-15.7", "cpu=x86"] + ["device_os=iOS-18|iOS-26","os=Mac-15.7", "cpu=x86"] - name: Mac_ios animated_blur_backdrop_filter_perf_ios__timeline_summary recipe: devicelab/devicelab_drone diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000000..ade159ddabcb9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: ci + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + runs-on: ${{ matrix.os }} + + name: ๐Ÿงช Test + + env: + FLUTTER_STORAGE_BASE_URL: https://download.shorebird.dev + + steps: + - name: ๐Ÿ“š Git Checkout + uses: actions/checkout@v4 + with: + # Fetch all branches and tags to ensure that Flutter can determine its version + fetch-depth: 0 + + - name: ๐ŸŽฏ Setup Dart + uses: dart-lang/setup-dart@v1 + + - name: ๐Ÿ“ฆ Install Dependencies + run: | + dart pub get -C ./dev/bots + dart pub get -C ./dev/tools + + - name: ๐Ÿงช Run Tests + run: dart ./dev/bots/test.dart diff --git a/.github/workflows/freeze.yml b/.github/workflows/freeze.yml index 11c904b149de7..d3f5355f82d4c 100644 --- a/.github/workflows/freeze.yml +++ b/.github/workflows/freeze.yml @@ -20,16 +20,14 @@ jobs: name: Check Code Freeze runs-on: ubuntu-latest if: ${{ github.repository == 'flutter/flutter' }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} + steps: - name: Check for changes in frozen folders + if: github.event_name != 'merge_group' uses: dorny/paths-filter@v3 id: filter with: + token: ${{ github.token }} filters: | frozen: - 'packages/flutter/lib/src/material/**' @@ -46,27 +44,9 @@ jobs: - 'packages/flutter/test_fixes/cupertino/**' - name: Fail on frozen changes - # This step only runs if the 'frozen' filter matched AND the override label is NOT present - if: | - steps.filter.outputs.frozen == 'true' && - !contains(github.event.pull_request.labels.*.name, 'override: code freeze') + if: github.event_name != 'merge_group' && steps.filter.outputs.frozen == 'true' && !contains(github.event.pull_request.labels.*.name, 'override code freeze') run: | echo "Error: Code changes detected during the current code freeze." - echo "The following paths are currently frozen:" - echo " - packages/flutter/lib/src/material/" - echo " - packages/flutter/lib/src/cupertino/" - echo " - (and associated tests/examples)" - echo "" - echo "If this is a critical fix that must land during the freeze, please file an issue for team-design." - echo "Information on this code freeze: https://github.com/flutter/flutter/issues/184093" + echo "If this is a critical fix that must land, please file an issue for team-design." + echo "Info: https://github.com/flutter/flutter/issues/184093" exit 1 - - - name: Pass if overridden - if: | - steps.filter.outputs.frozen == 'true' && - contains(github.event.pull_request.labels.*.name, 'override: code freeze') - run: echo "Code freeze overridden by override: code freeze label." - - - name: Pass if no frozen changes - if: steps.filter.outputs.frozen == 'false' - run: echo "No changes to frozen code." diff --git a/.github/workflows/shorebird_ci.yml b/.github/workflows/shorebird_ci.yml new file mode 100644 index 0000000000000..2d2905405521d --- /dev/null +++ b/.github/workflows/shorebird_ci.yml @@ -0,0 +1,130 @@ +name: shorebird_ci + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + push: + branches: + - shorebird/dev + +jobs: + # NOTE: Windows is not included because shorebird_tests depends on + # flutter_flavorizr which requires Xcode. Additionally, flutter_tools + # tests on Windows would need additional setup work. + + flutter-tools-tests: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + + runs-on: ${{ matrix.os }} + + name: ๐Ÿ› ๏ธ Flutter Tools Tests (${{ matrix.os }}) + + steps: + - name: ๐Ÿ“š Git Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: ๐ŸŽฏ Setup Dart + uses: dart-lang/setup-dart@v1 + + - name: ๐Ÿฆ Run Flutter Tools Tests + run: ../../bin/flutter test test/general.shard + working-directory: packages/flutter_tools + + shorebird-android-tests: + # Android tests run on Ubuntu (faster runners, no macOS needed) + runs-on: ubuntu-latest + + name: ๐Ÿค– Shorebird Android Tests + + steps: + - name: ๐Ÿ“š Git Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: ๐ŸŽฏ Setup Dart + uses: dart-lang/setup-dart@v1 + + - uses: actions/setup-java@v4 + with: + distribution: "zulu" + java-version: "17" + + - name: ๐Ÿ“ฆ Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: ๐Ÿค– Run Android Tests + run: dart test test/base_test.dart test/android_test.dart + working-directory: packages/shorebird_tests + + shorebird-ios-tests: + # iOS tests require macOS for Xcode + runs-on: macos-latest + + name: ๐ŸŽ Shorebird iOS + Android Smoke Tests + + steps: + - name: ๐Ÿ“š Git Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: ๐ŸŽฏ Setup Dart + uses: dart-lang/setup-dart@v1 + + - uses: actions/setup-java@v4 + with: + distribution: "zulu" + java-version: "17" + + - name: ๐Ÿ“ฆ Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: ๐ŸŽ Run iOS Tests + run: dart test test/base_test.dart test/ios_test.dart + working-directory: packages/shorebird_tests + + - name: ๐Ÿค– Run Android Smoke Test (macOS) + # Quick sanity check that Android builds work on macOS too + run: dart test test/android_test.dart --name "can build an apk" + working-directory: packages/shorebird_tests + env: + # Enables streaming subprocess output for debugging timeouts. + VERBOSE: "1" + + # Aggregator required by branch protection on shorebird/dev so the rule can + # list a single context ("ci") instead of every matrix-expanded job above. + ci: + if: always() + needs: + - flutter-tools-tests + - shorebird-android-tests + - shorebird-ios-tests + runs-on: ubuntu-latest + steps: + - if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} + run: | + echo "One or more upstream jobs failed or were cancelled." + exit 1 diff --git a/.github/workflows/sync-engine-version.yml b/.github/workflows/sync-engine-version.yml index ea6b786c74fbe..0b8ab0d9d817c 100644 --- a/.github/workflows/sync-engine-version.yml +++ b/.github/workflows/sync-engine-version.yml @@ -52,9 +52,8 @@ jobs: - name: Check for Changes id: git-check run: | - # -N (intent-to-add) treats untracked files as existing but empty # -f adds even if it's in the .gitignore (which it usually is) - git add -N -f bin/internal/engine.version + git add -f bin/internal/engine.version if git diff --cached --exit-code bin/internal/engine.version; then echo "changed=false" >> $GITHUB_OUTPUT diff --git a/CHANGELOG.md b/CHANGELOG.md index a9bd535641dfd..16249b5c3ef67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,8 +30,58 @@ More information and tips: docs/releases/Hotfix-Documentation-Best-Practices.md --> +## Flutter 3.44 Changes + +### [3.44.7](https://github.com/flutter/flutter/releases/tag/3.44.7) +- [flutter/188161](https://github.com/flutter/flutter/issues/188161) Fixes a resource leak that sometimes caused crashes when using external textures on some GPUs (such as Arm Mali). + +### [3.44.6](https://github.com/flutter/flutter/releases/tag/3.44.6) +- [flutter/187980](https://github.com/flutter/flutter/issues/187980) On Linux, building asset bundles with native assets enabled without a native app build directory crashes due to missing `CMakeCache.txt`. +- [flutter/188805](https://github.com/flutter/flutter/issues/188805) Fixes a crash when running Android instrumented tests. + +### [3.44.5](https://github.com/flutter/flutter/releases/tag/3.44.5) +- [flutter/188718](https://github.com/flutter/flutter/issues/188718) When building Flutter Android apps and add-to-app modules using AGP < 9 with KGP, the tool incorrectly logs Built-in Kotlin migration warnings. +- [flutter/186810](https://github.com/flutter/flutter/issues/186810) [flutter/187388](https://github.com/flutter/flutter/issues/187388) [flutter/187553](https://github.com/flutter/flutter/issues/187553) When building Android app bundles using flavors, or with an old app template combined with a plugin coming alphabetically before app, fixes problems with failing to include libapp.so in the produced app bundle. +- [flutter/188035](https://github.com/flutter/flutter/issues/188035) When rendering text with shadows, the shadows would appear elsewhere on the screen, separate from the text. +- [flutter/187237](https://github.com/flutter/flutter/issues/187237) Fixes a crash that can happen during app shutdown or rotation on some Android devices using Impeller/Vulkan with HCPP. + +### [3.44.4](https://github.com/flutter/flutter/releases/tag/3.44.4) +- [flutter/188192](https://github.com/flutter/flutter/issues/188192) Fix bounds checking in Flutter Linux's FlAccessibleTextField. + +### [3.44.3](https://github.com/flutter/flutter/releases/tag/3.44.3) +- [flutter/186738](https://github.com/flutter/flutter/issues/186738) Demote flavor/platform asset skipping messages from warning to trace level to reduce build log noise. +- [flutter/186899](https://github.com/flutter/flutter/issues/186899) On Android, destroying a texture before the GLES fence that it holds was used may cause a crash. +- [flutter/186953](https://github.com/flutter/flutter/issues/186953) When building concurrently with SwiftPM on macOS/iOS, concurrent directory/file/symlink creation may cause a crash. +- [flutter/183179](https://github.com/flutter/flutter/issues/183179) Fixes an issue that can cause crashes when rendering animated PNG images. + +### [3.44.2](https://github.com/flutter/flutter/releases/tag/3.44.2) +- [flutter/187322](https://github.com/flutter/flutter/issues/187322) When DTD connection is lost or fails at startup during widget-preview on all platforms, the tool crashes with unhandled exceptions instead of exiting gracefully. +- [flutter/186723](https://github.com/flutter/flutter/issues/186723) When Android apps switch from any `SystemUiMode` to edge-to-edge, the system bars unexpectedly remain invisible. +- [flutter/186054](https://github.com/flutter/flutter/issues/186054) When building an iOS or macOS app with Swift Package Manager enabled, remote package dependencies may fail to download. +- [flutter/185775](https://github.com/flutter/flutter/issues/185775) When building for non-iOS or macOS apps, Swift Package Manager warnings may appear. + +### [3.44.1](https://github.com/flutter/flutter/releases/tag/3.44.1) +- [flutter/186962](https://github.com/flutter/flutter/issues/186962) When the analysis server exits unexpectedly, the `flutter` tool can crash instead of outputting a descriptive error message. +- [flutter/186963](https://github.com/flutter/flutter/issues/186963) When failing to connect to a Chrome instance on Windows, the `flutter` tool can crash instead of outputting a descriptive error message. + +### [3.44.0](https://github.com/flutter/flutter/releases/tag/3.44.0) + +Learn about what's new in this release in [the blog post](https://blog.flutter.dev/whats-new-in-flutter-3-44-b0cc1ad3c527), and check out the [CHANGELOG](https://docs.flutter.dev/release/release-notes/release-notes-3.44.0) for a detailed list of all the new changes. + + ## Flutter 3.41 Changes +### [3.41.9](https://github.com/flutter/flutter/releases/tag/3.41.9) +- [flutter/185621](https://github.com/flutter/flutter/pull/185621) Fixes a potential integer overflow that can happen when handling some animated PNG files. + +### [3.41.8](https://github.com/flutter/flutter/releases/tag/3.41.8) +- [flutter/185150](https://github.com/flutter/flutter/issues/185150) When using profile mode on a physical iOS device, the app may fail to connect to the Dart VM. + +### [3.41.7](https://github.com/flutter/flutter/releases/tag/3.41.7) +- [flutter/184376](https://github.com/flutter/flutter/issues/184376) When building an iOS or macOS app, the build may fail due to git multi-pack-index error. +- [flutter/184254](https://github.com/flutter/flutter/issues/184254) When debugging on physical iOS devices and Xcode 26.4+, app often crashes. +- [flutter/184689](https://github.com/flutter/flutter/issues/184689) When using an ffi Windows package, don't require a plugin class. + ### [3.41.6](https://github.com/flutter/flutter/releases/tag/3.41.6) - [flutter/184025](https://github.com/flutter/flutter/pull/184025) Include a fix from Skia that ensures that the correct atlas for the glyph mask format is used consistently. - [flutter/182708](https://github.com/flutter/flutter/issues/182708) Visual issues with circles appearing jagged. Especially on thin stroked circles and circles with small radii. diff --git a/DEPS b/DEPS index 03ace68eac587..52c2d7eb2ddf8 100644 --- a/DEPS +++ b/DEPS @@ -16,6 +16,14 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'skia_revision': 'e9ed4fc9f1544c58d8a9347c1fc9471d8dd7c465', + # Our dart-sdk fork revision. Used both for the third_party/dart source + # clone (gen_snapshot etc.) and to key the macos-arm64 prebuilt object in + # GCS, so source and prebuilt stay the same revision. A published prebuilt + # must exist for this sha (see the macos-arm64 dart-sdk gcs dep below). + "dart_sdk_revision": "db98bdaa9d8f8e2250ff83d24abcaf775807244c", + "dart_sdk_git": "git@github.com:shorebirdtech/dart-sdk.git", + "updater_git": "https://github.com/shorebirdtech/updater.git", + "updater_rev": "1f85c4ab1ee5b540269b9859c75e1bffbb9050c7", # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. @@ -59,11 +67,11 @@ vars = { # updated revision list of existing dependencies. You will need to # gclient sync before and after update deps to ensure all deps are updated. # updated revision list of existing dependencies. - 'dart_revision': 'bd6280c3e8e99d957987abf4c49cc9b6f99971ac', + 'dart_revision': 'd684a576a6aa954ae107a03b2b4e1d61c3bebe93', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py - 'dart_ai_rev': 'f1cddc8e1905b9a5753a93ce451522e564311251', + 'dart_ai_rev': '3d3b7fdaddead83ce262377e5c4ce5b2a843066c', 'dart_binaryen_rev': '58de22cdfd0ccb38ce68632695c0493c587af932', 'dart_boringssl_rev': 'b0ae229f686f2be70689584d1e1c6a727a2cdfab', 'dart_core_rev': '347df4b546f315fc1ff69c6e65f2a023b0263b1d', @@ -284,7 +292,7 @@ deps = { 'https://boringssl.googlesource.com/boringssl.git' + '@' + Var('dart_boringssl_rev'), 'engine/src/flutter/third_party/dart': - Var('dart_git') + '/sdk.git' + '@' + Var('dart_revision'), + Var('dart_sdk_git') + '@' + Var('dart_sdk_revision'), # WARNING: Unused Dart dependencies in the list below till "WARNING:" marker are removed automatically - see create_updated_flutter_deps.py. @@ -304,7 +312,7 @@ deps = { Var('dart_git') + '/core.git' + '@' + Var('dart_core_rev'), 'engine/src/flutter/third_party/dart/third_party/pkg/dart_style': - Var('dart_git') + '/dart_style.git@9b302dd20b4b38979352f21cf515b44617d344ce', + Var('dart_git') + '/dart_style.git@20ab21773a526f480ca5948f32cf8c77e37d0dc9', 'engine/src/flutter/third_party/dart/third_party/pkg/dartdoc': Var('dart_git') + '/dartdoc.git@a57f497ae9df6e6073b5eb7ddf253d0b2eb470c3', @@ -396,14 +404,27 @@ deps = { 'dep_type': 'cipd', 'condition': 'host_os == "mac" and download_dart_sdk' }, + # Consume Shorebird's own published Dart SDK (built from + # shorebirdtech/dart-sdk) from GCS instead of Google's CIPD prebuilt, so + # the engine builds against our Dart fork and skips rebuilding it from + # source (see shards/macos.json: mac-arm64 drops --no-prebuilt-dart-sdk). + # The object is keyed by Var('dart_sdk_revision') so it tracks the same + # fork revision as the source clone above; sha256sum/size_bytes pin the + # specific artifact's content and must be updated whenever that revision + # bumps. tar.gz (not zip) so gclient's extraction preserves the executable + # bit on bin/dart etc. The bot reads this private bucket via a keyless-WIF + # reader SA (shorebirdtech/_build_engine sync.yaml). 'engine/src/flutter/prebuilts/macos-arm64/dart-sdk': { - 'packages': [ + 'bucket': 'shorebird-dart-sdk-prebuilt', + 'objects': [ { - 'package': 'flutter/dart-sdk/mac-arm64', - 'version': 'git_revision:'+Var('dart_revision') + 'object_name': Var('dart_sdk_revision') + '/dart-sdk-darwin-arm64.tar.gz', + 'sha256sum': '7fd7de2d929d6729975bed3cb18ded754fcc023b35f00c61295fb772f63add6c', + 'size_bytes': 205231858, + 'generation': 1781242258444863, } ], - 'dep_type': 'cipd', + 'dep_type': 'gcs', 'condition': 'host_os == "mac" and download_dart_sdk' }, 'engine/src/flutter/prebuilts/windows-x64/dart-sdk': { @@ -481,6 +502,9 @@ deps = { 'engine/src/flutter/third_party/ocmock': Var('flutter_git') + '/third_party/ocmock' + '@' + Var('ocmock_rev'), + 'engine/src/flutter/third_party/updater': + Var('updater_git') + '@' + Var('updater_rev'), + 'engine/src/flutter/third_party/libjpeg-turbo/src': Var('flutter_git') + '/third_party/libjpeg-turbo' + '@' + '0fb821f3b2e570b2783a94ccd9a2fb1f4916ae9f', diff --git a/TESTOWNERS b/TESTOWNERS index 57f294856ce2b..3a332ab17cf30 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -275,6 +275,7 @@ /dev/devicelab/bin/tasks/gradle_plugin_bundle_test.dart @gmackall @flutter/plugin /dev/devicelab/bin/tasks/gradle_plugin_fat_apk_test.dart @gmackall @flutter/plugin /dev/devicelab/bin/tasks/gradle_plugin_light_apk_test.dart @gmackall @flutter/plugin +/dev/devicelab/bin/tasks/hello_world_impeller_macos_sdfs.dart @b-luk @flutter/desktop /dev/devicelab/bin/tasks/hello_world_macos__compile.dart @cbracken @flutter/desktop /dev/devicelab/bin/tasks/hello_world_win_desktop__compile.dart @yaakovschectman @flutter/desktop /dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_simulator.dart @louisehsu @flutter/tool diff --git a/bin/internal/content_aware_hash.sh b/bin/internal/content_aware_hash.sh index dc9013e7db461..63b38e6eaa80a 100755 --- a/bin/internal/content_aware_hash.sh +++ b/bin/internal/content_aware_hash.sh @@ -65,4 +65,10 @@ if [[ "$CURRENT_BRANCH" != "main" && \ fi fi -git -C "$FLUTTER_ROOT" ls-tree "$BASEREF" -- "${TRACKEDFILES[@]}" | git hash-object --stdin +# Workaround for Xcode's git multi-pack-index incompatibility. +# https://github.com/flutter/flutter/issues/184376 +GIT_OPTS=() +if [[ "$(git --version)" == *"Apple Git"* ]]; then + GIT_OPTS=(-c core.multiPackIndex=false) +fi +git "${GIT_OPTS[@]}" -C "$FLUTTER_ROOT" ls-tree "$BASEREF" -- "${TRACKEDFILES[@]}" | git hash-object --stdin diff --git a/bin/internal/engine.version b/bin/internal/engine.version new file mode 100644 index 0000000000000..5d6ef34464a25 --- /dev/null +++ b/bin/internal/engine.version @@ -0,0 +1 @@ +e1eaecbcac6d9a32cb5590c646e21cf21252cf19 diff --git a/bin/internal/release-candidate-branch.version b/bin/internal/release-candidate-branch.version new file mode 100644 index 0000000000000..bf81c486b747c --- /dev/null +++ b/bin/internal/release-candidate-branch.version @@ -0,0 +1 @@ +flutter-3.44-candidate.0 diff --git a/bin/internal/update_dart_sdk.ps1 b/bin/internal/update_dart_sdk.ps1 index 318ef62add853..a588137834eb6 100644 --- a/bin/internal/update_dart_sdk.ps1 +++ b/bin/internal/update_dart_sdk.ps1 @@ -41,7 +41,7 @@ if ((Test-Path $engineStamp) -and ($engineVersion -eq (Get-Content $engineStamp) $dartSdkBaseUrl = $Env:FLUTTER_STORAGE_BASE_URL if (-not $dartSdkBaseUrl) { - $dartSdkBaseUrl = "https://storage.googleapis.com" + $dartSdkBaseUrl = "https://download.shorebird.dev" } if ($engineRealm) { $dartSdkBaseUrl = "$dartSdkBaseUrl/$engineRealm" diff --git a/bin/internal/update_dart_sdk.sh b/bin/internal/update_dart_sdk.sh index 41f419112e1f4..03c56c9e8079c 100755 --- a/bin/internal/update_dart_sdk.sh +++ b/bin/internal/update_dart_sdk.sh @@ -127,7 +127,7 @@ if [ ! -f "$ENGINE_STAMP" ] || [ "$ENGINE_VERSION" != "$(< "$ENGINE_STAMP")" ]; FIND=find fi - DART_SDK_BASE_URL="${FLUTTER_STORAGE_BASE_URL:-https://storage.googleapis.com}${ENGINE_REALM:+/$ENGINE_REALM}" + DART_SDK_BASE_URL="${FLUTTER_STORAGE_BASE_URL:-https://download.shorebird.dev}${ENGINE_REALM:+/$ENGINE_REALM}" DART_SDK_URL="$DART_SDK_BASE_URL/flutter_infra_release/flutter/$ENGINE_VERSION/$DART_ZIP_NAME" # if the sdk path exists, copy it to a temporary location diff --git a/dev/benchmarks/complex_layout/android/project-app.lockfile b/dev/benchmarks/complex_layout/android/project-app.lockfile index 998a52bbd4a60..477b791d07844 100644 --- a/dev/benchmarks/complex_layout/android/project-app.lockfile +++ b/dev/benchmarks/complex_layout/android/project-app.lockfile @@ -29,11 +29,11 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugCompileClassp androidx.profileinstaller:profileinstaller:1.3.1=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.test:rules:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -228,7 +228,7 @@ junit:junit:4.13.1=lintClassPath net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,lintClassPath net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,lintClassPath net.sf.jopt-simple:jopt-simple:4.9=lintClassPath -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,lintClassPath org.apache.ant:ant-antlr:1.10.9=lintClassPath org.apache.ant:ant-junit:1.10.9=lintClassPath org.apache.ant:ant-launcher:1.10.9=lintClassPath diff --git a/dev/benchmarks/complex_layout/android/project-integration_test.lockfile b/dev/benchmarks/complex_layout/android/project-integration_test.lockfile index 063428682c0e1..4c8512281172c 100644 --- a/dev/benchmarks/complex_layout/android/project-integration_test.lockfile +++ b/dev/benchmarks/complex_layout/android/project-integration_test.lockfile @@ -30,11 +30,11 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRu androidx.profileinstaller:profileinstaller:1.3.1=debugAndroidTestRuntimeClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.test:rules:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -231,7 +231,7 @@ net.bytebuddy:byte-buddy:1.14.10=debugUnitTestCompileClasspath,debugUnitTestRunt net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,lintClassPath net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,lintClassPath net.sf.jopt-simple:jopt-simple:4.9=lintClassPath -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,lintClassPath org.apache.ant:ant-antlr:1.10.9=lintClassPath org.apache.ant:ant-junit:1.10.9=lintClassPath org.apache.ant:ant-launcher:1.10.9=lintClassPath diff --git a/dev/benchmarks/macrobenchmarks/android/project-app.lockfile b/dev/benchmarks/macrobenchmarks/android/project-app.lockfile index 33f133092377b..f9ab193f276c3 100644 --- a/dev/benchmarks/macrobenchmarks/android/project-app.lockfile +++ b/dev/benchmarks/macrobenchmarks/android/project-app.lockfile @@ -30,11 +30,15 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugCompileClassp androidx.profileinstaller:profileinstaller:1.3.1=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.2.0=androidTestImplementationDependenciesMetadata +androidx.test.espresso:espresso-core:3.3.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.2.0=androidTestImplementationDependenciesMetadata +androidx.test.espresso:espresso-idling-resource:3.3.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test:monitor:1.2.0=androidTestImplementationDependenciesMetadata +androidx.test:monitor:1.3.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.test:rules:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test:runner:1.2.0=androidTestImplementationDependenciesMetadata +androidx.test:runner:1.3.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -177,7 +181,7 @@ javax.inject:javax.inject:1=_internal-unified-test-platform-android-device-provi junit:junit:4.12=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath,testImplementationDependenciesMetadata net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,androidTestImplementationDependenciesMetadata org.bouncycastle:bcpg-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcpkix-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcprov-jdk18on:1.80=kotlinBouncyCastleConfiguration diff --git a/dev/benchmarks/macrobenchmarks/android/project-integration_test.lockfile b/dev/benchmarks/macrobenchmarks/android/project-integration_test.lockfile index 3e5e0717435e1..c1375c942eccd 100644 --- a/dev/benchmarks/macrobenchmarks/android/project-integration_test.lockfile +++ b/dev/benchmarks/macrobenchmarks/android/project-integration_test.lockfile @@ -30,11 +30,11 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRu androidx.profileinstaller:profileinstaller:1.3.1=debugAndroidTestRuntimeClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.test:rules:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -225,7 +225,7 @@ net.bytebuddy:byte-buddy:1.14.10=debugUnitTestCompileClasspath,debugUnitTestRunt net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.sf.jopt-simple:jopt-simple:4.9=lintClassPath -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,lintClassPath org.apache.commons:commons-compress:1.12=lintClassPath org.apache.httpcomponents:httpclient:4.5.6=lintClassPath org.apache.httpcomponents:httpcore:4.4.10=lintClassPath diff --git a/dev/bots/post_process_docs.dart b/dev/bots/post_process_docs.dart index bfb05542e55e8..c6dffc4e41ad3 100644 --- a/dev/bots/post_process_docs.dart +++ b/dev/bots/post_process_docs.dart @@ -38,7 +38,7 @@ Future postProcess() async { await runProcessWithValidations([ 'curl', '-L', - 'https://storage.googleapis.com/flutter_infra_release/flutter/$revision/api_docs.zip', + 'https://download.shorebird.dev/flutter_infra_release/flutter/$revision/api_docs.zip', '--output', zipDestination, '--fail', diff --git a/dev/bots/suite_runners/run_verify_binaries_codesigned_tests.dart b/dev/bots/suite_runners/run_verify_binaries_codesigned_tests.dart index 2b4db79891f29..d08ecb26ba1b3 100644 --- a/dev/bots/suite_runners/run_verify_binaries_codesigned_tests.dart +++ b/dev/bots/suite_runners/run_verify_binaries_codesigned_tests.dart @@ -14,7 +14,7 @@ import '../utils.dart'; Future verifyCodesignedTestRunner() async { printProgress('${green}Running binaries codesign verification$reset'); - await runCommand('flutter', [ + await runCommand(path.join(flutterRoot, 'bin', 'flutter'), [ 'precache', '--android', '--ios', @@ -23,13 +23,14 @@ Future verifyCodesignedTestRunner() async { await verifyExist(flutterRoot); await verifySignatures(flutterRoot); + await verifyFatBinaries(flutterRoot); } /// Some binaries should always be codesigned, even on master. Verify that they /// are codesigned and have the correct entitlements. Future verifyPreCodesignedTestRunner() async { printProgress('${green}Running binaries codesign verification$reset'); - await runCommand('flutter', [ + await runCommand(path.join(flutterRoot, 'bin', 'flutter'), [ 'precache', '--android', '--ios', @@ -38,6 +39,7 @@ Future verifyPreCodesignedTestRunner() async { await verifyExist(flutterRoot); await verifySignatures(flutterRoot, forRelease: false); + await verifyFatBinaries(flutterRoot); } const List expectedEntitlements = [ @@ -335,6 +337,43 @@ Future verifySignatures( print('Verified that files are codesigned and have expected entitlements.'); } +/// Verify that specific binaries are fat binaries containing both x86_64 and arm64 slices. +Future verifyFatBinaries( + String flutterRoot, { + @visibleForTesting ProcessManager processManager = const LocalProcessManager(), +}) async { + final List fatBinaries = + presignedBinariesWithEntitlements(flutterRoot) + + presignedBinariesWithoutEntitlements(flutterRoot); + final failedBinaries = []; + + for (final binaryPath in fatBinaries) { + print('Verifying fat binary architectures for $binaryPath'); + final io.ProcessResult result = await processManager.run(['file', binaryPath]); + + if (result.exitCode != 0) { + print('Failed to run file command on $binaryPath: \n${result.stderr}'); + failedBinaries.add(binaryPath); + continue; + } + + final output = result.stdout as String; + final bool containsX86_64 = output.contains('x86_64'); + final bool containsArm64 = output.contains('arm64'); + + if (!containsX86_64 || !containsArm64) { + print('Binary $binaryPath is not a fat binary containing both x86_64 and arm64.'); + print('Output: $output'); + failedBinaries.add(binaryPath); + } + } + + if (failedBinaries.isNotEmpty) { + throw Exception('Failed fat binary verification for:\n${failedBinaries.join('\n')}'); + } + print('All expected fat binaries verified.'); +} + /// Find every binary file in the given [rootDirectory]. Future> findBinaryPaths( String rootDirectory, { diff --git a/dev/bots/test/codesign_test.dart b/dev/bots/test/codesign_test.dart index 3378e655b44e0..43169519a770a 100644 --- a/dev/bots/test/codesign_test.dart +++ b/dev/bots/test/codesign_test.dart @@ -282,4 +282,55 @@ void main() async { ); }); }); + + group('verifyFatBinaries', () { + test('succeeds if every binary is fat', () async { + final commandList = []; + final List fatBinaries = + presignedBinariesWithEntitlements(flutterRoot) + + presignedBinariesWithoutEntitlements(flutterRoot); + + for (final binaryPath in fatBinaries) { + commandList.add( + FakeCommand( + command: ['file', binaryPath], + stdout: + 'Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64]', + ), + ); + } + final ProcessManager processManager = FakeProcessManager.list(commandList); + await expectLater(verifyFatBinaries(flutterRoot, processManager: processManager), completes); + }); + + test('fails if a binary is not fat', () async { + final commandList = []; + final List fatBinaries = + presignedBinariesWithEntitlements(flutterRoot) + + presignedBinariesWithoutEntitlements(flutterRoot); + + if (fatBinaries.isNotEmpty) { + commandList.add( + FakeCommand( + command: ['file', fatBinaries.first], + stdout: 'Mach-O 64-bit executable x86_64', + ), + ); + for (final String binaryPath in fatBinaries.skip(1)) { + commandList.add( + FakeCommand( + command: ['file', binaryPath], + stdout: + 'Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64]', + ), + ); + } + } + final ProcessManager processManager = FakeProcessManager.list(commandList); + expect( + () async => verifyFatBinaries(flutterRoot, processManager: processManager), + throwsExceptionWith('Failed fat binary verification for:'), + ); + }); + }); } diff --git a/dev/bots/unpublish_package.dart b/dev/bots/unpublish_package.dart index 022111bd16ed9..03cc3a24d2fab 100644 --- a/dev/bots/unpublish_package.dart +++ b/dev/bots/unpublish_package.dart @@ -23,7 +23,7 @@ import 'package:process/process.dart'; const String gsBase = 'gs://flutter_infra_release'; const String releaseFolder = '/releases'; const String gsReleaseFolder = '$gsBase$releaseFolder'; -const String baseUrl = 'https://storage.googleapis.com/flutter_infra_release'; +const String baseUrl = 'https://download.shorebird.dev/flutter_infra_release'; /// Exception class for when a process fails to run, so we can catch /// it and provide something more readable than a stack trace. diff --git a/dev/devicelab/bin/tasks/hello_world_impeller_macos_sdfs.dart b/dev/devicelab/bin/tasks/hello_world_impeller_macos_sdfs.dart new file mode 100644 index 0000000000000..02b86389c8458 --- /dev/null +++ b/dev/devicelab/bin/tasks/hello_world_impeller_macos_sdfs.dart @@ -0,0 +1,136 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/task_result.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; +import 'package:path/path.dart' as path; +import 'package:xml/xml.dart'; + +Future run() async { + deviceOperatingSystem = DeviceOperatingSystem.macos; + + final Directory appDir = dir(path.join(flutterDirectory.path, 'examples/hello_world')); + final String plistPath = path.join(appDir.path, 'macos', 'Runner', 'Info.plist'); + final plistFile = File(plistPath); + + if (!plistFile.existsSync()) { + return TaskResult.failure('Info.plist not found at $plistPath'); + } + + var res = TaskResult.success(null); + + try { + await inDirectory(appDir, () async { + await flutter('packages', options: ['get']); + + // Step 1: Test without flag + final Process process1 = await startFlutter( + 'run', + options: ['--enable-impeller', '-d', 'macos'], + ); + + final completer1 = Completer(); + var sawMetalMessage = false; + + final StreamSubscription subscription1 = process1.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + print('[STDOUT 1]: $line'); + if (line.contains('Using the Impeller rendering backend (Metal).')) { + sawMetalMessage = true; + if (!completer1.isCompleted) { + completer1.complete(); + } + } + }); + + await Future.any(>[ + completer1.future, + Future.delayed(const Duration(minutes: 2)), + ]); + + process1.stdin.writeln('q'); + await process1.exitCode; + await subscription1.cancel(); + + if (!sawMetalMessage) { + res = TaskResult.failure( + 'Did not see "Using the Impeller rendering backend (Metal)." in output', + ); + return; // Exit early if first step fails + } + + // Step 2: Modify Info.plist to enable SDFS + final String xmlStr = plistFile.readAsStringSync(); + final xmlDoc = XmlDocument.parse(xmlStr); + final XmlElement dictNode = xmlDoc.findAllElements('dict').first; + + dictNode.children.add( + XmlElement(XmlName('key'), [], [ + XmlText('FLTEnableSDFs'), + ], /*isSelfClosing=*/ false), + ); + dictNode.children.add(XmlElement(XmlName('true'))); + + plistFile.writeAsStringSync(xmlDoc.toXmlString(pretty: true, indent: ' ')); + + // Run again with flag + final Process process2 = await startFlutter( + 'run', + options: ['--enable-impeller', '-d', 'macos'], + ); + + final completer2 = Completer(); + var sawSdfsMessage = false; + + final StreamSubscription subscription2 = process2.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + print('[STDOUT 2]: $line'); + if (line.contains('Using the Impeller rendering backend (MetalSDF).')) { + sawSdfsMessage = true; + if (!completer2.isCompleted) { + completer2.complete(); + } + } + }); + + await Future.any(>[ + completer2.future, + Future.delayed(const Duration(minutes: 2)), + ]); + + process2.stdin.writeln('q'); + await process2.exitCode; + await subscription2.cancel(); + + if (!sawSdfsMessage) { + res = TaskResult.failure( + 'Did not see "Using the Impeller rendering backend (MetalSDF)." in output', + ); + } + }); + } catch (e) { + res = TaskResult.failure('Test failed with exception: $e'); + } finally { + // Restore Info.plist + if (plistFile.existsSync()) { + await exec('git', ['checkout', plistPath]); + } + } + + return res; +} + +Future main() async { + await task(run); +} diff --git a/dev/devicelab/bin/tasks/ios_debug_workflow.dart b/dev/devicelab/bin/tasks/ios_debug_workflow.dart index a5a62fb081ceb..371565662bf69 100644 --- a/dev/devicelab/bin/tasks/ios_debug_workflow.dart +++ b/dev/devicelab/bin/tasks/ios_debug_workflow.dart @@ -90,7 +90,7 @@ Future _validateWorkflow({ Pattern expectedLog; Pattern unexpectedLog; const Pattern xcodeExpectedLog = 'Action result status: not yet started'; - final Pattern lldbExpectedLog = RegExp(r'Process .* resuming'); + final Pattern lldbExpectedLog = RegExp(r'location added to breakpoint'); switch (workflow) { case IosDebugWorkflow.xcode: expectedLog = xcodeExpectedLog; diff --git a/dev/integration_tests/android_semantics_testing/android/project-app.lockfile b/dev/integration_tests/android_semantics_testing/android/project-app.lockfile index 5488db5687991..00e124909e0b5 100644 --- a/dev/integration_tests/android_semantics_testing/android/project-app.lockfile +++ b/dev/integration_tests/android_semantics_testing/android/project-app.lockfile @@ -29,11 +29,11 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugCompileClassp androidx.profileinstaller:profileinstaller:1.3.1=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.test:rules:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -221,7 +221,7 @@ junit:junit:4.12=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRun net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.sf.jopt-simple:jopt-simple:4.9=lintClassPath -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,lintClassPath org.apache.commons:commons-compress:1.12=lintClassPath org.apache.httpcomponents:httpclient:4.5.6=lintClassPath org.apache.httpcomponents:httpcore:4.4.10=lintClassPath diff --git a/dev/integration_tests/android_semantics_testing/android/project-integration_test.lockfile b/dev/integration_tests/android_semantics_testing/android/project-integration_test.lockfile index 3e5e0717435e1..c1375c942eccd 100644 --- a/dev/integration_tests/android_semantics_testing/android/project-integration_test.lockfile +++ b/dev/integration_tests/android_semantics_testing/android/project-integration_test.lockfile @@ -30,11 +30,11 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRu androidx.profileinstaller:profileinstaller:1.3.1=debugAndroidTestRuntimeClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.test:rules:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -225,7 +225,7 @@ net.bytebuddy:byte-buddy:1.14.10=debugUnitTestCompileClasspath,debugUnitTestRunt net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.sf.jopt-simple:jopt-simple:4.9=lintClassPath -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,lintClassPath org.apache.commons:commons-compress:1.12=lintClassPath org.apache.httpcomponents:httpclient:4.5.6=lintClassPath org.apache.httpcomponents:httpcore:4.4.10=lintClassPath diff --git a/dev/integration_tests/channels/android/project-app.lockfile b/dev/integration_tests/channels/android/project-app.lockfile index 9efb0cdf68df6..2f001b8a89bfa 100644 --- a/dev/integration_tests/channels/android/project-app.lockfile +++ b/dev/integration_tests/channels/android/project-app.lockfile @@ -29,11 +29,11 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugCompileClassp androidx.profileinstaller:profileinstaller:1.3.1=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.test:rules:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -176,7 +176,7 @@ javax.inject:javax.inject:1=_internal-unified-test-platform-android-device-provi junit:junit:4.12=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib org.bouncycastle:bcpg-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcpkix-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcprov-jdk18on:1.80=kotlinBouncyCastleConfiguration diff --git a/dev/integration_tests/channels/android/project-integration_test.lockfile b/dev/integration_tests/channels/android/project-integration_test.lockfile index 8b8be6c4df035..94f396fc9e790 100644 --- a/dev/integration_tests/channels/android/project-integration_test.lockfile +++ b/dev/integration_tests/channels/android/project-integration_test.lockfile @@ -30,11 +30,11 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRu androidx.profileinstaller:profileinstaller:1.3.1=debugAndroidTestRuntimeClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.test:rules:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -179,7 +179,7 @@ net.bytebuddy:byte-buddy-agent:1.14.10=debugUnitTestCompileClasspath,debugUnitTe net.bytebuddy:byte-buddy:1.14.10=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath,testImplementationDependenciesMetadata net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib org.bouncycastle:bcpg-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcpkix-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcprov-jdk18on:1.80=kotlinBouncyCastleConfiguration diff --git a/dev/integration_tests/display_cutout_rotation/android/project-app.lockfile b/dev/integration_tests/display_cutout_rotation/android/project-app.lockfile index 30de4d3585ef6..a5fa3aef4c0bb 100644 --- a/dev/integration_tests/display_cutout_rotation/android/project-app.lockfile +++ b/dev/integration_tests/display_cutout_rotation/android/project-app.lockfile @@ -29,11 +29,11 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugCompileClassp androidx.profileinstaller:profileinstaller:1.3.1=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.test:rules:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -176,7 +176,7 @@ javax.inject:javax.inject:1=_internal-unified-test-platform-android-device-provi junit:junit:4.12=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib org.bouncycastle:bcpg-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcpkix-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcprov-jdk18on:1.80=kotlinBouncyCastleConfiguration diff --git a/dev/integration_tests/display_cutout_rotation/android/project-integration_test.lockfile b/dev/integration_tests/display_cutout_rotation/android/project-integration_test.lockfile index d9ef0ec5d85da..76281f1f5e461 100644 --- a/dev/integration_tests/display_cutout_rotation/android/project-integration_test.lockfile +++ b/dev/integration_tests/display_cutout_rotation/android/project-integration_test.lockfile @@ -30,11 +30,11 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRu androidx.profileinstaller:profileinstaller:1.3.1=debugAndroidTestRuntimeClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.test:rules:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -179,7 +179,7 @@ net.bytebuddy:byte-buddy-agent:1.14.10=debugUnitTestCompileClasspath,debugUnitTe net.bytebuddy:byte-buddy:1.14.10=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath,testImplementationDependenciesMetadata net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib org.bouncycastle:bcpg-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcpkix-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcprov-jdk18on:1.80=kotlinBouncyCastleConfiguration diff --git a/dev/integration_tests/flavors/android/project-app.lockfile b/dev/integration_tests/flavors/android/project-app.lockfile index afa43f03bbbb3..4461d8d6c3882 100644 --- a/dev/integration_tests/flavors/android/project-app.lockfile +++ b/dev/integration_tests/flavors/android/project-app.lockfile @@ -29,11 +29,11 @@ androidx.loader:loader:1.0.0=freeDebugAndroidTestCompileClasspath,freeDebugCompi androidx.profileinstaller:profileinstaller:1.3.1=freeDebugRuntimeClasspath,freeDebugUnitTestRuntimeClasspath,freeProfileRuntimeClasspath,freeProfileUnitTestRuntimeClasspath,freeReleaseRuntimeClasspath,freeReleaseUnitTestRuntimeClasspath,paidDebugRuntimeClasspath,paidDebugUnitTestRuntimeClasspath,paidProfileRuntimeClasspath,paidProfileUnitTestRuntimeClasspath,paidReleaseRuntimeClasspath,paidReleaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=freeDebugAndroidTestCompileClasspath,freeDebugCompileClasspath,freeDebugRuntimeClasspath,freeDebugUnitTestCompileClasspath,freeDebugUnitTestRuntimeClasspath,freeProfileCompileClasspath,freeProfileRuntimeClasspath,freeProfileUnitTestCompileClasspath,freeProfileUnitTestRuntimeClasspath,freeReleaseCompileClasspath,freeReleaseRuntimeClasspath,freeReleaseUnitTestCompileClasspath,freeReleaseUnitTestRuntimeClasspath,paidDebugAndroidTestCompileClasspath,paidDebugCompileClasspath,paidDebugRuntimeClasspath,paidDebugUnitTestCompileClasspath,paidDebugUnitTestRuntimeClasspath,paidProfileCompileClasspath,paidProfileRuntimeClasspath,paidProfileUnitTestCompileClasspath,paidProfileUnitTestRuntimeClasspath,paidReleaseCompileClasspath,paidReleaseRuntimeClasspath,paidReleaseUnitTestCompileClasspath,paidReleaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=freeDebugAndroidTestCompileClasspath,freeDebugCompileClasspath,freeDebugRuntimeClasspath,freeDebugUnitTestCompileClasspath,freeDebugUnitTestRuntimeClasspath,freeProfileCompileClasspath,freeProfileRuntimeClasspath,freeProfileUnitTestCompileClasspath,freeProfileUnitTestRuntimeClasspath,freeReleaseCompileClasspath,freeReleaseRuntimeClasspath,freeReleaseUnitTestCompileClasspath,freeReleaseUnitTestRuntimeClasspath,paidDebugAndroidTestCompileClasspath,paidDebugCompileClasspath,paidDebugRuntimeClasspath,paidDebugUnitTestCompileClasspath,paidDebugUnitTestRuntimeClasspath,paidProfileCompileClasspath,paidProfileRuntimeClasspath,paidProfileUnitTestCompileClasspath,paidProfileUnitTestRuntimeClasspath,paidReleaseCompileClasspath,paidReleaseRuntimeClasspath,paidReleaseUnitTestCompileClasspath,paidReleaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=freeDebugAndroidTestCompileClasspath,freeDebugCompileClasspath,freeDebugRuntimeClasspath,freeDebugUnitTestCompileClasspath,freeDebugUnitTestRuntimeClasspath,freeProfileCompileClasspath,freeProfileRuntimeClasspath,freeProfileUnitTestCompileClasspath,freeProfileUnitTestRuntimeClasspath,freeReleaseCompileClasspath,freeReleaseRuntimeClasspath,freeReleaseUnitTestCompileClasspath,freeReleaseUnitTestRuntimeClasspath,paidDebugAndroidTestCompileClasspath,paidDebugCompileClasspath,paidDebugRuntimeClasspath,paidDebugUnitTestCompileClasspath,paidDebugUnitTestRuntimeClasspath,paidProfileCompileClasspath,paidProfileRuntimeClasspath,paidProfileUnitTestCompileClasspath,paidProfileUnitTestRuntimeClasspath,paidReleaseCompileClasspath,paidReleaseRuntimeClasspath,paidReleaseUnitTestCompileClasspath,paidReleaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=freeDebugAndroidTestCompileClasspath,freeDebugCompileClasspath,freeDebugRuntimeClasspath,freeDebugUnitTestCompileClasspath,freeDebugUnitTestRuntimeClasspath,freeProfileCompileClasspath,freeProfileRuntimeClasspath,freeProfileUnitTestCompileClasspath,freeProfileUnitTestRuntimeClasspath,freeReleaseCompileClasspath,freeReleaseRuntimeClasspath,freeReleaseUnitTestCompileClasspath,freeReleaseUnitTestRuntimeClasspath,paidDebugAndroidTestCompileClasspath,paidDebugCompileClasspath,paidDebugRuntimeClasspath,paidDebugUnitTestCompileClasspath,paidDebugUnitTestRuntimeClasspath,paidProfileCompileClasspath,paidProfileRuntimeClasspath,paidProfileUnitTestCompileClasspath,paidProfileUnitTestRuntimeClasspath,paidReleaseCompileClasspath,paidReleaseRuntimeClasspath,paidReleaseUnitTestCompileClasspath,paidReleaseUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=freeDebugAndroidTestCompileClasspath,freeDebugCompileClasspath,freeDebugRuntimeClasspath,freeDebugUnitTestCompileClasspath,freeDebugUnitTestRuntimeClasspath,freeProfileCompileClasspath,freeProfileRuntimeClasspath,freeProfileUnitTestCompileClasspath,freeProfileUnitTestRuntimeClasspath,freeReleaseCompileClasspath,freeReleaseRuntimeClasspath,freeReleaseUnitTestCompileClasspath,freeReleaseUnitTestRuntimeClasspath,paidDebugAndroidTestCompileClasspath,paidDebugCompileClasspath,paidDebugRuntimeClasspath,paidDebugUnitTestCompileClasspath,paidDebugUnitTestRuntimeClasspath,paidProfileCompileClasspath,paidProfileRuntimeClasspath,paidProfileUnitTestCompileClasspath,paidProfileUnitTestRuntimeClasspath,paidReleaseCompileClasspath,paidReleaseRuntimeClasspath,paidReleaseUnitTestCompileClasspath,paidReleaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=freeDebugAndroidTestCompileClasspath,freeDebugCompileClasspath,freeDebugRuntimeClasspath,freeDebugUnitTestCompileClasspath,freeDebugUnitTestRuntimeClasspath,freeProfileCompileClasspath,freeProfileRuntimeClasspath,freeProfileUnitTestCompileClasspath,freeProfileUnitTestRuntimeClasspath,freeReleaseCompileClasspath,freeReleaseRuntimeClasspath,freeReleaseUnitTestCompileClasspath,freeReleaseUnitTestRuntimeClasspath,paidDebugAndroidTestCompileClasspath,paidDebugCompileClasspath,paidDebugRuntimeClasspath,paidDebugUnitTestCompileClasspath,paidDebugUnitTestRuntimeClasspath,paidProfileCompileClasspath,paidProfileRuntimeClasspath,paidProfileUnitTestCompileClasspath,paidProfileUnitTestRuntimeClasspath,paidReleaseCompileClasspath,paidReleaseRuntimeClasspath,paidReleaseUnitTestCompileClasspath,paidReleaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=freeDebugAndroidTestCompileClasspath,freeDebugCompileClasspath,freeDebugRuntimeClasspath,freeDebugUnitTestCompileClasspath,freeDebugUnitTestRuntimeClasspath,freeProfileCompileClasspath,freeProfileRuntimeClasspath,freeProfileUnitTestCompileClasspath,freeProfileUnitTestRuntimeClasspath,freeReleaseCompileClasspath,freeReleaseRuntimeClasspath,freeReleaseUnitTestCompileClasspath,freeReleaseUnitTestRuntimeClasspath,paidDebugAndroidTestCompileClasspath,paidDebugCompileClasspath,paidDebugRuntimeClasspath,paidDebugUnitTestCompileClasspath,paidDebugUnitTestRuntimeClasspath,paidProfileCompileClasspath,paidProfileRuntimeClasspath,paidProfileUnitTestCompileClasspath,paidProfileUnitTestRuntimeClasspath,paidReleaseCompileClasspath,paidReleaseRuntimeClasspath,paidReleaseUnitTestCompileClasspath,paidReleaseUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=freeDebugAndroidTestCompileClasspath,freeDebugCompileClasspath,freeDebugRuntimeClasspath,freeDebugUnitTestCompileClasspath,freeDebugUnitTestRuntimeClasspath,freeProfileCompileClasspath,freeProfileRuntimeClasspath,freeProfileUnitTestCompileClasspath,freeProfileUnitTestRuntimeClasspath,freeReleaseCompileClasspath,freeReleaseRuntimeClasspath,freeReleaseUnitTestCompileClasspath,freeReleaseUnitTestRuntimeClasspath,paidDebugAndroidTestCompileClasspath,paidDebugCompileClasspath,paidDebugRuntimeClasspath,paidDebugUnitTestCompileClasspath,paidDebugUnitTestRuntimeClasspath,paidProfileCompileClasspath,paidProfileRuntimeClasspath,paidProfileUnitTestCompileClasspath,paidProfileUnitTestRuntimeClasspath,paidReleaseCompileClasspath,paidReleaseRuntimeClasspath,paidReleaseUnitTestCompileClasspath,paidReleaseUnitTestRuntimeClasspath androidx.test:rules:1.2.0=freeDebugAndroidTestCompileClasspath,freeDebugCompileClasspath,freeDebugRuntimeClasspath,freeDebugUnitTestCompileClasspath,freeDebugUnitTestRuntimeClasspath,freeProfileCompileClasspath,freeProfileRuntimeClasspath,freeProfileUnitTestCompileClasspath,freeProfileUnitTestRuntimeClasspath,freeReleaseCompileClasspath,freeReleaseRuntimeClasspath,freeReleaseUnitTestCompileClasspath,freeReleaseUnitTestRuntimeClasspath,paidDebugAndroidTestCompileClasspath,paidDebugCompileClasspath,paidDebugRuntimeClasspath,paidDebugUnitTestCompileClasspath,paidDebugUnitTestRuntimeClasspath,paidProfileCompileClasspath,paidProfileRuntimeClasspath,paidProfileUnitTestCompileClasspath,paidProfileUnitTestRuntimeClasspath,paidReleaseCompileClasspath,paidReleaseRuntimeClasspath,paidReleaseUnitTestCompileClasspath,paidReleaseUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=freeDebugAndroidTestCompileClasspath,freeDebugCompileClasspath,freeDebugRuntimeClasspath,freeDebugUnitTestCompileClasspath,freeDebugUnitTestRuntimeClasspath,freeProfileCompileClasspath,freeProfileRuntimeClasspath,freeProfileUnitTestCompileClasspath,freeProfileUnitTestRuntimeClasspath,freeReleaseCompileClasspath,freeReleaseRuntimeClasspath,freeReleaseUnitTestCompileClasspath,freeReleaseUnitTestRuntimeClasspath,paidDebugAndroidTestCompileClasspath,paidDebugCompileClasspath,paidDebugRuntimeClasspath,paidDebugUnitTestCompileClasspath,paidDebugUnitTestRuntimeClasspath,paidProfileCompileClasspath,paidProfileRuntimeClasspath,paidProfileUnitTestCompileClasspath,paidProfileUnitTestRuntimeClasspath,paidReleaseCompileClasspath,paidReleaseRuntimeClasspath,paidReleaseUnitTestCompileClasspath,paidReleaseUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=freeDebugAndroidTestCompileClasspath,freeDebugCompileClasspath,freeDebugRuntimeClasspath,freeDebugUnitTestCompileClasspath,freeDebugUnitTestRuntimeClasspath,freeProfileCompileClasspath,freeProfileRuntimeClasspath,freeProfileUnitTestCompileClasspath,freeProfileUnitTestRuntimeClasspath,freeReleaseCompileClasspath,freeReleaseRuntimeClasspath,freeReleaseUnitTestCompileClasspath,freeReleaseUnitTestRuntimeClasspath,paidDebugAndroidTestCompileClasspath,paidDebugCompileClasspath,paidDebugRuntimeClasspath,paidDebugUnitTestCompileClasspath,paidDebugUnitTestRuntimeClasspath,paidProfileCompileClasspath,paidProfileRuntimeClasspath,paidProfileUnitTestCompileClasspath,paidProfileUnitTestRuntimeClasspath,paidReleaseCompileClasspath,paidReleaseRuntimeClasspath,paidReleaseUnitTestCompileClasspath,paidReleaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=freeDebugAndroidTestCompileClasspath,freeDebugCompileClasspath,freeDebugRuntimeClasspath,freeDebugUnitTestCompileClasspath,freeDebugUnitTestRuntimeClasspath,freeProfileCompileClasspath,freeProfileRuntimeClasspath,freeProfileUnitTestCompileClasspath,freeProfileUnitTestRuntimeClasspath,freeReleaseCompileClasspath,freeReleaseRuntimeClasspath,freeReleaseUnitTestCompileClasspath,freeReleaseUnitTestRuntimeClasspath,paidDebugAndroidTestCompileClasspath,paidDebugCompileClasspath,paidDebugRuntimeClasspath,paidDebugUnitTestCompileClasspath,paidDebugUnitTestRuntimeClasspath,paidProfileCompileClasspath,paidProfileRuntimeClasspath,paidProfileUnitTestCompileClasspath,paidProfileUnitTestRuntimeClasspath,paidReleaseCompileClasspath,paidReleaseRuntimeClasspath,paidReleaseUnitTestCompileClasspath,paidReleaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=freeDebugAndroidTestCompileClasspath,freeDebugCompileClasspath,freeDebugRuntimeClasspath,freeDebugUnitTestCompileClasspath,freeDebugUnitTestRuntimeClasspath,freeProfileCompileClasspath,freeProfileRuntimeClasspath,freeProfileUnitTestCompileClasspath,freeProfileUnitTestRuntimeClasspath,freeReleaseCompileClasspath,freeReleaseRuntimeClasspath,freeReleaseUnitTestCompileClasspath,freeReleaseUnitTestRuntimeClasspath,paidDebugAndroidTestCompileClasspath,paidDebugCompileClasspath,paidDebugRuntimeClasspath,paidDebugUnitTestCompileClasspath,paidDebugUnitTestRuntimeClasspath,paidProfileCompileClasspath,paidProfileRuntimeClasspath,paidProfileUnitTestCompileClasspath,paidProfileUnitTestRuntimeClasspath,paidReleaseCompileClasspath,paidReleaseRuntimeClasspath,paidReleaseUnitTestCompileClasspath,paidReleaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=freeDebugAndroidTestCompileClasspath,freeDebugCompileClasspath,freeDebugRuntimeClasspath,freeDebugUnitTestCompileClasspath,freeDebugUnitTestRuntimeClasspath,freeProfileCompileClasspath,freeProfileRuntimeClasspath,freeProfileUnitTestCompileClasspath,freeProfileUnitTestRuntimeClasspath,freeReleaseCompileClasspath,freeReleaseRuntimeClasspath,freeReleaseUnitTestCompileClasspath,freeReleaseUnitTestRuntimeClasspath,paidDebugAndroidTestCompileClasspath,paidDebugCompileClasspath,paidDebugRuntimeClasspath,paidDebugUnitTestCompileClasspath,paidDebugUnitTestRuntimeClasspath,paidProfileCompileClasspath,paidProfileRuntimeClasspath,paidProfileUnitTestCompileClasspath,paidProfileUnitTestRuntimeClasspath,paidReleaseCompileClasspath,paidReleaseRuntimeClasspath,paidReleaseUnitTestCompileClasspath,paidReleaseUnitTestRuntimeClasspath @@ -221,7 +221,7 @@ junit:junit:4.12=freeDebugAndroidTestCompileClasspath,freeDebugCompileClasspath, net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.sf.jopt-simple:jopt-simple:4.9=lintClassPath -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,freeDebugAndroidTestCompileClasspath,freeDebugCompileClasspath,freeDebugRuntimeClasspath,freeDebugUnitTestCompileClasspath,freeDebugUnitTestRuntimeClasspath,freeProfileCompileClasspath,freeProfileRuntimeClasspath,freeProfileUnitTestCompileClasspath,freeProfileUnitTestRuntimeClasspath,freeReleaseCompileClasspath,freeReleaseRuntimeClasspath,freeReleaseUnitTestCompileClasspath,freeReleaseUnitTestRuntimeClasspath,lintClassPath,paidDebugAndroidTestCompileClasspath,paidDebugCompileClasspath,paidDebugRuntimeClasspath,paidDebugUnitTestCompileClasspath,paidDebugUnitTestRuntimeClasspath,paidProfileCompileClasspath,paidProfileRuntimeClasspath,paidProfileUnitTestCompileClasspath,paidProfileUnitTestRuntimeClasspath,paidReleaseCompileClasspath,paidReleaseRuntimeClasspath,paidReleaseUnitTestCompileClasspath,paidReleaseUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,lintClassPath org.apache.commons:commons-compress:1.12=lintClassPath org.apache.httpcomponents:httpclient:4.5.6=lintClassPath org.apache.httpcomponents:httpcore:4.4.10=lintClassPath diff --git a/dev/integration_tests/flavors/android/project-integration_test.lockfile b/dev/integration_tests/flavors/android/project-integration_test.lockfile index 8b8be6c4df035..94f396fc9e790 100644 --- a/dev/integration_tests/flavors/android/project-integration_test.lockfile +++ b/dev/integration_tests/flavors/android/project-integration_test.lockfile @@ -30,11 +30,11 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRu androidx.profileinstaller:profileinstaller:1.3.1=debugAndroidTestRuntimeClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.test:rules:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -179,7 +179,7 @@ net.bytebuddy:byte-buddy-agent:1.14.10=debugUnitTestCompileClasspath,debugUnitTe net.bytebuddy:byte-buddy:1.14.10=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath,testImplementationDependenciesMetadata net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib org.bouncycastle:bcpg-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcpkix-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcprov-jdk18on:1.80=kotlinBouncyCastleConfiguration diff --git a/dev/integration_tests/flutter_gallery/android/project-app.lockfile b/dev/integration_tests/flutter_gallery/android/project-app.lockfile index 96c190f6460b8..671974815eb05 100644 --- a/dev/integration_tests/flutter_gallery/android/project-app.lockfile +++ b/dev/integration_tests/flutter_gallery/android/project-app.lockfile @@ -49,15 +49,15 @@ androidx.profileinstaller:profileinstaller:1.3.1=debugRuntimeClasspath,debugUnit androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.test.espresso:espresso-core:3.1.1=androidTestImplementationDependenciesMetadata -androidx.test.espresso:espresso-core:3.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.test.espresso:espresso-idling-resource:3.1.1=androidTestImplementationDependenciesMetadata -androidx.test.espresso:espresso-idling-resource:3.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.test:monitor:1.1.1=androidTestImplementationDependenciesMetadata -androidx.test:monitor:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.test:rules:1.1.1=androidTestImplementationDependenciesMetadata androidx.test:rules:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.test:runner:1.1.1=androidTestImplementationDependenciesMetadata -androidx.test:runner:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -198,7 +198,7 @@ junit:junit:4.12=androidTestImplementationDependenciesMetadata,debugAndroidTestC junit:junit:4.13=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath,testImplementationDependenciesMetadata net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,androidTestImplementationDependenciesMetadata org.bouncycastle:bcpg-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcpkix-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcprov-jdk18on:1.80=kotlinBouncyCastleConfiguration diff --git a/dev/integration_tests/flutter_gallery/android/project-integration_test.lockfile b/dev/integration_tests/flutter_gallery/android/project-integration_test.lockfile index 8b8be6c4df035..94f396fc9e790 100644 --- a/dev/integration_tests/flutter_gallery/android/project-integration_test.lockfile +++ b/dev/integration_tests/flutter_gallery/android/project-integration_test.lockfile @@ -30,11 +30,11 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRu androidx.profileinstaller:profileinstaller:1.3.1=debugAndroidTestRuntimeClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.test:rules:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -179,7 +179,7 @@ net.bytebuddy:byte-buddy-agent:1.14.10=debugUnitTestCompileClasspath,debugUnitTe net.bytebuddy:byte-buddy:1.14.10=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath,testImplementationDependenciesMetadata net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib org.bouncycastle:bcpg-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcpkix-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcprov-jdk18on:1.80=kotlinBouncyCastleConfiguration diff --git a/dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/settings.gradle b/dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/settings.gradle index 5368feb7ecde7..66364cdf4a190 100644 --- a/dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/settings.gradle +++ b/dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/settings.gradle @@ -7,7 +7,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - def flutterStorageUrl = System.getenv("FLUTTER_STORAGE_BASE_URL") ?: "https://storage.googleapis.com" + def flutterStorageUrl = System.getenv("FLUTTER_STORAGE_BASE_URL") ?: "https://download.shorebird.dev" maven { url = uri("$flutterStorageUrl/download.flutter.io") } diff --git a/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/settings.gradle.kts b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/settings.gradle.kts index f6d75bce11757..da25b49a46f7f 100644 --- a/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/settings.gradle.kts +++ b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/settings.gradle.kts @@ -16,7 +16,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - val flutterStorageUrl = System.getenv("FLUTTER_STORAGE_BASE_URL") ?: "https://storage.googleapis.com" + val flutterStorageUrl = System.getenv("FLUTTER_STORAGE_BASE_URL") ?: "https://download.shorebird.dev" maven("$flutterStorageUrl/download.flutter.io") } } diff --git a/dev/integration_tests/release_smoke_test/android/project-app.lockfile b/dev/integration_tests/release_smoke_test/android/project-app.lockfile index e06ca99236078..69e855384af5a 100644 --- a/dev/integration_tests/release_smoke_test/android/project-app.lockfile +++ b/dev/integration_tests/release_smoke_test/android/project-app.lockfile @@ -30,11 +30,15 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugCompileClassp androidx.profileinstaller:profileinstaller:1.3.1=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.2.0=androidTestImplementationDependenciesMetadata +androidx.test.espresso:espresso-core:3.3.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.2.0=androidTestImplementationDependenciesMetadata +androidx.test.espresso:espresso-idling-resource:3.3.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test:monitor:1.2.0=androidTestImplementationDependenciesMetadata +androidx.test:monitor:1.3.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.test:rules:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test:runner:1.2.0=androidTestImplementationDependenciesMetadata +androidx.test:runner:1.3.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -224,7 +228,7 @@ junit:junit:4.13=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,pro net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.sf.jopt-simple:jopt-simple:4.9=lintClassPath -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,androidTestImplementationDependenciesMetadata,lintClassPath org.apache.commons:commons-compress:1.12=lintClassPath org.apache.httpcomponents:httpclient:4.5.6=lintClassPath org.apache.httpcomponents:httpcore:4.4.10=lintClassPath diff --git a/dev/integration_tests/release_smoke_test/android/project-integration_test.lockfile b/dev/integration_tests/release_smoke_test/android/project-integration_test.lockfile index 3e5e0717435e1..c1375c942eccd 100644 --- a/dev/integration_tests/release_smoke_test/android/project-integration_test.lockfile +++ b/dev/integration_tests/release_smoke_test/android/project-integration_test.lockfile @@ -30,11 +30,11 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRu androidx.profileinstaller:profileinstaller:1.3.1=debugAndroidTestRuntimeClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.test:rules:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -225,7 +225,7 @@ net.bytebuddy:byte-buddy:1.14.10=debugUnitTestCompileClasspath,debugUnitTestRunt net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.sf.jopt-simple:jopt-simple:4.9=lintClassPath -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,lintClassPath org.apache.commons:commons-compress:1.12=lintClassPath org.apache.httpcomponents:httpclient:4.5.6=lintClassPath org.apache.httpcomponents:httpcore:4.4.10=lintClassPath diff --git a/dev/integration_tests/spell_check/android/project-app.lockfile b/dev/integration_tests/spell_check/android/project-app.lockfile index 460ade520ad19..03c7ca3c0e05b 100644 --- a/dev/integration_tests/spell_check/android/project-app.lockfile +++ b/dev/integration_tests/spell_check/android/project-app.lockfile @@ -29,11 +29,11 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugCompileClassp androidx.profileinstaller:profileinstaller:1.3.1=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.test:rules:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -176,7 +176,7 @@ javax.inject:javax.inject:1=_internal-unified-test-platform-android-device-provi junit:junit:4.12=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib org.bouncycastle:bcpg-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcpkix-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcprov-jdk18on:1.80=kotlinBouncyCastleConfiguration diff --git a/dev/integration_tests/spell_check/android/project-integration_test.lockfile b/dev/integration_tests/spell_check/android/project-integration_test.lockfile index 8b8be6c4df035..94f396fc9e790 100644 --- a/dev/integration_tests/spell_check/android/project-integration_test.lockfile +++ b/dev/integration_tests/spell_check/android/project-integration_test.lockfile @@ -30,11 +30,11 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRu androidx.profileinstaller:profileinstaller:1.3.1=debugAndroidTestRuntimeClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.test:rules:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -179,7 +179,7 @@ net.bytebuddy:byte-buddy-agent:1.14.10=debugUnitTestCompileClasspath,debugUnitTe net.bytebuddy:byte-buddy:1.14.10=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath,testImplementationDependenciesMetadata net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib org.bouncycastle:bcpg-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcpkix-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcprov-jdk18on:1.80=kotlinBouncyCastleConfiguration diff --git a/dev/integration_tests/ui/android/project-app.lockfile b/dev/integration_tests/ui/android/project-app.lockfile index 35c7a8b4dba49..f6d8c2a157ba8 100644 --- a/dev/integration_tests/ui/android/project-app.lockfile +++ b/dev/integration_tests/ui/android/project-app.lockfile @@ -29,11 +29,11 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugCompileClassp androidx.profileinstaller:profileinstaller:1.3.1=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.test:rules:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -175,7 +175,7 @@ javax.inject:javax.inject:1=_internal-unified-test-platform-android-device-provi junit:junit:4.12=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib org.bouncycastle:bcpg-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcpkix-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcprov-jdk18on:1.80=kotlinBouncyCastleConfiguration diff --git a/dev/integration_tests/ui/android/project-integration_test.lockfile b/dev/integration_tests/ui/android/project-integration_test.lockfile index 8b8be6c4df035..94f396fc9e790 100644 --- a/dev/integration_tests/ui/android/project-integration_test.lockfile +++ b/dev/integration_tests/ui/android/project-integration_test.lockfile @@ -30,11 +30,11 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRu androidx.profileinstaller:profileinstaller:1.3.1=debugAndroidTestRuntimeClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.test:rules:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -179,7 +179,7 @@ net.bytebuddy:byte-buddy-agent:1.14.10=debugUnitTestCompileClasspath,debugUnitTe net.bytebuddy:byte-buddy:1.14.10=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath,testImplementationDependenciesMetadata net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib org.bouncycastle:bcpg-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcpkix-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcprov-jdk18on:1.80=kotlinBouncyCastleConfiguration diff --git a/dev/tools/create_api_docs.dart b/dev/tools/create_api_docs.dart index 47d336587ca0a..6c65430bf6973 100644 --- a/dev/tools/create_api_docs.dart +++ b/dev/tools/create_api_docs.dart @@ -923,8 +923,8 @@ class PlatformDocGenerator { for (final String platform in kPlatformDocs.keys) { final String zipFile = kPlatformDocs[platform]!.zipName; - final url = - 'https://storage.googleapis.com/${realm}flutter_infra_release/flutter/$engineRevision/$zipFile'; + final String url = + 'https://download.shorebird.dev/${realm}flutter_infra_release/flutter/$engineRevision/$zipFile'; await _extractDocs(url, platform, kPlatformDocs[platform]!, outputDir); } } diff --git a/engine/src/build/config/compiler/BUILD.gn b/engine/src/build/config/compiler/BUILD.gn index e06cf625abe8f..a7f5a3ae35bb9 100644 --- a/engine/src/build/config/compiler/BUILD.gn +++ b/engine/src/build/config/compiler/BUILD.gn @@ -443,7 +443,10 @@ config("compiler") { # Example PR: https://github.com/dart-lang/native/pull/1615 ldflags += [ "-Wl,--no-undefined", - "-Wl,--exclude-libs,ALL", + + # TODO: Terrible hack, but otherwise libupdater.a symbols can't + # be exported from libflutter.so, even when added to android_exports.lst. + # "-Wl,--exclude-libs,ALL", # Enable identical code folding to reduce size. "-Wl,--icf=all", @@ -646,6 +649,8 @@ config("runtime_library") { ldflags += [ "-Wl,--warn-shared-textrel" ] libs += [ + # Rust requires libunwind. + "unwind", "c", "dl", "m", @@ -660,6 +665,9 @@ config("runtime_library") { } else if (current_cpu == "x86") { current_android_cpu = "i686" } + # libunwind.a is located in the respective android cpu subdirectories. + # The clang version needs to match the version in the lib_dirs line above. + lib_dirs += [ "${android_toolchain_root}/lib/clang/19/lib/linux/${current_android_cpu}/" ] libs += [ "clang_rt.builtins-${current_android_cpu}-android" ] } diff --git a/engine/src/build/toolchain/win/BUILD.gn b/engine/src/build/toolchain/win/BUILD.gn index 45a98b1ecd64b..b5073fb22277b 100644 --- a/engine/src/build/toolchain/win/BUILD.gn +++ b/engine/src/build/toolchain/win/BUILD.gn @@ -204,8 +204,11 @@ template("msvc_toolchain") { expname = "${dllname}.exp" pdbname = "${dllname}.pdb" rspfile = "${dllname}.rsp" + # .def files are used to export symbols from the DLL. This arg will be + # removed by the python tool wrapper if the .def file doesn't exist. + deffile = "${dllname}.def" - link_command = "\"$python_path\" $tool_wrapper_path link-wrapper $env False link.exe /nologo /IMPLIB:$libname /DLL /OUT:$dllname /PDB:${dllname}.pdb @$rspfile" + link_command = "\"$python_path\" $tool_wrapper_path link-wrapper $env False link.exe /nologo /IMPLIB:$libname /DLL /OUT:$dllname /PDB:${dllname}.pdb /DEF:$deffile @$rspfile" # TODO(brettw) support manifests #manifest_command = "\"$python_path\" $tool_wrapper_path manifest-wrapper $env mt.exe -nologo -manifest $manifests -out:${dllname}.manifest" diff --git a/engine/src/build/toolchain/win/tool_wrapper.py b/engine/src/build/toolchain/win/tool_wrapper.py index b4fc8485ffa17..7866c6122d832 100644 --- a/engine/src/build/toolchain/win/tool_wrapper.py +++ b/engine/src/build/toolchain/win/tool_wrapper.py @@ -121,6 +121,17 @@ def ExecLinkWrapper(self, arch, use_separate_mspdbsrv, *args): self._UseSeparateMspdbsrv(env, args) if sys.platform == 'win32': args = list(args) # *args is a tuple by default, which is read-only. + + # Remove the /DEF arg if not provided. We would ideally be able to do this + # in build\toolchain\win\BUILD.gn, but there doesn't seem to be a way to + # conditionally add args to the command line based on whether a file exists + # or not, so we do it here instead. + def_arg_prefix = "/DEF:" + for arg in args: + if arg.startswith(def_arg_prefix): + def_file = arg[len(def_arg_prefix):] + if not os.path.exists(def_file): + args.remove(arg) args[0] = args[0].replace('/', '\\') # https://docs.python.org/2/library/subprocess.html: # "On Unix with shell=True [...] if args is a sequence, the first item diff --git a/engine/src/flutter/.ci.yaml b/engine/src/flutter/.ci.yaml index 9603e82f95834..12a9d757a2525 100644 --- a/engine/src/flutter/.ci.yaml +++ b/engine/src/flutter/.ci.yaml @@ -30,10 +30,10 @@ platform_properties: {"dependency": "open_jdk", "version": "version:21"} ] device_type: none - os: Mac-15.5|Mac-15.7 + os: Mac-15.7 $flutter/osx_sdk : >- { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } windows: properties: @@ -465,12 +465,12 @@ targets: ] $flutter/osx_sdk : >- { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } # Do not remove(https://github.com/flutter/flutter/issues/144644) # Scheduler will fail to get the platform drone_dimensions: - - os=Mac-15.5|Mac-15.7 + - os=Mac-15.7 - name: Mac clangd recipe: engine_v2/builder @@ -494,12 +494,12 @@ targets: config_name: mac_ios_engine $flutter/osx_sdk : >- { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } # Do not remove(https://github.com/flutter/flutter/issues/144644) # Scheduler will fail to get the platform drone_dimensions: - - os=Mac-15.5|Mac-15.7 + - os=Mac-15.7 - cpu=x86 - name: Mac mac_ios_engine_ddm @@ -514,12 +514,12 @@ targets: config_name: mac_ios_engine_ddm $flutter/osx_sdk : >- { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } # Do not remove(https://github.com/flutter/flutter/issues/144644) # Scheduler will fail to get the platform drone_dimensions: - - os=Mac-15.5|Mac-15.7 + - os=Mac-15.7 - cpu=x86 - name: Linux windows_android_aot_engine diff --git a/engine/src/flutter/BUILD.gn b/engine/src/flutter/BUILD.gn index 7a4d1eaab385b..d3ca9a9b38f39 100644 --- a/engine/src/flutter/BUILD.gn +++ b/engine/src/flutter/BUILD.gn @@ -119,6 +119,9 @@ group("flutter") { # path_ops "//flutter/tools/path_ops", + + # Built alongside gen_snapshot arm64 targets. + "$dart_src/runtime/bin:analyze_snapshot", ] if (host_os == "linux" || host_os == "mac") { @@ -128,13 +131,6 @@ group("flutter") { ] } - if (host_os == "linux") { - public_deps += [ - # Built alongside gen_snapshot for 64 bit targets - "$dart_src/runtime/bin:analyze_snapshot", - ] - } - if (full_dart_sdk) { public_deps += [ "//flutter/web_sdk" ] } @@ -214,6 +210,7 @@ group("unittests") { "//flutter/runtime:no_dart_plugin_registrant_unittests", "//flutter/runtime:runtime_unittests", "//flutter/shell/common:shell_unittests", + "//flutter/shell/common/shorebird:shorebird_unittests", "//flutter/shell/geometry:geometry_unittests", "//flutter/shell/gpu:gpu_surface_unittests", "//flutter/shell/platform/embedder:embedder_a11y_unittests", @@ -345,3 +342,19 @@ if (host_os == "win") { outputs = [ "$root_build_dir/gen_snapshot/gen_snapshot.exe" ] } } + +# A top-level target for analyze_snapshot, modeled after the gen_snapshot +# target above. +if (host_os == "win") { + _analyze_snapshot_target = + "$dart_src/runtime/bin:analyze_snapshot($host_toolchain)" + + copy("analyze_snapshot") { + deps = [ _analyze_snapshot_target ] + + analyze_snapshot_out_dir = + get_label_info(_analyze_snapshot_target, "root_out_dir") + sources = [ "$analyze_snapshot_out_dir/analyze_snapshot.exe" ] + outputs = [ "$root_build_dir/analyze_snapshot/analyze_snapshot.exe" ] + } +} diff --git a/engine/src/flutter/build/archives/BUILD.gn b/engine/src/flutter/build/archives/BUILD.gn index 4f902010d21dc..55700433541db 100644 --- a/engine/src/flutter/build/archives/BUILD.gn +++ b/engine/src/flutter/build/archives/BUILD.gn @@ -45,6 +45,7 @@ generated_file("artifacts_entitlement_config") { if (build_engine_artifacts) { zip_bundle("artifacts") { deps = [ + "$dart_src/runtime/bin:analyze_snapshot", "$dart_src/runtime/bin:gen_snapshot", "//flutter/flutter_frontend_server:frontend_server", "//flutter/impeller/compiler:impellerc", @@ -142,6 +143,10 @@ if (build_engine_artifacts) { if (host_os == "mac") { deps += [ ":artifacts_entitlement_config" ] files += [ + { + source = "$root_out_dir/analyze_snapshot$exe" + destination = "analyze_snapshot$exe" + }, { source = "$target_gen_dir/entitlements.txt" destination = "entitlements.txt" @@ -320,14 +325,25 @@ if (is_mac) { } if (host_os == "win") { + # This rule archives both gen_snapshot *and* analyze_snapshot. The name is + # misleading. We (shorebird) have updated this rule to include + # analyze_snapshot but did not update the name because it is referenced + # elsewhere in the tooling. zip_bundle("archive_win_gen_snapshot") { - deps = [ "//flutter:gen_snapshot" ] + deps = [ + "//flutter:analyze_snapshot", + "//flutter:gen_snapshot", + ] output = "$full_target_platform_name-$flutter_runtime_mode/windows-x64.zip" files = [ { source = "$root_out_dir/gen_snapshot/gen_snapshot.exe" destination = "gen_snapshot.exe" }, + { + source = "$root_out_dir/analyze_snapshot/analyze_snapshot.exe" + destination = "analyze_snapshot.exe" + }, ] } } diff --git a/engine/src/flutter/build/dart/tools/dart_pkg.py b/engine/src/flutter/build/dart/tools/dart_pkg.py index c60e3e02431e4..5b3e0a8b5f3ee 100755 --- a/engine/src/flutter/build/dart/tools/dart_pkg.py +++ b/engine/src/flutter/build/dart/tools/dart_pkg.py @@ -163,7 +163,10 @@ def main(): for source in args.package_sources: relative_source = os.path.relpath(source, common_source_prefix) target = os.path.join(target_dir, relative_source) - copy(source, target) + try: + copy(source, target) + except shutil.SameFileError: + pass # Copy sdk-ext sources into pkg directory sdk_ext_dir = os.path.join(target_dir, 'sdk_ext') @@ -179,7 +182,10 @@ def main(): for source in args.sdk_ext_files: relative_source = os.path.relpath(source, common_source_prefix) target = os.path.join(sdk_ext_dir, relative_source) - copy(source, target) + try: + copy(source, target) + except shutil.SameFileError: + pass # Write stamp file. with open(args.stamp_file, 'w'): diff --git a/engine/src/flutter/build/zip_bundle.gni b/engine/src/flutter/build/zip_bundle.gni index 51e72df0ad854..707ab046d3637 100644 --- a/engine/src/flutter/build/zip_bundle.gni +++ b/engine/src/flutter/build/zip_bundle.gni @@ -55,7 +55,7 @@ template("zip_bundle") { license_path = rebase_path("//flutter/sky/packages/sky_engine/LICENSE", "//flutter") git_url = "https://github.com/flutter/engine/tree/$engine_version" - sky_engine_url = "https://storage.googleapis.com/flutter_infra_release/flutter/$engine_version/sky_engine.zip" + sky_engine_url = "https://download.shorebird.dev/flutter_infra_release/flutter/$engine_version/sky_engine.zip" outputs = [ license_readme ] contents = [ "# $target_name", diff --git a/engine/src/flutter/ci/builders/linux_web_engine_test.json b/engine/src/flutter/ci/builders/linux_web_engine_test.json index 26de45b6fe241..e818bc9064788 100644 --- a/engine/src/flutter/ci/builders/linux_web_engine_test.json +++ b/engine/src/flutter/ci/builders/linux_web_engine_test.json @@ -493,7 +493,7 @@ "recipe": "engine_v2/tester_engine", "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { diff --git a/engine/src/flutter/ci/builders/local_engine.json b/engine/src/flutter/ci/builders/local_engine.json index 4aa63478e8e7b..7e7789cddcc9f 100644 --- a/engine/src/flutter/ci/builders/local_engine.json +++ b/engine/src/flutter/ci/builders/local_engine.json @@ -3,7 +3,7 @@ { "cas_archive": false, "drone_dimensions": [ - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "device_type=none" ], "gclient_variables": { @@ -29,14 +29,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "cas_archive": false, "drone_dimensions": [ - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "device_type=none" ], "gclient_variables": { @@ -63,14 +63,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "cas_archive": false, "drone_dimensions": [ - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "device_type=none" ], "gclient_variables": { @@ -96,14 +96,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "cas_archive": false, "drone_dimensions": [ - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "device_type=none" ], "gclient_variables": { @@ -129,14 +129,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "cas_archive": false, "drone_dimensions": [ - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "device_type=none" ], "gclient_variables": { @@ -163,14 +163,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "cas_archive": false, "drone_dimensions": [ - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "device_type=none" ], "gclient_variables": { @@ -199,14 +199,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "cas_archive": false, "drone_dimensions": [ - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "device_type=none" ], "gclient_variables": { @@ -231,7 +231,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, @@ -370,7 +370,7 @@ { "cas_archive": false, "drone_dimensions": [ - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "device_type=none" ], "gclient_variables": { @@ -396,14 +396,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "cas_archive": false, "drone_dimensions": [ - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "device_type=none" ], "gclient_variables": { @@ -429,14 +429,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "cas_archive": false, "drone_dimensions": [ - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "device_type=none" ], "gclient_variables": { @@ -461,7 +461,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, @@ -520,7 +520,7 @@ { "cas_archive": false, "drone_dimensions": [ - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "device_type=none" ], "gclient_variables": { @@ -545,7 +545,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, @@ -604,7 +604,7 @@ { "cas_archive": false, "drone_dimensions": [ - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "device_type=none" ], "gclient_variables": { @@ -629,14 +629,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "cas_archive": false, "drone_dimensions": [ - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "device_type=none" ], "gclient_variables": { @@ -662,14 +662,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "cas_archive": false, "drone_dimensions": [ - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "device_type=none" ], "gclient_variables": { @@ -695,14 +695,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "cas_archive": false, "drone_dimensions": [ - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "device_type=none" ], "gclient_variables": { @@ -728,7 +728,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, @@ -841,7 +841,7 @@ { "cas_archive": false, "drone_dimensions": [ - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "device_type=none" ], "gclient_variables": { @@ -866,7 +866,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, @@ -925,7 +925,7 @@ { "cas_archive": false, "drone_dimensions": [ - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "device_type=none" ], "gclient_variables": { @@ -950,7 +950,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, @@ -1063,7 +1063,7 @@ "name": "macos/wasm_release", "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7" + "os=Mac-15.7" ], "gclient_variables": { "download_android_deps": false, @@ -1093,7 +1093,7 @@ "name": "macos/wasm_debug_unopt", "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7" + "os=Mac-15.7" ], "gclient_variables": { "download_android_deps": false, diff --git a/engine/src/flutter/ci/builders/mac_android_aot_engine.json b/engine/src/flutter/ci/builders/mac_android_aot_engine.json index bec77665a0480..a71ef35738f81 100644 --- a/engine/src/flutter/ci/builders/mac_android_aot_engine.json +++ b/engine/src/flutter/ci/builders/mac_android_aot_engine.json @@ -25,7 +25,7 @@ ], "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=x86" ], "gclient_variables": { @@ -65,7 +65,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, @@ -83,7 +83,7 @@ ], "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=x86" ], "gclient_variables": { @@ -125,7 +125,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, @@ -143,7 +143,7 @@ ], "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=x86" ], "gclient_variables": { @@ -185,7 +185,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, @@ -203,7 +203,7 @@ ], "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=x86" ], "gclient_variables": { @@ -243,7 +243,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, @@ -261,7 +261,7 @@ ], "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=x86" ], "gclient_variables": { @@ -303,7 +303,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, @@ -321,7 +321,7 @@ ], "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=x86" ], "gclient_variables": { @@ -363,7 +363,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } } diff --git a/engine/src/flutter/ci/builders/mac_clang_tidy.json b/engine/src/flutter/ci/builders/mac_clang_tidy.json index 1ad127acec92e..be8f1df0ec788 100644 --- a/engine/src/flutter/ci/builders/mac_clang_tidy.json +++ b/engine/src/flutter/ci/builders/mac_clang_tidy.json @@ -3,7 +3,7 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -29,14 +29,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -63,7 +63,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } } @@ -74,7 +74,7 @@ "recipe": "engine_v2/tester_engine", "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -120,7 +120,7 @@ ], "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, @@ -129,7 +129,7 @@ "recipe": "engine_v2/tester_engine", "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -175,7 +175,7 @@ ], "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, @@ -184,7 +184,7 @@ "recipe": "engine_v2/tester_engine", "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -230,7 +230,7 @@ ], "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, @@ -239,7 +239,7 @@ "recipe": "engine_v2/tester_engine", "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -285,7 +285,7 @@ ], "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, @@ -294,7 +294,7 @@ "recipe": "engine_v2/tester_engine", "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -358,7 +358,7 @@ ], "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } } diff --git a/engine/src/flutter/ci/builders/mac_host_engine.json b/engine/src/flutter/ci/builders/mac_host_engine.json index c79c50e5d6294..91ba3fd358311 100644 --- a/engine/src/flutter/ci/builders/mac_host_engine.json +++ b/engine/src/flutter/ci/builders/mac_host_engine.json @@ -17,7 +17,7 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -63,7 +63,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 @@ -84,7 +84,7 @@ ], "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -132,7 +132,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 @@ -140,7 +140,7 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -186,7 +186,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 @@ -205,7 +205,7 @@ ], "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -248,7 +248,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 @@ -256,7 +256,7 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -299,7 +299,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 @@ -307,7 +307,7 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -350,7 +350,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 @@ -370,7 +370,7 @@ ], "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -416,7 +416,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 @@ -424,7 +424,7 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -469,7 +469,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 @@ -477,7 +477,7 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -522,7 +522,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 @@ -543,7 +543,7 @@ ], "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -592,7 +592,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 @@ -600,7 +600,7 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -648,7 +648,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 @@ -656,7 +656,7 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -703,7 +703,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 @@ -722,7 +722,7 @@ ], "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -769,7 +769,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 @@ -777,7 +777,7 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -824,7 +824,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 @@ -832,7 +832,7 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -879,7 +879,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 @@ -899,7 +899,7 @@ ], "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -949,7 +949,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 @@ -957,7 +957,7 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -1006,7 +1006,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 @@ -1014,7 +1014,7 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -1063,7 +1063,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "timeout": 90 diff --git a/engine/src/flutter/ci/builders/mac_ios_engine.json b/engine/src/flutter/ci/builders/mac_ios_engine.json index 726cb23488543..e52151e7dbb87 100644 --- a/engine/src/flutter/ci/builders/mac_ios_engine.json +++ b/engine/src/flutter/ci/builders/mac_ios_engine.json @@ -14,7 +14,7 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -54,14 +54,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -103,14 +103,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -150,14 +150,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -197,14 +197,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -244,14 +244,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -293,14 +293,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -344,14 +344,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -393,14 +393,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -442,14 +442,14 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } }, { "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -491,7 +491,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } } } diff --git a/engine/src/flutter/ci/builders/mac_unopt.json b/engine/src/flutter/ci/builders/mac_unopt.json index f82560976f9c8..a72da4b83bc77 100644 --- a/engine/src/flutter/ci/builders/mac_unopt.json +++ b/engine/src/flutter/ci/builders/mac_unopt.json @@ -4,7 +4,7 @@ "cas_archive": false, "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -35,7 +35,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "tests": [ @@ -57,7 +57,7 @@ "cas_archive": false, "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -86,7 +86,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "tests": [ @@ -108,7 +108,7 @@ "cas_archive": false, "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -138,7 +138,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "tests": [ @@ -159,7 +159,7 @@ "cas_archive": false, "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -189,7 +189,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "tests": [ @@ -216,7 +216,7 @@ ], "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -246,7 +246,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "tests": [ @@ -268,14 +268,14 @@ "properties": { "$flutter/osx_sdk": { "runtime_versions": [ - "ios-18-2_16c5032a" + "ios-26-2_17c52" ], - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=x86" ], "gclient_variables": { @@ -332,7 +332,7 @@ "cas_archive": false, "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -364,7 +364,7 @@ }, "properties": { "$flutter/osx_sdk": { - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "tests": [ @@ -394,14 +394,14 @@ "properties": { "$flutter/osx_sdk": { "runtime_versions": [ - "ios-18-2_16c5032a" + "ios-26-2_17c52" ], - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { @@ -462,14 +462,14 @@ "properties": { "$flutter/osx_sdk": { "runtime_versions": [ - "ios-18-2_16c5032a" + "ios-26-2_17c52" ], - "sdk_version": "16c5032a" + "sdk_version": "17c52" } }, "drone_dimensions": [ "device_type=none", - "os=Mac-15.5|Mac-15.7", + "os=Mac-15.7", "cpu=arm64" ], "gclient_variables": { diff --git a/engine/src/flutter/common/config.gni b/engine/src/flutter/common/config.gni index 318d305bd37fc..35ec1c7e058dc 100644 --- a/engine/src/flutter/common/config.gni +++ b/engine/src/flutter/common/config.gni @@ -67,6 +67,14 @@ if (slimpeller) { feature_defines_list += [ "SLIMPELLER=1" ] } +if (is_android || is_ios || is_linux || is_mac || is_win) { + feature_defines_list += [ "SHOREBIRD_PLATFORM_SUPPORTED=1" ] +} + +if (is_ios) { + feature_defines_list += [ "SHOREBIRD_USE_INTERPRETER=1" ] +} + if (is_ios || is_mac) { flutter_cflags_objc = [ "-Werror=overriding-method-mismatch", diff --git a/engine/src/flutter/fml/BUILD.gn b/engine/src/flutter/fml/BUILD.gn index 8a0912b3cf5b2..69f88e485a7f4 100644 --- a/engine/src/flutter/fml/BUILD.gn +++ b/engine/src/flutter/fml/BUILD.gn @@ -67,6 +67,8 @@ source_set("fml") { "process.h", "raster_thread_merger.cc", "raster_thread_merger.h", + "safe_math.cc", + "safe_math.h", "shared_thread_merger.cc", "shared_thread_merger.h", "status.h", @@ -111,7 +113,7 @@ source_set("fml") { ":string_conversion", ] - deps = [] + deps = [ "//third_party/abseil-cpp/absl/numeric:int128" ] if (target_os != "wasm") { deps += [ "//flutter/third_party/icu" ] @@ -347,6 +349,7 @@ if (enable_unittests) { "message_loop_unittests.cc", "paths_unittests.cc", "raster_thread_merger_unittests.cc", + "safe_math_unittests.cc", "string_conversion_unittests.cc", "synchronization/count_down_latch_unittests.cc", "synchronization/semaphore_unittest.cc", diff --git a/engine/src/flutter/fml/safe_math.cc b/engine/src/flutter/fml/safe_math.cc new file mode 100644 index 0000000000000..f68f3158a3243 --- /dev/null +++ b/engine/src/flutter/fml/safe_math.cc @@ -0,0 +1,41 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +#include "flutter/fml/safe_math.h" +#include "third_party/abseil-cpp/absl/numeric/int128.h" + +namespace fml { + +size_t SafeMath::mul(size_t x, size_t y) { + return sizeof(size_t) == sizeof(uint64_t) ? mul64(x, y) : mul32(x, y); +} + +uint32_t SafeMath::mul32(uint32_t x, uint32_t y) { + uint64_t big_x = x; + uint64_t big_y = y; + uint64_t result = big_x * big_y; + if (result >> 32) { + overflow_detected_ = true; + } + return static_cast(result); +} + +uint64_t SafeMath::mul64(uint64_t x, uint64_t y) { + if (x <= std::numeric_limits::max() && + y <= std::numeric_limits::max()) { + return x * y; + } + + absl::uint128 big_x = x; + absl::uint128 big_y = y; + absl::uint128 result = big_x * big_y; + if (absl::Uint128High64(result)) { + overflow_detected_ = true; + } + return absl::Uint128Low64(result); +} + +} // namespace fml diff --git a/engine/src/flutter/fml/safe_math.h b/engine/src/flutter/fml/safe_math.h new file mode 100644 index 0000000000000..14d424647afa9 --- /dev/null +++ b/engine/src/flutter/fml/safe_math.h @@ -0,0 +1,30 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_FML_SAFE_MATH_H_ +#define FLUTTER_FML_SAFE_MATH_H_ + +#include +#include + +namespace fml { + +// Math operations that check for overflow. +// Based on Skia's SkSafeMath. +class SafeMath { + public: + bool overflow_detected() const { return overflow_detected_; } + + size_t mul(size_t x, size_t y); + + private: + uint32_t mul32(uint32_t x, uint32_t y); + uint64_t mul64(uint64_t x, uint64_t y); + + bool overflow_detected_ = false; +}; + +} // namespace fml + +#endif // FLUTTER_FML_SAFE_MATH_H_ diff --git a/engine/src/flutter/fml/safe_math_unittests.cc b/engine/src/flutter/fml/safe_math_unittests.cc new file mode 100644 index 0000000000000..c272cc564f605 --- /dev/null +++ b/engine/src/flutter/fml/safe_math_unittests.cc @@ -0,0 +1,39 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +#include "flutter/fml/safe_math.h" +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { + +TEST(SafeMathTest, MultiplySizeT) { + // Multiplication with no overflow. + fml::SafeMath safe1; + EXPECT_EQ(safe1.mul(1000, 2000), static_cast(2000000)); + EXPECT_FALSE(safe1.overflow_detected()); + + // Overflow detection when multiplying size_t values at or near the maximum. + fml::SafeMath safe2; + safe2.mul(std::numeric_limits::max(), + std::numeric_limits::max()); + EXPECT_TRUE(safe2.overflow_detected()); + + fml::SafeMath safe3; + safe3.mul(std::numeric_limits::max() >> 2, 5); + EXPECT_TRUE(safe3.overflow_detected()); + + // Overflow detection for a result that slightly exceeds the range of a + // uint64_t. + if (sizeof(size_t) == sizeof(uint64_t)) { + fml::SafeMath safe4; + safe4.mul(static_cast(1ULL << 32), static_cast(1ULL << 32)); + EXPECT_TRUE(safe4.overflow_detected()); + } +} + +} // namespace testing +} // namespace flutter diff --git a/engine/src/flutter/impeller/display_list/aiks_dl_text_unittests.cc b/engine/src/flutter/impeller/display_list/aiks_dl_text_unittests.cc index 30b884eca47e7..f1e96d9d2dce1 100644 --- a/engine/src/flutter/impeller/display_list/aiks_dl_text_unittests.cc +++ b/engine/src/flutter/impeller/display_list/aiks_dl_text_unittests.cc @@ -1055,5 +1055,31 @@ TEST_P(AiksTest, TextWithNonUniformScale) { ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); } +TEST_P(AiksTest, TextWithShadowAndPosition) { + DisplayListBuilder builder; + builder.Scale(GetContentScale().x, GetContentScale().y); + builder.Clear(DlColor::kWhite()); + + auto frame = MakeDefaultTextFrame("Hello", 25.0f); + auto text = DlTextImpeller::Make(frame); + DlPaint paint = DlPaint().setColor(DlColor::kMagenta()); + DlPaint shadow_paint_ctm = DlPaint().setMaskFilter( + DlBlurMaskFilter::Make(DlBlurStyle::kNormal, 5.0f, true)); + DlPaint shadow_paint_no_ctm = DlPaint().setMaskFilter( + DlBlurMaskFilter::Make(DlBlurStyle::kNormal, 5.0f, false)); + + builder.Translate(100, 100); + builder.Scale(4, 4); + for (int x = 10; x <= 100; x += 30) { + builder.DrawText(text, x, 20, shadow_paint_ctm); + builder.DrawText(text, x, 20, paint); + + builder.DrawText(text, x, 50, shadow_paint_no_ctm); + builder.DrawText(text, x, 50, paint); + } + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + } // namespace testing } // namespace impeller diff --git a/engine/src/flutter/impeller/display_list/canvas.cc b/engine/src/flutter/impeller/display_list/canvas.cc index 6cff7f547fb6d..1b67031f6e483 100644 --- a/engine/src/flutter/impeller/display_list/canvas.cc +++ b/engine/src/flutter/impeller/display_list/canvas.cc @@ -1947,7 +1947,7 @@ void Canvas::DrawTextFrame(const std::shared_ptr& text_frame, ? std::optional(paint.stroke) : std::nullopt); - entity.SetTransform(GetCurrentTransform()); + entity.SetTransform(GetCurrentTransform().Translate(position)); if (AttemptBlurredTextOptimization(text_frame, text_contents, entity, paint)) { diff --git a/engine/src/flutter/impeller/entity/contents/text_contents.cc b/engine/src/flutter/impeller/entity/contents/text_contents.cc index 2f4c6963ed418..d02d115e5a352 100644 --- a/engine/src/flutter/impeller/entity/contents/text_contents.cc +++ b/engine/src/flutter/impeller/entity/contents/text_contents.cc @@ -58,9 +58,7 @@ void TextContents::SetForceTextColor(bool value) { } std::optional TextContents::GetCoverage(const Entity& entity) const { - const Matrix entity_offset_transform = - entity.GetTransform() * Matrix::MakeTranslation(position_); - return frame_->GetBounds().TransformBounds(entity_offset_transform); + return frame_->GetBounds().TransformBounds(entity.GetTransform()); } void TextContents::SetTextProperties( @@ -92,7 +90,7 @@ Scalar AttractToOne(Scalar x) { void TextContents::ComputeVertexData( VS::PerVertexData* vtx_contents, - const Matrix& entity_transform, + const Matrix& entity_offset_transform, const std::shared_ptr& frame, Point position, const Matrix& screen_transform, @@ -108,9 +106,6 @@ void TextContents::ComputeVertexData( constexpr std::array unit_points = {Point{0, 0}, Point{1, 0}, Point{0, 1}, Point{1, 1}}; - Matrix entity_offset_transform = - entity_transform * Matrix::MakeTranslation(position); - ISize atlas_size = atlas->GetTexture()->GetSize(); bool is_translation_scale = entity_offset_transform.IsTranslationScaleOnly(); Matrix basis_transform = entity_offset_transform.Basis(); diff --git a/engine/src/flutter/impeller/renderer/backend/gles/test/texture_gles_unittests.cc b/engine/src/flutter/impeller/renderer/backend/gles/test/texture_gles_unittests.cc index ecada9846d04d..2a2090696218b 100644 --- a/engine/src/flutter/impeller/renderer/backend/gles/test/texture_gles_unittests.cc +++ b/engine/src/flutter/impeller/renderer/backend/gles/test/texture_gles_unittests.cc @@ -67,6 +67,38 @@ TEST_P(TextureGLESTest, CanSetSyncFence) { ASSERT_FALSE(sync_fence.has_value()); } +TEST_P(TextureGLESTest, TextureDtorDeletesFence) { + ContextGLES& context_gles = ContextGLES::Cast(*GetContext()); + if (!context_gles.GetReactor() + ->GetProcTable() + .GetDescription() + ->GetGlVersion() + .IsAtLeast(Version{3, 0, 0})) { + GTEST_SKIP() << "GL Version too low to test sync fence."; + } + + TextureDescriptor desc; + desc.storage_mode = StorageMode::kDevicePrivate; + desc.size = {100, 100}; + desc.format = PixelFormat::kR8G8B8A8UNormInt; + + auto texture = GetContext()->GetResourceAllocator()->CreateTexture(desc); + ASSERT_TRUE(!!texture); + + HandleGLES fence = + context_gles.GetReactor()->CreateHandle(HandleType::kFence); + bool fence_collected = false; + context_gles.GetReactor()->RegisterCleanupCallback( + fence, [&]() { fence_collected = true; }); + TextureGLES::Cast(*texture).SetFence(fence); + + texture.reset(); + ASSERT_TRUE(context_gles.GetReactor()->AddOperation( + [](const ReactorGLES& reactor) {})); + ASSERT_TRUE(context_gles.GetReactor()->React()); + EXPECT_TRUE(fence_collected); +} + TEST_P(TextureGLESTest, Binds2DTexture) { TextureDescriptor desc; desc.storage_mode = StorageMode::kDevicePrivate; diff --git a/engine/src/flutter/impeller/renderer/backend/gles/texture_gles.cc b/engine/src/flutter/impeller/renderer/backend/gles/texture_gles.cc index 5a9009c550f50..0bd9a73e35751 100644 --- a/engine/src/flutter/impeller/renderer/backend/gles/texture_gles.cc +++ b/engine/src/flutter/impeller/renderer/backend/gles/texture_gles.cc @@ -133,11 +133,13 @@ TextureGLES::TextureGLES(std::shared_ptr reactor, type_(GetTextureTypeFromDescriptor( GetTextureDescriptor(), reactor_->GetProcTable().GetCapabilities())), - handle_(external_handle.has_value() - ? external_handle.value() - : (threadsafe ? reactor_->CreateHandle(ToHandleType(type_)) - : reactor_->CreateUntrackedHandle( - ToHandleType(type_)))), + handle_( + external_handle.has_value() + ? UniqueHandleGLES(reactor_, external_handle.value()) + : (threadsafe + ? UniqueHandleGLES(reactor_, ToHandleType(type_)) + : UniqueHandleGLES::MakeUntracked(reactor_, + ToHandleType(type_)))), is_wrapped_(fbo.has_value() || external_handle.has_value()), wrapped_fbo_(fbo) { // Ensure the texture descriptor itself is valid. @@ -157,16 +159,8 @@ TextureGLES::TextureGLES(std::shared_ptr reactor, is_valid_ = true; } -// |Texture| -TextureGLES::~TextureGLES() { - reactor_->CollectHandle(handle_); - if (!cached_fbo_.IsDead()) { - reactor_->CollectHandle(cached_fbo_); - } -} - void TextureGLES::Leak() { - handle_ = HandleGLES::DeadHandle(); + handle_.Release(); } // |Texture| @@ -177,7 +171,7 @@ bool TextureGLES::IsValid() const { // |Texture| void TextureGLES::SetLabel(std::string_view label) { #ifdef IMPELLER_DEBUG - reactor_->SetDebugLabel(handle_, label); + reactor_->SetDebugLabel(handle_.Get(), label); #endif // IMPELLER_DEBUG } @@ -185,7 +179,8 @@ void TextureGLES::SetLabel(std::string_view label) { void TextureGLES::SetLabel(std::string_view label, std::string_view trailing) { #ifdef IMPELLER_DEBUG if (reactor_->CanSetDebugLabels()) { - reactor_->SetDebugLabel(handle_, std::format("{} {}", label, trailing)); + reactor_->SetDebugLabel(handle_.Get(), + std::format("{} {}", label, trailing)); } #endif // IMPELLER_DEBUG } @@ -265,7 +260,7 @@ bool TextureGLES::OnSetContents(std::shared_ptr mapping, return false; } - ReactorGLES::Operation texture_upload = [handle = handle_, // + ReactorGLES::Operation texture_upload = [handle = handle_.Get(), // mapping, // format = gles_format.value(), // size = tex_descriptor.size, // @@ -366,7 +361,7 @@ void TextureGLES::InitializeContentsIfNecessary() const { } const auto& gl = reactor_->GetProcTable(); - std::optional handle = reactor_->GetGLHandle(handle_); + std::optional handle = reactor_->GetGLHandle(handle_.Get()); if (!handle.has_value()) { VALIDATION_LOG << "Could not initialize the contents of texture."; return; @@ -447,7 +442,7 @@ std::optional TextureGLES::GetGLHandle() const { if (!IsValid()) { return std::nullopt; } - return reactor_->GetGLHandle(handle_); + return reactor_->GetGLHandle(handle_.Get()); } bool TextureGLES::Bind() const { @@ -457,13 +452,12 @@ bool TextureGLES::Bind() const { } const auto& gl = reactor_->GetProcTable(); - if (fence_.has_value()) { - std::optional fence = reactor_->GetGLFence(fence_.value()); + if (fence_.IsValid()) { + std::optional fence = reactor_->GetGLFence(fence_.Get()); if (fence.has_value()) { gl.WaitSync(fence.value(), 0, GL_TIMEOUT_IGNORED); } - reactor_->CollectHandle(fence_.value()); - fence_ = std::nullopt; + fence_.Reset(); } switch (type_) { @@ -614,21 +608,21 @@ std::optional TextureGLES::GetFBO() const { } void TextureGLES::SetFence(HandleGLES fence) { - FML_DCHECK(!fence_.has_value()); - fence_ = fence; + FML_DCHECK(!fence_.IsValid()); + fence_ = UniqueHandleGLES(reactor_, fence); } // Visible for testing. std::optional TextureGLES::GetSyncFence() const { - return fence_; + return fence_.IsValid() ? std::optional(fence_.Get()) : std::nullopt; } void TextureGLES::SetCachedFBO(HandleGLES fbo) { - cached_fbo_ = fbo; + cached_fbo_ = UniqueHandleGLES(reactor_, fbo); } const HandleGLES& TextureGLES::GetCachedFBO() const { - return cached_fbo_; + return cached_fbo_.Get(); } } // namespace impeller diff --git a/engine/src/flutter/impeller/renderer/backend/gles/texture_gles.h b/engine/src/flutter/impeller/renderer/backend/gles/texture_gles.h index 95893e8428a80..127305b02b34b 100644 --- a/engine/src/flutter/impeller/renderer/backend/gles/texture_gles.h +++ b/engine/src/flutter/impeller/renderer/backend/gles/texture_gles.h @@ -12,6 +12,7 @@ #include "impeller/core/texture.h" #include "impeller/renderer/backend/gles/handle_gles.h" #include "impeller/renderer/backend/gles/reactor_gles.h" +#include "impeller/renderer/backend/gles/unique_handle_gles.h" namespace impeller { @@ -79,9 +80,6 @@ class TextureGLES final : public Texture, TextureDescriptor desc, bool threadsafe = false); - // |Texture| - ~TextureGLES() override; - // |Texture| bool IsValid() const override; @@ -156,12 +154,12 @@ class TextureGLES final : public Texture, private: std::shared_ptr reactor_; const Type type_; - HandleGLES handle_; - mutable std::optional fence_ = std::nullopt; + UniqueHandleGLES handle_; + mutable UniqueHandleGLES fence_; mutable std::bitset<6> slices_initialized_ = 0; const bool is_wrapped_; const std::optional wrapped_fbo_; - HandleGLES cached_fbo_ = HandleGLES::DeadHandle(); + UniqueHandleGLES cached_fbo_; bool is_valid_ = false; TextureGLES(std::shared_ptr reactor, diff --git a/engine/src/flutter/impeller/renderer/backend/gles/unique_handle_gles.cc b/engine/src/flutter/impeller/renderer/backend/gles/unique_handle_gles.cc index d45dd4d79a550..89f67dc7f2aef 100644 --- a/engine/src/flutter/impeller/renderer/backend/gles/unique_handle_gles.cc +++ b/engine/src/flutter/impeller/renderer/backend/gles/unique_handle_gles.cc @@ -30,6 +30,10 @@ UniqueHandleGLES::UniqueHandleGLES(std::shared_ptr reactor, : reactor_(std::move(reactor)), handle_(handle) {} UniqueHandleGLES::~UniqueHandleGLES() { + CollectHandle(); +} + +void UniqueHandleGLES::CollectHandle() { if (!handle_.IsDead() && reactor_) { reactor_->CollectHandle(handle_); } @@ -43,9 +47,31 @@ bool UniqueHandleGLES::IsValid() const { return !handle_.IsDead(); } +void UniqueHandleGLES::Reset() { + CollectHandle(); + reactor_.reset(); + handle_ = HandleGLES::DeadHandle(); +} + +HandleGLES UniqueHandleGLES::Release() { + reactor_.reset(); + HandleGLES old_handle = handle_; + handle_ = HandleGLES::DeadHandle(); + return old_handle; +} + UniqueHandleGLES::UniqueHandleGLES(UniqueHandleGLES&& other) { std::swap(reactor_, other.reactor_); std::swap(handle_, other.handle_); } +UniqueHandleGLES& UniqueHandleGLES::operator=(UniqueHandleGLES&& other) { + if (this != &other) { + Reset(); + std::swap(reactor_, other.reactor_); + std::swap(handle_, other.handle_); + } + return *this; +} + } // namespace impeller diff --git a/engine/src/flutter/impeller/renderer/backend/gles/unique_handle_gles.h b/engine/src/flutter/impeller/renderer/backend/gles/unique_handle_gles.h index e5bbc33293bdc..10390c264a599 100644 --- a/engine/src/flutter/impeller/renderer/backend/gles/unique_handle_gles.h +++ b/engine/src/flutter/impeller/renderer/backend/gles/unique_handle_gles.h @@ -17,6 +17,8 @@ namespace impeller { /// class UniqueHandleGLES { public: + UniqueHandleGLES() = default; + UniqueHandleGLES(std::shared_ptr reactor, HandleType type); static UniqueHandleGLES MakeUntracked(std::shared_ptr reactor, @@ -27,18 +29,26 @@ class UniqueHandleGLES { ~UniqueHandleGLES(); UniqueHandleGLES(UniqueHandleGLES&&); + UniqueHandleGLES& operator=(UniqueHandleGLES&&); UniqueHandleGLES(const UniqueHandleGLES&) = delete; - UniqueHandleGLES& operator=(const UniqueHandleGLES&) = delete; const HandleGLES& Get() const; bool IsValid() const; + /// Collect the managed handle and replace it with a dead handle. + void Reset(); + + /// Release ownership of the handle. + HandleGLES Release(); + private: std::shared_ptr reactor_ = nullptr; HandleGLES handle_ = HandleGLES::DeadHandle(); + + void CollectHandle(); }; } // namespace impeller diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/BUILD.gn b/engine/src/flutter/impeller/renderer/backend/vulkan/BUILD.gn index 12fbcd6aea5ef..d75a54d7af0ef 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/BUILD.gn +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/BUILD.gn @@ -6,6 +6,20 @@ import("//flutter/testing/android/native_activity/native_activity.gni") import("//flutter/vulkan/config.gni") import("../../../tools/impeller.gni") +source_set("mock_vulkan") { + testonly = true + + sources = [ + "test/mock_vulkan.cc", + "test/mock_vulkan.h", + ] + + deps = [ + ":vulkan", + "//flutter/fml", + ] +} + impeller_component("vulkan_unittests") { testonly = true sources = [ @@ -25,13 +39,12 @@ impeller_component("vulkan_unittests") { "resource_manager_vk_unittests.cc", "surface_context_vk_unittests.cc", "test/gpu_tracer_unittests.cc", - "test/mock_vulkan.cc", - "test/mock_vulkan.h", "test/mock_vulkan_unittests.cc", "test/sampler_library_vk_unittests.cc", "test/swapchain_unittests.cc", ] deps = [ + ":mock_vulkan", ":vulkan", "../../../playground:playground_test", "//flutter/testing:testing_lib", @@ -179,9 +192,13 @@ if (is_android) { testonly = true - sources = [ "android/ahb_texture_source_vk_unittests.cc" ] + sources = [ + "android/ahb_swapchain_vk_unittests.cc", + "android/ahb_texture_source_vk_unittests.cc", + ] deps = [ + ":mock_vulkan", ":unittests_fixtures", ":vulkan", "//flutter/testing", diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/android/ahb_swapchain_vk_unittests.cc b/engine/src/flutter/impeller/renderer/backend/vulkan/android/ahb_swapchain_vk_unittests.cc new file mode 100644 index 0000000000000..9b652986ed7b9 --- /dev/null +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/android/ahb_swapchain_vk_unittests.cc @@ -0,0 +1,50 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "gtest/gtest.h" + +#include "impeller/renderer/backend/vulkan/swapchain/ahb/ahb_swapchain_vk.h" +#include "impeller/renderer/backend/vulkan/test/mock_vulkan.h" +#include "impeller/toolkit/android/surface_control.h" + +namespace impeller::android::testing { + +using impeller::testing::GetMockVulkanFunctions; +using impeller::testing::MockVulkanContextBuilder; + +const std::vector kAndroidDeviceExtensions = { + "VK_KHR_swapchain", + "VK_ANDROID_external_memory_android_hardware_buffer", + "VK_KHR_sampler_ycbcr_conversion", + "VK_KHR_external_memory", + "VK_EXT_queue_family_foreign", + "VK_KHR_dedicated_allocation", +}; + +class FakeSurfaceControl : public SurfaceControl { + public: + bool IsValid() const override { return true; } + + ASurfaceControl* GetHandle() const override { return nullptr; } + + bool RemoveFromParent() const override { return true; } +}; + +TEST(AndroidAHBSwapchainTest, AHBSwapchainDtorCallsWaitIdle) { + const auto context = MockVulkanContextBuilder() + .SetDeviceExtensions(kAndroidDeviceExtensions) + .Build(); + + auto ahb_swapchain = std::shared_ptr(new AHBSwapchainVK( + context, std::make_shared(), {}, {100, 100}, false)); + + ahb_swapchain.reset(); + + auto called_functions = GetMockVulkanFunctions(context->GetDevice()); + EXPECT_NE(std::find(called_functions->begin(), called_functions->end(), + "vkDeviceWaitIdle"), + called_functions->end()); +} + +} // namespace impeller::android::testing diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/ahb/ahb_swapchain_impl_vk.cc b/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/ahb/ahb_swapchain_impl_vk.cc index 308eadd64c263..0a0a8d658fb78 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/ahb/ahb_swapchain_impl_vk.cc +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/ahb/ahb_swapchain_impl_vk.cc @@ -102,7 +102,11 @@ AHBSwapchainImplVK::AHBSwapchainImplVK( is_valid_ = control && control->IsValid(); } -AHBSwapchainImplVK::~AHBSwapchainImplVK() = default; +AHBSwapchainImplVK::~AHBSwapchainImplVK() { + // Ensure that the Vulkan device is idle before destroying objects that the + // device may have been using. + WaitIdle(); +} const ISize& AHBSwapchainImplVK::GetSize() const { return desc_.size; @@ -117,6 +121,17 @@ const android::HardwareBufferDescriptor& AHBSwapchainImplVK::GetDescriptor() return desc_; } +void AHBSwapchainImplVK::WaitIdle() const { + if (transients_) { + if (auto context = transients_->GetContext().lock()) { + auto result = ContextVK::Cast(*context).GetDevice().waitIdle(); + if (result != vk::Result::eSuccess) { + FML_LOG(INFO) << "Device waitIdle failed: " << vk::to_string(result); + } + } + } +} + std::unique_ptr AHBSwapchainImplVK::AcquireNextDrawable() { auto context = transients_->GetContext().lock(); if (!context) { diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/ahb/ahb_swapchain_impl_vk.h b/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/ahb/ahb_swapchain_impl_vk.h index ecfdd126993fe..e372013d22bd7 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/ahb/ahb_swapchain_impl_vk.h +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/ahb/ahb_swapchain_impl_vk.h @@ -140,6 +140,8 @@ class AHBSwapchainImplVK final bool Present(const std::shared_ptr& texture); + void WaitIdle() const; + vk::UniqueSemaphore CreateRenderReadySemaphore( const std::shared_ptr& fd) const; diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/ahb/ahb_swapchain_vk.cc b/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/ahb/ahb_swapchain_vk.cc index dc90a2768688d..d6fc9900183cf 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/ahb/ahb_swapchain_vk.cc +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/ahb/ahb_swapchain_vk.cc @@ -17,14 +17,14 @@ bool AHBSwapchainVK::IsAvailableOnPlatform() { android::HardwareBuffer::IsAvailableOnPlatform(); } -AHBSwapchainVK::AHBSwapchainVK(const std::shared_ptr& context, - ANativeWindow* window, - const CreateTransactionCB& cb, - const ISize& size, - bool enable_msaa) +AHBSwapchainVK::AHBSwapchainVK( + const std::shared_ptr& context, + const std::shared_ptr& surface_control, + const CreateTransactionCB& cb, + const ISize& size, + bool enable_msaa) : context_(context), - surface_control_( - std::make_shared(window, "ImpellerSurface")), + surface_control_(surface_control), enable_msaa_(enable_msaa), cb_(cb) { UpdateSurfaceSize(size); diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/ahb/ahb_swapchain_vk.h b/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/ahb/ahb_swapchain_vk.h index 90b6359fef0b7..1ddfa44966e27 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/ahb/ahb_swapchain_vk.h +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/ahb/ahb_swapchain_vk.h @@ -13,6 +13,10 @@ namespace impeller { +namespace android::testing { +FML_TEST_CLASS(AndroidAHBSwapchainTest, AHBSwapchainDtorCallsWaitIdle); +} // namespace android::testing + using CreateTransactionCB = std::function; //------------------------------------------------------------------------------ @@ -55,6 +59,8 @@ class AHBSwapchainVK final : public SwapchainVK { private: friend class SwapchainVK; + FML_FRIEND_TEST(android::testing::AndroidAHBSwapchainTest, + AHBSwapchainDtorCallsWaitIdle); std::weak_ptr context_; std::shared_ptr surface_control_; @@ -62,11 +68,12 @@ class AHBSwapchainVK final : public SwapchainVK { CreateTransactionCB cb_; std::shared_ptr impl_; - explicit AHBSwapchainVK(const std::shared_ptr& context, - ANativeWindow* window, - const CreateTransactionCB& cb, - const ISize& size, - bool enable_msaa); + explicit AHBSwapchainVK( + const std::shared_ptr& context, + const std::shared_ptr& surface_control, + const CreateTransactionCB& cb, + const ISize& size, + bool enable_msaa); }; } // namespace impeller diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/swapchain_vk.cc b/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/swapchain_vk.cc index 0fe5c91185f13..316e3039a32f3 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/swapchain_vk.cc +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/swapchain/swapchain_vk.cc @@ -50,13 +50,15 @@ std::shared_ptr SwapchainVK::Create( AHBSwapchainVK::IsAvailableOnPlatform()) { CreateTransactionCB callback = android_get_device_api_level() >= 34 ? cb : CreateTransactionCB({}); - auto ahb_swapchain = std::shared_ptr(new AHBSwapchainVK( - context, // - window.GetHandle(), // - callback, // - window.GetSize(), // - enable_msaa // - )); + auto surface_control = std::shared_ptr( + android::SurfaceControl::Create(window.GetHandle(), "ImpellerSurface")); + auto ahb_swapchain = + std::shared_ptr(new AHBSwapchainVK(context, // + surface_control, // + callback, // + window.GetSize(), // + enable_msaa // + )); if (ahb_swapchain->IsValid()) { return ahb_swapchain; diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/test/mock_vulkan.cc b/engine/src/flutter/impeller/renderer/backend/vulkan/test/mock_vulkan.cc index b51299d3cbdfe..fffea95acf1a9 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/test/mock_vulkan.cc +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/test/mock_vulkan.cc @@ -110,6 +110,7 @@ class MockDevice final { struct MockVulkanState { std::vector instance_extensions; std::vector instance_layers; + std::vector device_extensions; std::function @@ -165,32 +166,46 @@ VkResult vkEnumerateInstanceExtensionProperties( VkExtensionProperties* pProperties) { if (!pProperties) { *pPropertyCount = GetMockVulkanState().instance_extensions.size(); + return VK_SUCCESS; } else { uint32_t count = 0; + VkResult result = VK_SUCCESS; for (const std::string& ext : GetMockVulkanState().instance_extensions) { - strncpy(pProperties[count].extensionName, ext.c_str(), - sizeof(VkExtensionProperties::extensionName)); + if (count >= *pPropertyCount) { + result = VK_INCOMPLETE; + break; + } + snprintf(pProperties[count].extensionName, + sizeof(pProperties[count].extensionName), "%s", ext.c_str()); pProperties[count].specVersion = 0; count++; } + *pPropertyCount = count; + return result; } - return VK_SUCCESS; } VkResult vkEnumerateInstanceLayerProperties(uint32_t* pPropertyCount, VkLayerProperties* pProperties) { if (!pProperties) { *pPropertyCount = GetMockVulkanState().instance_layers.size(); + return VK_SUCCESS; } else { uint32_t count = 0; - for (const std::string& layer : GetMockVulkanState().instance_layers) { - strncpy(pProperties[count].layerName, layer.c_str(), - sizeof(VkLayerProperties::layerName)); + VkResult result = VK_SUCCESS; + for (const std::string& ext : GetMockVulkanState().instance_layers) { + if (count >= *pPropertyCount) { + result = VK_INCOMPLETE; + break; + } + snprintf(pProperties[count].layerName, + sizeof(pProperties[count].layerName), "%s", ext.c_str()); pProperties[count].specVersion = 0; count++; } + *pPropertyCount = count; + return result; } - return VK_SUCCESS; } VkResult vkEnumeratePhysicalDevices(VkInstance instance, @@ -246,12 +261,24 @@ VkResult vkEnumerateDeviceExtensionProperties( uint32_t* pPropertyCount, VkExtensionProperties* pProperties) { if (!pProperties) { - *pPropertyCount = 1; + *pPropertyCount = GetMockVulkanState().device_extensions.size(); + return VK_SUCCESS; } else { - strcpy(pProperties[0].extensionName, "VK_KHR_swapchain"); - pProperties[0].specVersion = 0; + uint32_t count = 0; + VkResult result = VK_SUCCESS; + for (const std::string& ext : GetMockVulkanState().device_extensions) { + if (count >= *pPropertyCount) { + result = VK_INCOMPLETE; + break; + } + snprintf(pProperties[count].extensionName, + sizeof(pProperties[count].extensionName), "%s", ext.c_str()); + pProperties[count].specVersion = 0; + count++; + } + *pPropertyCount = count; + return result; } - return VK_SUCCESS; } VkResult vkCreateDevice(VkPhysicalDevice physicalDevice, @@ -439,6 +466,12 @@ VkResult vkCreateGraphicsPipelines( return VK_SUCCESS; } +VkResult vkDeviceWaitIdle(VkDevice device) { + MockDevice* mock_device = reinterpret_cast(device); + mock_device->AddCalledFunction("vkDeviceWaitIdle"); + return VK_SUCCESS; +} + void vkDestroyDevice(VkDevice device, const VkAllocationCallbacks* pAllocator) { MockDevice* mock_device = reinterpret_cast(device); mock_device->AddCalledFunction("vkDestroyDevice"); @@ -929,6 +962,8 @@ PFN_vkVoidFunction GetMockVulkanProcAddress(VkInstance instance, return reinterpret_cast(vkCreatePipelineLayout); } else if (strcmp("vkCreateGraphicsPipelines", pName) == 0) { return reinterpret_cast(vkCreateGraphicsPipelines); + } else if (strcmp("vkDeviceWaitIdle", pName) == 0) { + return reinterpret_cast(vkDeviceWaitIdle); } else if (strcmp("vkDestroyDevice", pName) == 0) { return reinterpret_cast(vkDestroyDevice); } else if (strcmp("vkDestroyInstance", pName) == 0) { @@ -1026,6 +1061,7 @@ PFN_vkVoidFunction GetMockVulkanProcAddress(VkInstance instance, MockVulkanContextBuilder::MockVulkanContextBuilder() : instance_extensions_({"VK_KHR_surface", "VK_MVK_macos_surface"}), + device_extensions_({"VK_KHR_swapchain"}), format_properties_callback_([](VkPhysicalDevice physicalDevice, VkFormat format, VkFormatProperties* pFormatProperties) { @@ -1054,6 +1090,7 @@ std::shared_ptr MockVulkanContextBuilder::Build() { g_mock_vulkan_state.reset(new MockVulkanState()); g_mock_vulkan_state->instance_extensions = instance_extensions_; g_mock_vulkan_state->instance_layers = instance_layers_; + g_mock_vulkan_state->device_extensions = device_extensions_; g_mock_vulkan_state->format_properties_callback = format_properties_callback_; g_mock_vulkan_state->physical_device_properties_callback = physical_properties_callback_; diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/test/mock_vulkan.h b/engine/src/flutter/impeller/renderer/backend/vulkan/test/mock_vulkan.h index 79772c1fc6529..eb82a8e8325ab 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/test/mock_vulkan.h +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/test/mock_vulkan.h @@ -89,6 +89,12 @@ class MockVulkanContextBuilder { return *this; } + MockVulkanContextBuilder& SetDeviceExtensions( + const std::vector& device_extensions) { + device_extensions_ = device_extensions; + return *this; + } + /// Set the behavior of vkGetPhysicalDeviceFormatProperties, which needs to /// respond differently for different formats. MockVulkanContextBuilder& SetPhysicalDeviceFormatPropertiesCallback( @@ -132,6 +138,7 @@ class MockVulkanContextBuilder { std::function settings_callback_; std::vector instance_extensions_; std::vector instance_layers_; + std::vector device_extensions_; std::optional embedder_data_; std::function SurfaceControl::Create(ANativeWindow* window, + const char* debug_name) { + return std::unique_ptr( + new SurfaceControlImpl(window, debug_name)); } bool SurfaceControl::IsAvailableOnPlatform() { diff --git a/engine/src/flutter/impeller/toolkit/android/surface_control.h b/engine/src/flutter/impeller/toolkit/android/surface_control.h index b853ed488040a..d17c21c6cac16 100644 --- a/engine/src/flutter/impeller/toolkit/android/surface_control.h +++ b/engine/src/flutter/impeller/toolkit/android/surface_control.h @@ -5,7 +5,8 @@ #ifndef FLUTTER_IMPELLER_TOOLKIT_ANDROID_SURFACE_CONTROL_H_ #define FLUTTER_IMPELLER_TOOLKIT_ANDROID_SURFACE_CONTROL_H_ -#include "flutter/fml/unique_object.h" +#include + #include "impeller/toolkit/android/proc_table.h" namespace impeller::android { @@ -37,8 +38,9 @@ class SurfaceControl { /// other control properties. If no debug name is /// specified, the value "Impeller Layer" is used. /// - explicit SurfaceControl(ANativeWindow* window, - const char* debug_name = nullptr); + static std::unique_ptr Create( + ANativeWindow* window, + const char* debug_name = nullptr); //---------------------------------------------------------------------------- /// @brief Removes the surface control from the presentation hierarchy @@ -46,15 +48,11 @@ class SurfaceControl { /// reference to the control. At this point, it may be collected /// when the compositor is also done using it. /// - ~SurfaceControl(); - - SurfaceControl(const SurfaceControl&) = delete; + virtual ~SurfaceControl() = default; - SurfaceControl& operator=(const SurfaceControl&) = delete; + virtual bool IsValid() const = 0; - bool IsValid() const; - - ASurfaceControl* GetHandle() const; + virtual ASurfaceControl* GetHandle() const = 0; //---------------------------------------------------------------------------- /// @brief Remove the surface control from the hierarchy of nodes @@ -66,22 +64,7 @@ class SurfaceControl { /// @return `true` If the control will be removed from the hierarchy of /// nodes presented by the system compositor. /// - bool RemoveFromParent() const; - - private: - struct UniqueASurfaceControlTraits { - static ASurfaceControl* InvalidValue() { return nullptr; } - - static bool IsValid(ASurfaceControl* value) { - return value != InvalidValue(); - } - - static void Free(ASurfaceControl* value) { - GetProcTable().ASurfaceControl_release(value); - } - }; - - fml::UniqueObject control_; + virtual bool RemoveFromParent() const = 0; }; } // namespace impeller::android diff --git a/engine/src/flutter/impeller/toolkit/android/surface_control_impl.cc b/engine/src/flutter/impeller/toolkit/android/surface_control_impl.cc new file mode 100644 index 0000000000000..1742d2112cd75 --- /dev/null +++ b/engine/src/flutter/impeller/toolkit/android/surface_control_impl.cc @@ -0,0 +1,51 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "impeller/toolkit/android/surface_control_impl.h" + +#include "impeller/base/validation.h" +#include "impeller/toolkit/android/surface_transaction.h" + +namespace impeller::android { + +SurfaceControlImpl::SurfaceControlImpl(ANativeWindow* window, + const char* debug_name) { + if (window == nullptr) { + VALIDATION_LOG << "Parent window of surface was null."; + return; + } + if (debug_name == nullptr) { + debug_name = "Impeller Layer"; + } + control_.reset( + GetProcTable().ASurfaceControl_createFromWindow(window, debug_name)); +} + +SurfaceControlImpl::~SurfaceControlImpl() { + if (IsValid() && !RemoveFromParent()) { + VALIDATION_LOG << "Surface control could not be removed from its parent. " + "Expect a leak."; + } +} + +bool SurfaceControlImpl::IsValid() const { + return control_.is_valid(); +} + +ASurfaceControl* SurfaceControlImpl::GetHandle() const { + return control_.get(); +} + +bool SurfaceControlImpl::RemoveFromParent() const { + if (!IsValid()) { + return false; + } + SurfaceTransaction transaction; + if (!transaction.SetParent(*this, nullptr)) { + return false; + } + return transaction.Apply(); +} + +} // namespace impeller::android diff --git a/engine/src/flutter/impeller/toolkit/android/surface_control_impl.h b/engine/src/flutter/impeller/toolkit/android/surface_control_impl.h new file mode 100644 index 0000000000000..58c4225313668 --- /dev/null +++ b/engine/src/flutter/impeller/toolkit/android/surface_control_impl.h @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_IMPELLER_TOOLKIT_ANDROID_SURFACE_CONTROL_IMPL_H_ +#define FLUTTER_IMPELLER_TOOLKIT_ANDROID_SURFACE_CONTROL_IMPL_H_ + +#include "flutter/fml/unique_object.h" +#include "impeller/toolkit/android/surface_control.h" + +namespace impeller::android { + +class SurfaceControlImpl final : public SurfaceControl { + public: + virtual ~SurfaceControlImpl(); + + SurfaceControlImpl(const SurfaceControlImpl&) = delete; + + SurfaceControlImpl& operator=(const SurfaceControlImpl&) = delete; + + SurfaceControlImpl(ANativeWindow* window, const char* debug_name); + + bool IsValid() const override; + + ASurfaceControl* GetHandle() const override; + + bool RemoveFromParent() const override; + + private: + friend class SurfaceControl; + + struct UniqueASurfaceControlTraits { + static ASurfaceControl* InvalidValue() { return nullptr; } + + static bool IsValid(ASurfaceControl* value) { + return value != InvalidValue(); + } + + static void Free(ASurfaceControl* value) { + GetProcTable().ASurfaceControl_release(value); + } + }; + + fml::UniqueObject control_; +}; + +} // namespace impeller::android + +#endif // FLUTTER_IMPELLER_TOOLKIT_ANDROID_SURFACE_CONTROL_IMPL_H_ diff --git a/engine/src/flutter/impeller/toolkit/interop/README.md b/engine/src/flutter/impeller/toolkit/interop/README.md index 6e75a53a11775..279379217e143 100644 --- a/engine/src/flutter/impeller/toolkit/interop/README.md +++ b/engine/src/flutter/impeller/toolkit/interop/README.md @@ -27,7 +27,7 @@ A single-header C API for 2D graphics and text rendering. [Impeller](../../READM Users may plug in a custom toolchain into the Flutter Engine build system to build the `libimpeller.so` dynamic library. However, for the common platforms, the CI bots upload a tarball containing the library and headers. This URL for the SDK tarball for a particular platform can be constructed as follows: ```sh -https://storage.googleapis.com/flutter_infra_release/flutter/$FLUTTER_SHA/$PLATFORM_ARCH/impeller_sdk.zip +https://download.shorebird.dev/flutter_infra_release/flutter/$FLUTTER_SHA/$PLATFORM_ARCH/impeller_sdk.zip ``` The `$FLUTTER_SHA` is the Git hash in the [Flutter repository](https://github.com/flutter/flutter). The `$PLATFORM_ARCH` can be determined from the table below. diff --git a/engine/src/flutter/lib/snapshot/BUILD.gn b/engine/src/flutter/lib/snapshot/BUILD.gn index e4b52cac21985..e495383162a89 100644 --- a/engine/src/flutter/lib/snapshot/BUILD.gn +++ b/engine/src/flutter/lib/snapshot/BUILD.gn @@ -35,7 +35,10 @@ group("generate_snapshot_bins") { if (host_os == "mac" && (target_os == "mac" || target_os == "ios" || target_os == "android")) { # For macOS target builds: needed for both target CPUs (arm64, x64). - public_deps += [ ":create_macos_gen_snapshots" ] + public_deps += [ + ":create_macos_analyze_snapshots", + ":create_macos_gen_snapshots", + ] } else if (host_os == "mac" && (target_cpu == "arm" || target_cpu == "arm64")) { # For iOS, Android target builds: all AOT target CPUs are arm/arm64. @@ -46,9 +49,11 @@ group("generate_snapshot_bins") { public_deps = [ "$dart_src/runtime/bin:gen_snapshot($host_toolchain)" ] } - # Build analyze_snapshot for 64-bit target CPUs. - if (host_os == "linux" && (target_cpu == "x64" || target_cpu == "arm64" || - target_cpu == "riscv64")) { + # Build analyze_snapshot for 64-bit target CPUs on linux. + # Or always targeting arm64 for Shorebird builds. + if ((host_os == "linux" && + (target_cpu == "x64" || target_cpu == "riscv64")) || + target_cpu == "arm64") { public_deps += [ "$dart_src/runtime/bin:analyze_snapshot($host_toolchain)" ] } } @@ -257,6 +262,69 @@ if (host_os == "mac" && ":create_macos_gen_snapshot_x64${gen_snapshot_suffix}", ] } + + # Added by shorebird. + # analyze_snapshot targets below were copied from the gen_snapshot targets + # above to allow us to include analyze_snapshot in the artifacts generated + # for create_ios_framework.py. + template("build_mac_analyze_snapshot") { + assert(defined(invoker.host_arch)) + host_cpu = invoker.host_arch + + build_toolchain = "//build/toolchain/mac:clang_$host_cpu" + analyze_snapshot_target_name = "analyze_snapshot" + + # At this point, the gen_snapshot equivalent changes + # gen_ snapshot_target_name to "gen_snapshot_host_targeting_host". There is + # no equivalent for analyze_snapshot, so we don't do that here. + # + # It's unclear whether we need to do so now, but we didn't previously, so + # we're not doing it now until we have a reason to. + + analyze_snapshot_target = + "$dart_src/runtime/bin:$analyze_snapshot_target_name($build_toolchain)" + + copy(target_name) { + # The toolchain-specific output directory. For cross-compiles, this is a + # clang-x64 or clang-arm64 subdirectory of the top-level build directory. + output_dir = get_label_info(analyze_snapshot_target, "root_out_dir") + + sources = [ "${output_dir}/${analyze_snapshot_target_name}" ] + outputs = [ + "${root_out_dir}/artifacts_$host_cpu/analyze_snapshot_${target_cpu}", + ] + deps = [ analyze_snapshot_target ] + } + } + + build_mac_analyze_snapshot( + "create_macos_analyze_snapshot_arm64_${target_cpu}") { + host_arch = "arm64" + } + + build_mac_analyze_snapshot( + "create_macos_analyze_snapshot_x64_${target_cpu}") { + host_arch = "x64" + } + + action("create_macos_analyze_snapshots") { + script = "//flutter/sky/tools/create_macos_binary.py" + outputs = [ "${root_out_dir}/analyze_snapshot_${target_cpu}" ] + args = [ + "--in-arm64", + rebase_path( + "${root_out_dir}/artifacts_arm64/analyze_snapshot_${target_cpu}"), + "--in-x64", + rebase_path( + "${root_out_dir}/artifacts_x64/analyze_snapshot_${target_cpu}"), + "--out", + rebase_path("${root_out_dir}/analyze_snapshot_${target_cpu}"), + ] + deps = [ + ":create_macos_analyze_snapshot_arm64_${target_cpu}", + ":create_macos_analyze_snapshot_x64_${target_cpu}", + ] + } } source_set("snapshot") { diff --git a/engine/src/flutter/lib/ui/BUILD.gn b/engine/src/flutter/lib/ui/BUILD.gn index ecc16ce1109e4..3bad6b4e948a9 100644 --- a/engine/src/flutter/lib/ui/BUILD.gn +++ b/engine/src/flutter/lib/ui/BUILD.gn @@ -269,6 +269,7 @@ if (enable_unittests) { "painting/image_decoder_no_gl_unittests.h", "painting/image_dispose_unittests.cc", "painting/image_encoding_unittests.cc", + "painting/image_generator_apng_unittests.cc", "painting/image_generator_registry_unittests.cc", "painting/paint_unittests.cc", "painting/path_unittests.cc", diff --git a/engine/src/flutter/lib/ui/painting/image_generator_apng.cc b/engine/src/flutter/lib/ui/painting/image_generator_apng.cc index 08182c9ff2bd4..6b21aebb51d08 100644 --- a/engine/src/flutter/lib/ui/painting/image_generator_apng.cc +++ b/engine/src/flutter/lib/ui/painting/image_generator_apng.cc @@ -7,6 +7,7 @@ #include #include "flutter/fml/logging.h" +#include "flutter/fml/safe_math.h" #include "third_party/skia/include/codec/SkCodec.h" #include "third_party/skia/include/codec/SkCodecAnimation.h" #include "third_party/skia/include/core/SkAlphaType.h" @@ -91,10 +92,26 @@ bool APNGImageGenerator::GetPixels(const SkImageInfo& info, APNGImage& frame = images_[image_index]; SkImageInfo frame_info = frame.codec->getInfo(); - auto frame_row_bytes = frame_info.bytesPerPixel() * frame_info.width(); + + fml::SafeMath safe; + size_t frame_row_bytes = + safe.mul(frame_info.bytesPerPixel(), frame_info.width()); + if (safe.overflow_detected()) { + FML_DLOG(ERROR) << "Failed to decode image at index " << image_index + << " (frame index: " << frame_index + << ") of APNG due to frame row bytes overflow."; + return false; + } if (frame.pixels.empty()) { - frame.pixels.resize(frame_row_bytes * frame_info.height()); + size_t pixels_bytes = safe.mul(frame_row_bytes, frame_info.height()); + if (safe.overflow_detected()) { + FML_DLOG(ERROR) << "Failed to decode image at index " << image_index + << " (frame index: " << frame_index + << ") of APNG due to pixel buffer size overflow."; + return false; + } + frame.pixels.resize(pixels_bytes); SkCodec::Result result = frame.codec->getPixels( frame.codec->getInfo(), frame.pixels.data(), frame_row_bytes); if (result != SkCodec::kSuccess) { @@ -269,6 +286,9 @@ std::unique_ptr APNGImageGenerator::MakeFromData( } } + if (chunk->get_data_length() < sizeof(AnimationControlChunkData)) { + return nullptr; + } const AnimationControlChunkData* animation_data = CastChunkData(chunk); @@ -413,6 +433,9 @@ APNGImageGenerator::DemuxNextImage(const void* buffer_p, // The presence of an fcTL chunk is optional for the first (default) image // of a PNG. Both cases are handled in APNGImage. if (chunk->get_type() == kFrameControlChunkType) { + if (chunk->get_data_length() < sizeof(FrameControlChunkData)) { + return std::make_pair(std::nullopt, nullptr); + } control_data = CastChunkData(chunk); ImageGenerator::FrameInfo frame_info; @@ -474,7 +497,10 @@ APNGImageGenerator::DemuxNextImage(const void* buffer_p, // sequence number prepended to its data, so subtract that space from // the buffer. if (chunk->get_type() == kFrameDataChunkType) { - chunk_space -= 4; + if (chunk->get_data_length() < kFrameDataSequenceNumberSize) { + return std::make_pair(std::nullopt, nullptr); + } + chunk_space -= kFrameDataSequenceNumberSize; } } @@ -510,17 +536,21 @@ APNGImageGenerator::DemuxNextImage(const void* buffer_p, // Copy the image data/ancillary chunks. for (const ChunkHeader* c : image_chunks) { if (c->get_type() == kFrameDataChunkType) { + FML_DCHECK(c->get_data_length() >= kFrameDataSequenceNumberSize); + // Write a new IDAT chunk header. ChunkHeader* write_header = reinterpret_cast(write_cursor); - write_header->set_data_length(c->get_data_length() - 4); + write_header->set_data_length(c->get_data_length() - + kFrameDataSequenceNumberSize); write_header->set_type(kImageDataChunkType); write_cursor += sizeof(ChunkHeader); // Copy all of the data except for the 4 byte sequence number at the // beginning of the fdAT data. memcpy(write_cursor, - reinterpret_cast(c) + sizeof(ChunkHeader) + 4, + reinterpret_cast(c) + sizeof(ChunkHeader) + + kFrameDataSequenceNumberSize, write_header->get_data_length()); write_cursor += write_header->get_data_length(); @@ -628,8 +658,13 @@ void APNGImageGenerator::ChunkHeader::UpdateChunkCrc32() { uint32_t APNGImageGenerator::ChunkHeader::ComputeChunkCrc32() { // Exclude the length field at the beginning of the chunk header. size_t length = sizeof(ChunkHeader) - 4 + get_data_length(); - uint8_t* chunk_data_p = reinterpret_cast(this) + 4; + const uint8_t* chunk_data = reinterpret_cast(this) + 4; + return ComputeCrc32(chunk_data, length); +} + +uint32_t APNGImageGenerator::ComputeCrc32(const uint8_t* data, size_t length) { uint32_t crc = 0; + const uint8_t* data_p = data; // zlib's crc32 can only take 16 bits at a time for the length, but PNG // supports a 32 bit chunk length, so looping is necessary here. @@ -641,9 +676,9 @@ uint32_t APNGImageGenerator::ChunkHeader::ComputeChunkCrc32() { length16 = std::numeric_limits::max(); } - crc = crc32(crc, chunk_data_p, length16); + crc = crc32(crc, data_p, length16); length -= length16; - chunk_data_p += length16; + data_p += length16; } while (length > 0); return crc; diff --git a/engine/src/flutter/lib/ui/painting/image_generator_apng.h b/engine/src/flutter/lib/ui/painting/image_generator_apng.h index 1bf1c26e298d0..6aa1634cc46e9 100644 --- a/engine/src/flutter/lib/ui/painting/image_generator_apng.h +++ b/engine/src/flutter/lib/ui/painting/image_generator_apng.h @@ -53,10 +53,16 @@ class APNGImageGenerator : public ImageGenerator { static std::unique_ptr MakeFromData(sk_sp data); + /// Computes the CRC of the data in a PNG chunk. + static uint32_t ComputeCrc32(const uint8_t* data, size_t length); + private: static constexpr uint8_t kPngSignature[8] = {137, 80, 78, 71, 13, 10, 26, 10}; static constexpr size_t kChunkCrcSize = 4; + /// The size of the sequence number at the beginning of an fdAT chunk. + static constexpr size_t kFrameDataSequenceNumberSize = 4; + enum ChunkType { kImageHeaderChunkType = 'IHDR', kAnimationControlChunkType = 'acTL', diff --git a/engine/src/flutter/lib/ui/painting/image_generator_apng_unittests.cc b/engine/src/flutter/lib/ui/painting/image_generator_apng_unittests.cc new file mode 100644 index 0000000000000..4765004b180ae --- /dev/null +++ b/engine/src/flutter/lib/ui/painting/image_generator_apng_unittests.cc @@ -0,0 +1,127 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/lib/ui/painting/image_generator_apng.h" + +#include +#include +#include + +#include "flutter/lib/ui/painting/image_generator_registry.h" +#include "flutter/testing/testing.h" +#include "third_party/skia/include/core/SkData.h" + +namespace flutter { +namespace testing { + +namespace { + +// Writes a big-endian uint32_t to a buffer. +void WriteBE32(std::vector& buf, uint32_t val) { + buf.push_back((val >> 24) & 0xFF); + buf.push_back((val >> 16) & 0xFF); + buf.push_back((val >> 8) & 0xFF); + buf.push_back(val & 0xFF); +} + +// Writes a big-endian uint16_t to a buffer. +void WriteBE16(std::vector& buf, uint16_t val) { + buf.push_back((val >> 8) & 0xFF); + buf.push_back(val & 0xFF); +} + +// Appends a PNG chunk (length + type + data + CRC) to the buffer. +void AppendChunk(std::vector& buf, + const char type[4], + const std::vector& data) { + FML_CHECK(data.size() <= std::numeric_limits::max()); + WriteBE32(buf, static_cast(data.size())); + size_t type_start = buf.size(); + buf.insert(buf.end(), type, type + 4); + buf.insert(buf.end(), data.begin(), data.end()); + uint32_t crc = APNGImageGenerator::ComputeCrc32(buf.data() + type_start, + 4 + data.size()); + WriteBE32(buf, crc); +} + +// Builds a minimal valid APNG with a malicious fdAT chunk whose +// data_length is less than 4, which would trigger an integer underflow +// in DemuxNextImage() without the bounds check fix. +std::vector BuildMaliciousApng(uint32_t fdat_data_length) { + std::vector apng; + + // PNG signature + const uint8_t sig[] = {137, 80, 78, 71, 13, 10, 26, 10}; + apng.insert(apng.end(), sig, sig + 8); + + // IHDR: 1x1 RGBA, 8-bit + { + std::vector ihdr; + WriteBE32(ihdr, 1); // width + WriteBE32(ihdr, 1); // height + ihdr.push_back(8); // bit depth + ihdr.push_back(6); // color type (RGBA) + ihdr.push_back(0); // compression + ihdr.push_back(0); // filter + ihdr.push_back(0); // interlace + AppendChunk(apng, "IHDR", ihdr); + } + + // acTL: 1 frame, loop forever + { + std::vector actl; + WriteBE32(actl, 1); // num_frames + WriteBE32(actl, 0); // num_plays (0 = infinite) + AppendChunk(apng, "acTL", actl); + } + + // fcTL for frame 0 + { + std::vector fctl; + WriteBE32(fctl, 0); // sequence_number + WriteBE32(fctl, 1); // width + WriteBE32(fctl, 1); // height + WriteBE32(fctl, 0); // x_offset + WriteBE32(fctl, 0); // y_offset + WriteBE16(fctl, 1); // delay_num + WriteBE16(fctl, 10); // delay_den + fctl.push_back(0); // dispose_op + fctl.push_back(0); // blend_op + AppendChunk(apng, "fcTL", fctl); + } + + // Malicious fdAT for frame 0: data_length < 4 + // An fdAT chunk must have at least 4 bytes (sequence number). + // With data_length < 4, the subtraction in DemuxNextImage() underflows. + AppendChunk(apng, "fdAT", std::vector(fdat_data_length, 0)); + + // IEND + AppendChunk(apng, "IEND", {}); + + return apng; +} + +} // namespace + +// Verify that the APNG decoder can handle fdAT chunks whose length is shorter +// than the required 4-byte sequence number. +TEST(APNGImageGeneratorTest, FdATWithShortDataLengthDoesNotCrash) { + ImageGeneratorRegistry registry; + + auto make_generator = [](uint32_t fdat_length) -> auto { + auto apng_bytes = BuildMaliciousApng(fdat_length); + auto data = SkData::MakeWithCopy(apng_bytes.data(), apng_bytes.size()); + return APNGImageGenerator::MakeFromData(data); + }; + + // The decoder should reject fdAT chunks that are less than 4 bytes long. + EXPECT_EQ(make_generator(0), nullptr); + EXPECT_EQ(make_generator(2), nullptr); + + // Creating the generator should succeed if the fdAT has sufficient length. + EXPECT_NE(make_generator(4), nullptr); +} + +} // namespace testing +} // namespace flutter diff --git a/engine/src/flutter/lib/web_ui/dev/generate_builder_json.dart b/engine/src/flutter/lib/web_ui/dev/generate_builder_json.dart index ca28b5a930413..316115ca6338e 100644 --- a/engine/src/flutter/lib/web_ui/dev/generate_builder_json.dart +++ b/engine/src/flutter/lib/web_ui/dev/generate_builder_json.dart @@ -102,7 +102,7 @@ class GenerateBuilderJsonCommand extends Command { packageLock, 'Mac', BrowserName.safari, - specificOS: 'Mac-15.5|Mac-15.7', + specificOS: 'Mac-15.7', cpu: 'arm64', ), ]; diff --git a/engine/src/flutter/lib/web_ui/dev/steps/copy_artifacts_step.dart b/engine/src/flutter/lib/web_ui/dev/steps/copy_artifacts_step.dart index e95e0b3f0a4b3..5d94008deed48 100644 --- a/engine/src/flutter/lib/web_ui/dev/steps/copy_artifacts_step.dart +++ b/engine/src/flutter/lib/web_ui/dev/steps/copy_artifacts_step.dart @@ -55,8 +55,8 @@ class CopyArtifactsStep implements PipelineStep { 'Could not generate artifact bucket url for unknown realm.', ), }; - final url = Uri.https( - 'storage.googleapis.com', + final Uri url = Uri.https( + 'download.shorebird.dev', '${realmComponent}flutter_infra_release/flutter/${realm == LuciRealm.Try ? gitRevision : contentHash}/flutter-web-sdk.zip', ); final http.Response response = await http.Client().get(url); diff --git a/engine/src/flutter/runtime/BUILD.gn b/engine/src/flutter/runtime/BUILD.gn index 2f7aea3d0d107..259ad3b4b697f 100644 --- a/engine/src/flutter/runtime/BUILD.gn +++ b/engine/src/flutter/runtime/BUILD.gn @@ -114,10 +114,15 @@ source_set("runtime") { "//flutter/fml", "//flutter/lib/io", "//flutter/shell/common:display", + "//flutter/shell/common/shorebird:updater", "//flutter/skia", "//flutter/third_party/tonic", "//flutter/txt", ] + + if (is_ios) { + deps += [ "//flutter/runtime/shorebird:patch_cache" ] + } } if (enable_unittests) { diff --git a/engine/src/flutter/runtime/dart_isolate.cc b/engine/src/flutter/runtime/dart_isolate.cc index 29fa000131c8e..4fcacccd3ad26 100644 --- a/engine/src/flutter/runtime/dart_isolate.cc +++ b/engine/src/flutter/runtime/dart_isolate.cc @@ -1108,7 +1108,9 @@ Dart_Isolate DartIsolate::DartIsolateGroupCreateCallback( advisory_script_entrypoint, parent_group_data.GetChildIsolatePreparer(), parent_group_data.GetIsolateCreateCallback(), - parent_group_data.GetIsolateShutdownCallback()))); + parent_group_data.GetIsolateShutdownCallback(), + nullptr // native_assets_manager + ))); TaskRunners null_task_runners(advisory_script_uri, /* platform= */ nullptr, diff --git a/engine/src/flutter/runtime/dart_snapshot.cc b/engine/src/flutter/runtime/dart_snapshot.cc index 198a2e75a7edc..fc70f63a346f2 100644 --- a/engine/src/flutter/runtime/dart_snapshot.cc +++ b/engine/src/flutter/runtime/dart_snapshot.cc @@ -6,6 +6,7 @@ #include +#include #include "flutter/fml/native_library.h" #include "flutter/fml/paths.h" #include "flutter/fml/trace_event.h" @@ -13,6 +14,11 @@ #include "flutter/runtime/dart_vm.h" #include "third_party/dart/runtime/include/dart_api.h" +#if SHOREBIRD_USE_INTERPRETER +#include "flutter/runtime/shorebird/patch_cache.h" // nogncheck +#endif +#include "flutter/shell/common/shorebird/updater.h" // nogncheck + namespace flutter { const char* DartSnapshot::kVMDataSymbol = "kDartVmSnapshotData"; @@ -145,7 +151,20 @@ static std::shared_ptr ResolveIsolateData( nullptr, // release_func true // dontneed_safe ); -#else // DART_SNAPSHOT_STATIC_LINK +#else // DART_SNAPSHOT_STATIC_LINK + // Tell the Rust updater we're booting from whatever patch it selected. + // This copies next_boot โ†’ current_boot in the Rust state. The call is + // guarded inside Updater to execute at most once per process โ€” see the + // Updater class comment for why this matters in add-to-app and + // FlutterEngineGroup scenarios. + shorebird::Updater::Instance().ReportLaunchStart(); +#if SHOREBIRD_USE_INTERPRETER + // Try loading from a Shorebird patch first. + if (auto mapping = TryLoadFromPatch(settings.application_library_paths, + DartSnapshot::kIsolateDataSymbol)) { + return mapping; + } +#endif // SHOREBIRD_USE_INTERPRETER return SearchMapping( settings.isolate_snapshot_data, // embedder_mapping_callback settings.isolate_snapshot_data_path, // file_path @@ -165,7 +184,15 @@ static std::shared_ptr ResolveIsolateInstructions( nullptr, // release_func true // dontneed_safe ); -#else // DART_SNAPSHOT_STATIC_LINK +#else // DART_SNAPSHOT_STATIC_LINK +#if SHOREBIRD_USE_INTERPRETER + // Try loading from a Shorebird patch first. + if (auto mapping = + TryLoadFromPatch(settings.application_library_paths, + DartSnapshot::kIsolateInstructionsSymbol)) { + return mapping; + } +#endif // SHOREBIRD_USE_INTERPRETER return SearchMapping( settings.isolate_snapshot_instr, // embedder_mapping_callback settings.isolate_snapshot_instr_path, // file_path diff --git a/engine/src/flutter/runtime/runtime_controller.cc b/engine/src/flutter/runtime/runtime_controller.cc index 53f1684eea6ae..91cb3fee6dd4a 100644 --- a/engine/src/flutter/runtime/runtime_controller.cc +++ b/engine/src/flutter/runtime/runtime_controller.cc @@ -123,6 +123,24 @@ std::unique_ptr RuntimeController::Clone() const { ); } +// Shorebird: |Clone|, except the new controller launches isolates from +// |isolate_snapshot| instead of this controller's snapshot. Used by the +// production hot restart to relaunch the root isolate from a newly +// installed patch; see shell/common/shorebird/hot_restart.h. +std::unique_ptr RuntimeController::CloneWithSnapshot( + fml::RefPtr isolate_snapshot) const { + return std::make_unique(client_, // + vm_, // + std::move(isolate_snapshot), // + idle_notification_callback_, // + platform_data_, // + isolate_create_callback_, // + isolate_shutdown_callback_, // + persistent_isolate_data_, // + context_ // + ); +} + bool RuntimeController::FlushRuntimeStateToIsolate() { FML_DCHECK(!has_flushed_runtime_state_) << "FlushRuntimeStateToIsolate is called more than once somehow."; diff --git a/engine/src/flutter/runtime/runtime_controller.h b/engine/src/flutter/runtime/runtime_controller.h index 38a279bf3f511..d0cf5ff6d1039 100644 --- a/engine/src/flutter/runtime/runtime_controller.h +++ b/engine/src/flutter/runtime/runtime_controller.h @@ -184,6 +184,18 @@ class RuntimeController : public PlatformConfigurationClient, /// std::unique_ptr Clone() const; + //---------------------------------------------------------------------------- + /// @brief Shorebird: like |Clone|, but the cloned controller launches + /// isolates from |isolate_snapshot| instead of this + /// controller's snapshot. Used by the production hot restart + /// to relaunch the root isolate from a newly installed patch. + /// + /// @return A clone of the existing runtime controller with the given + /// isolate snapshot. + /// + std::unique_ptr CloneWithSnapshot( + fml::RefPtr isolate_snapshot) const; + //---------------------------------------------------------------------------- /// @brief Notify the isolate that a new view is available. /// diff --git a/engine/src/flutter/runtime/shorebird/BUILD.gn b/engine/src/flutter/runtime/shorebird/BUILD.gn new file mode 100644 index 0000000000000..1f856e36d3f48 --- /dev/null +++ b/engine/src/flutter/runtime/shorebird/BUILD.gn @@ -0,0 +1,16 @@ +import("//flutter/common/config.gni") + +source_set("patch_cache") { + sources = [ + "patch_cache.cc", + "patch_cache.h", + "patch_mapping.cc", + "patch_mapping.h", + ] + + deps = [ + "//flutter/fml", + "//flutter/runtime:libdart", + "//flutter/shell/common/shorebird:updater", + ] +} diff --git a/engine/src/flutter/runtime/shorebird/patch_cache.cc b/engine/src/flutter/runtime/shorebird/patch_cache.cc new file mode 100644 index 0000000000000..8c5cc3a83f182 --- /dev/null +++ b/engine/src/flutter/runtime/shorebird/patch_cache.cc @@ -0,0 +1,165 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/runtime/shorebird/patch_cache.h" + +#include + +#include "flutter/fml/logging.h" +#include "flutter/fml/mapping.h" +#include "flutter/runtime/shorebird/patch_mapping.h" +#include "third_party/dart/runtime/include/dart_api.h" + +namespace flutter { + +namespace { + +// These symbol names match the constants in dart_snapshot.cc. +// We duplicate them here rather than extracting them into a header. +// They are actually defined down in Dart and will never change. +constexpr const char* kIsolateDataSymbol = "kDartIsolateSnapshotData"; +constexpr const char* kIsolateInstructionsSymbol = + "kDartIsolateSnapshotInstructions"; + +} // namespace + +// PatchCacheEntry implementation + +std::shared_ptr PatchCacheEntry::Create( + const std::string& path) { + // vmcode files currently use ELF internally after a prefix of a Shorebird + // linker header. + auto elf_mapping = fml::FileMapping::CreateReadOnly(path); + if (!elf_mapping) { + FML_LOG(ERROR) << "Failed to map file: " << path; + return nullptr; + } + + int elf_file_offset = Shorebird_ReadLinkHeader(elf_mapping->GetMapping(), + elf_mapping->GetSize()); + + const char* error = nullptr; + // The VM Snapshot is identical for all binaries produced by a given version + // of Dart. Our linker checks this and will fail to link if ever the VM + // snapshot changes. We ignore the VM data/instrs here. + const uint8_t* ignored_vm_data = nullptr; + const uint8_t* ignored_vm_instrs = nullptr; + const uint8_t* isolate_data = nullptr; + const uint8_t* isolate_instrs = nullptr; + + Dart_LoadedElf* elf = Dart_LoadELF( + path.c_str(), elf_file_offset, &error, &ignored_vm_data, + &ignored_vm_instrs, &isolate_data, &isolate_instrs, dart::bin::kReadOnly); + + if (elf == nullptr) { + FML_LOG(ERROR) << "Failed to load patch at " << path << " error: " << error; + return nullptr; + } + + FML_LOG(INFO) << "Loaded patch from " << path; + + return std::shared_ptr( + new PatchCacheEntry(path, elf, isolate_data, isolate_instrs)); +} + +PatchCacheEntry::PatchCacheEntry(const std::string& path, + Dart_LoadedElf* elf, + const uint8_t* isolate_data, + const uint8_t* isolate_instrs) + : path_(path), + elf_(elf), + isolate_data_(isolate_data), + isolate_instrs_(isolate_instrs) {} + +PatchCacheEntry::~PatchCacheEntry() { + if (elf_ != nullptr) { + FML_LOG(INFO) << "Unloading patch from " << path_; + Dart_UnloadELF(elf_); + elf_ = nullptr; + } +} + +PatchCache& PatchCache::Instance() { + static PatchCache instance; + return instance; +} + +std::shared_ptr PatchCache::GetOrLoad( + const std::string& path) { + std::lock_guard lock(mutex_); + + // Check if we have a cached entry that's still alive + auto it = cache_.find(path); + if (it != cache_.end()) { + if (auto entry = it->second.lock()) { + FML_LOG(INFO) << "PatchCache hit for " << path; + return entry; + } + // Entry expired, remove it + cache_.erase(it); + } + + // Load a new entry + auto entry = PatchCacheEntry::Create(path); + if (entry) { + cache_[path] = entry; // Store weak_ptr + } + + return entry; +} + +void PatchCache::PruneExpired() { + std::lock_guard lock(mutex_); + + for (auto it = cache_.begin(); it != cache_.end();) { + if (it->second.expired()) { + it = cache_.erase(it); + } else { + ++it; + } + } +} + +std::shared_ptr TryLoadFromPatch( + const std::vector& native_library_paths, + const char* symbol_name) { + if (native_library_paths.empty()) { + return nullptr; + } + + // Check if the first path is a Shorebird patch (.vmcode file) + const auto& patch_path = native_library_paths.front(); + bool is_patch = patch_path.find(".vmcode") != std::string::npos; + if (!is_patch) { + return nullptr; + } + + // Patches only contain isolate data/instructions, not VM data/instructions. + // Return nullptr for VM symbols to allow fallback to the base app. + std::string symbol(symbol_name); + if (symbol != kIsolateDataSymbol && symbol != kIsolateInstructionsSymbol) { + return nullptr; + } + + // Load the patch using the cache. + auto cache_entry = PatchCache::Instance().GetOrLoad(patch_path); + if (!cache_entry) { + FML_LOG(FATAL) << "Failed to load symbol from patch at " << patch_path; + return nullptr; + } + + FML_LOG(INFO) << "Loading symbol from patch: " << symbol_name; + + // ReportLaunchStart is now called from ResolveIsolateData in + // dart_snapshot.cc, which runs before TryLoadFromPatch on all platforms. + + if (symbol == kIsolateDataSymbol) { + return PatchMapping::CreateIsolateData(cache_entry); + } else { + FML_CHECK(symbol == kIsolateInstructionsSymbol); + return PatchMapping::CreateIsolateInstructions(cache_entry); + } +} + +} // namespace flutter diff --git a/engine/src/flutter/runtime/shorebird/patch_cache.h b/engine/src/flutter/runtime/shorebird/patch_cache.h new file mode 100644 index 0000000000000..1f2e14fab711d --- /dev/null +++ b/engine/src/flutter/runtime/shorebird/patch_cache.h @@ -0,0 +1,104 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_RUNTIME_SHOREBIRD_PATCH_CACHE_H_ +#define FLUTTER_RUNTIME_SHOREBIRD_PATCH_CACHE_H_ + +#include +#include +#include +#include +#include + +#include + +#include "flutter/fml/macros.h" +#include "flutter/fml/mapping.h" + +namespace flutter { + +/// A cache entry that holds a loaded patch file and its extracted snapshot +/// pointers. The patch is automatically unloaded when the last reference to +/// this entry is released. +class PatchCacheEntry { + public: + /// Creates a new cache entry by loading the patch file at the given path. + /// Returns nullptr if loading fails. + static std::shared_ptr Create(const std::string& path); + + ~PatchCacheEntry(); + + /// Returns the isolate snapshot data pointer. + const uint8_t* isolate_data() const { return isolate_data_; } + + /// Returns the isolate snapshot instructions pointer. + const uint8_t* isolate_instructions() const { return isolate_instrs_; } + + /// Returns the path this entry was loaded from. + const std::string& path() const { return path_; } + + private: + PatchCacheEntry(const std::string& path, + Dart_LoadedElf* elf, + const uint8_t* isolate_data, + const uint8_t* isolate_instrs); + + std::string path_; + Dart_LoadedElf* elf_; + const uint8_t* isolate_data_; + const uint8_t* isolate_instrs_; + + FML_DISALLOW_COPY_AND_ASSIGN(PatchCacheEntry); +}; + +/// A thread-safe cache for loaded patch files. Cache entries are automatically +/// removed when all references to them are released. +class PatchCache { + public: + /// Returns the singleton instance of the cache. + static PatchCache& Instance(); + + /// Gets or loads a patch file at the given path. If the file is already + /// cached and the entry is still alive, returns the existing entry. + /// Otherwise, loads the file and creates a new cache entry. + /// Returns nullptr if loading fails. + std::shared_ptr GetOrLoad(const std::string& path); + + /// Removes expired entries from the cache. This is called automatically + /// by GetOrLoad, but can also be called explicitly. + void PruneExpired(); + + private: + PatchCache() = default; + ~PatchCache() = default; + + std::mutex mutex_; + // We store weak references so entries are automatically cleaned up when + // all ElfMapping instances release their references. + std::map> cache_; + + FML_DISALLOW_COPY_AND_ASSIGN(PatchCache); +}; + +/// Checks if the first path in native_library_paths is a Shorebird patch +/// (.vmcode file) and if so, attempts to load the requested symbol from +/// the patch. +/// +/// @param native_library_paths The list of library paths to check. The first +/// path is checked for the .vmcode extension. +/// @param symbol_name The symbol to load (kIsolateDataSymbol or +/// kIsolateInstructionsSymbol). +/// @return A mapping for the requested symbol if this is a patch and the +/// symbol is available in the patch, nullptr otherwise. +/// +/// Note: Patches only contain isolate data/instructions, not VM +/// data/instructions. For VM symbols, this will always return nullptr, +/// allowing the caller to fall back to loading from the base app. +std::shared_ptr TryLoadFromPatch( + const std::vector& native_library_paths, + const char* symbol_name); + +} // namespace flutter + +#endif // FLUTTER_RUNTIME_SHOREBIRD_PATCH_CACHE_H_ diff --git a/engine/src/flutter/runtime/shorebird/patch_mapping.cc b/engine/src/flutter/runtime/shorebird/patch_mapping.cc new file mode 100644 index 0000000000000..d444cda817c34 --- /dev/null +++ b/engine/src/flutter/runtime/shorebird/patch_mapping.cc @@ -0,0 +1,51 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/runtime/shorebird/patch_mapping.h" + +#include "third_party/dart/runtime/include/dart_native_api.h" + +namespace flutter { + +std::shared_ptr PatchMapping::CreateIsolateData( + std::shared_ptr entry) { + if (!entry) { + return nullptr; + } + const uint8_t* data = entry->isolate_data(); + size_t size = Dart_SnapshotDataSize(data); + return std::shared_ptr(new PatchMapping(entry, data, size)); +} + +std::shared_ptr PatchMapping::CreateIsolateInstructions( + std::shared_ptr entry) { + if (!entry) { + return nullptr; + } + const uint8_t* data = entry->isolate_instructions(); + size_t size = Dart_SnapshotInstrSize(data); + return std::shared_ptr(new PatchMapping(entry, data, size)); +} + +PatchMapping::PatchMapping(std::shared_ptr entry, + const uint8_t* data, + size_t size) + : cache_entry_(std::move(entry)), data_(data), size_(size) {} + +PatchMapping::~PatchMapping() = default; + +size_t PatchMapping::GetSize() const { + return size_; +} + +const uint8_t* PatchMapping::GetMapping() const { + return data_; +} + +bool PatchMapping::IsDontNeedSafe() const { + // Patch mappings are file-backed and safe for madvise(DONTNEED). + return true; +} + +} // namespace flutter diff --git a/engine/src/flutter/runtime/shorebird/patch_mapping.h b/engine/src/flutter/runtime/shorebird/patch_mapping.h new file mode 100644 index 0000000000000..8c2e494a9004c --- /dev/null +++ b/engine/src/flutter/runtime/shorebird/patch_mapping.h @@ -0,0 +1,55 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_RUNTIME_SHOREBIRD_PATCH_MAPPING_H_ +#define FLUTTER_RUNTIME_SHOREBIRD_PATCH_MAPPING_H_ + +#include + +#include "flutter/fml/macros.h" +#include "flutter/fml/mapping.h" +#include "flutter/runtime/shorebird/patch_cache.h" + +namespace flutter { + +/// A Mapping implementation that references data from a cached patch file. +/// Holding a reference to this mapping keeps the underlying patch loaded. +class PatchMapping final : public fml::Mapping { + public: + /// Creates a mapping for the isolate snapshot data from the given cache + /// entry. + static std::shared_ptr CreateIsolateData( + std::shared_ptr entry); + + /// Creates a mapping for the isolate snapshot instructions from the given + /// cache entry. + static std::shared_ptr CreateIsolateInstructions( + std::shared_ptr entry); + + ~PatchMapping() override; + + // |fml::Mapping| + size_t GetSize() const override; + + // |fml::Mapping| + const uint8_t* GetMapping() const override; + + // |fml::Mapping| + bool IsDontNeedSafe() const override; + + private: + PatchMapping(std::shared_ptr entry, + const uint8_t* data, + size_t size); + + std::shared_ptr cache_entry_; + const uint8_t* data_; + size_t size_; + + FML_DISALLOW_COPY_AND_ASSIGN(PatchMapping); +}; + +} // namespace flutter + +#endif // FLUTTER_RUNTIME_SHOREBIRD_PATCH_MAPPING_H_ diff --git a/engine/src/flutter/shell/common/BUILD.gn b/engine/src/flutter/shell/common/BUILD.gn index e1aba738f17be..e6ab962ff4106 100644 --- a/engine/src/flutter/shell/common/BUILD.gn +++ b/engine/src/flutter/shell/common/BUILD.gn @@ -114,6 +114,8 @@ source_set("common") { "shell.h", "shell_io_manager.cc", "shell_io_manager.h", + "shorebird/hot_restart.cc", + "shorebird/hot_restart.h", "skia_event_tracer_impl.cc", "skia_event_tracer_impl.h", "snapshot_controller.cc", @@ -153,6 +155,7 @@ source_set("common") { "//flutter/lib/ui", "//flutter/runtime", "//flutter/shell/common:base64", + "//flutter/shell/common/shorebird:updater", "//flutter/shell/geometry", "//flutter/shell/profiling", "//flutter/skia", @@ -342,6 +345,7 @@ if (enable_unittests) { "//flutter/common/graphics", "//flutter/display_list/testing:display_list_testing", "//flutter/shell/common:base64", + "//flutter/shell/common/shorebird:updater", "//flutter/shell/profiling:profiling_unittests", "//flutter/shell/version", "//flutter/testing:fixture_test", diff --git a/engine/src/flutter/shell/common/engine.cc b/engine/src/flutter/shell/common/engine.cc index fd0915c1accb6..0285f32e55aff 100644 --- a/engine/src/flutter/shell/common/engine.cc +++ b/engine/src/flutter/shell/common/engine.cc @@ -214,6 +214,24 @@ bool Engine::Restart(RunConfiguration configuration) { return Run(std::move(configuration)) == Engine::RunStatus::Success; } +// Shorebird: |Restart|, but the relaunched isolate boots from +// |isolate_snapshot| (a newly installed patch) instead of the snapshot this +// engine booted with. +bool Engine::RestartWithSnapshot( + RunConfiguration configuration, + fml::RefPtr isolate_snapshot) { + TRACE_EVENT0("flutter", "Engine::RestartWithSnapshot"); + if (!configuration.IsValid()) { + FML_LOG(ERROR) << "Engine run configuration was invalid."; + return false; + } + delegate_.OnPreEngineRestart(); + runtime_controller_ = + runtime_controller_->CloneWithSnapshot(std::move(isolate_snapshot)); + UpdateAssetManager(nullptr); + return Run(std::move(configuration)) == Engine::RunStatus::Success; +} + Engine::RunStatus Engine::Run(RunConfiguration configuration) { if (!configuration.IsValid()) { FML_LOG(ERROR) << "Engine run configuration was invalid."; @@ -222,8 +240,11 @@ Engine::RunStatus Engine::Run(RunConfiguration configuration) { last_entry_point_ = configuration.GetEntrypoint(); last_entry_point_library_ = configuration.GetEntrypointLibrary(); -#if (FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG) - // This is only used to support restart. +// This is only used to support restart. Shorebird: also retained in release +// on supported platforms, where the production hot restart relaunches the +// isolate with the same entrypoint arguments. +#if (FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG) || \ + SHOREBIRD_PLATFORM_SUPPORTED last_entry_point_args_ = configuration.GetEntrypointArgs(); #endif diff --git a/engine/src/flutter/shell/common/engine.h b/engine/src/flutter/shell/common/engine.h index e07f7d4f5697c..fe41a8c34542d 100644 --- a/engine/src/flutter/shell/common/engine.h +++ b/engine/src/flutter/shell/common/engine.h @@ -505,6 +505,20 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { /// [[nodiscard]] bool Restart(RunConfiguration configuration); + //---------------------------------------------------------------------------- + /// @brief Shorebird: like |Restart|, but relaunches the root isolate + /// from the given isolate snapshot instead of the one this + /// engine booted with. Used by the production hot restart to + /// boot a newly installed patch; see + /// shell/common/shorebird/hot_restart.h. + /// + /// @attention Like |Restart|, a non-successful restart still tears down + /// the existing root isolate. + /// + [[nodiscard]] bool RestartWithSnapshot( + RunConfiguration configuration, + fml::RefPtr isolate_snapshot); + //---------------------------------------------------------------------------- /// @brief Setup default font manager according to specific platform. /// diff --git a/engine/src/flutter/shell/common/shell.cc b/engine/src/flutter/shell/common/shell.cc index 4e920cbbb5115..c129c81ff9a1a 100644 --- a/engine/src/flutter/shell/common/shell.cc +++ b/engine/src/flutter/shell/common/shell.cc @@ -47,6 +47,8 @@ #include "third_party/skia/include/core/SkGraphics.h" #include "third_party/tonic/common/log.h" +#include "flutter/shell/common/shorebird/updater.h" + namespace flutter { constexpr char kSkiaChannel[] = "flutter/skia"; @@ -522,6 +524,18 @@ Shell::Shell(DartVMRef vm, is_gpu_disabled_sync_switch_(new fml::SyncSwitch(is_gpu_disabled)), weak_factory_gpu_(nullptr), weak_factory_(this) { + // Report launch outcome to the Shorebird updater for crash recovery. + // If the VM failed to start, we report failure so the updater can roll + // back the patch. These calls are guarded inside Updater to execute at + // most once per process โ€” only the first Shell's outcome is reported. + // In add-to-app, subsequent engines are silently ignored since they + // boot from the same snapshot that was already reported on. + // On unsupported platforms, NoOpUpdater handles these calls gracefully. + if (!vm_) { + shorebird::Updater::Instance().ReportLaunchFailure(); + } else { + shorebird::Updater::Instance().ReportLaunchSuccess(); + } FML_CHECK(!settings.enable_software_rendering || !settings.enable_impeller) << "Software rendering is incompatible with Impeller."; if (!settings.enable_impeller && settings.warn_on_impeller_opt_out) { @@ -603,6 +617,10 @@ Shell::Shell(DartVMRef vm, } Shell::~Shell() { + // Shorebird: stop servicing hot restart requests. Blocks until any + // in-flight request against this shell has finished scheduling. + ShorebirdUnregisterRestartHost(); + #if !SLIMPELLER PersistentCache::GetCacheForProcess()->RemoveWorkerTaskRunner( task_runners_.GetIOTaskRunner()); @@ -903,6 +921,10 @@ bool Shell::Setup(std::unique_ptr platform_view, is_set_up_ = true; + // Shorebird: allow package:shorebird_code_push to hot restart this shell + // in production. No-op unless eligible; see hot_restart.cc. + ShorebirdRegisterRestartHost(); + #if !SLIMPELLER PersistentCache::GetCacheForProcess()->AddWorkerTaskRunner( task_runners_.GetIOTaskRunner()); diff --git a/engine/src/flutter/shell/common/shell.h b/engine/src/flutter/shell/common/shell.h index a43f3ecda4cea..9bedcf570e571 100644 --- a/engine/src/flutter/shell/common/shell.h +++ b/engine/src/flutter/shell/common/shell.h @@ -445,6 +445,13 @@ class Shell final : public PlatformView::Delegate, static std::pair> InferVmInitDataFromSettings(Settings& settings); + // Shorebird: hot restart host registration. Declared here because the + // implementations need this shell's settings, VM, and engine; defined in + // shell/common/shorebird/hot_restart.cc to keep the logic out of this + // file. + void ShorebirdRegisterRestartHost(); + void ShorebirdUnregisterRestartHost(); + private: using ServiceProtocolHandler = std::function(); + auto* mock_ptr = mock.get(); + shorebird::Updater::SetInstanceForTesting(std::move(mock)); + shorebird::Updater::ResetLaunchStateForTesting(); + + auto settings = CreateSettingsForFixture(); + auto task_runners = GetTaskRunnersForFixture(); + auto shell = CreateShell(settings, task_runners); + ASSERT_TRUE(shell); + + const auto& log = mock_ptr->call_log(); + ASSERT_EQ(log.size(), 2u); + EXPECT_EQ(log[0], "ReportLaunchStart"); + EXPECT_EQ(log[1], "ReportLaunchSuccess"); + + DestroyShell(std::move(shell), task_runners); + shorebird::Updater::ResetLaunchStateForTesting(); + shorebird::Updater::ResetInstanceForTesting(); +} + +// In add-to-app, multiple engines may be created within a single process. +// Only the first engine should report launch start/success to the Rust +// updater. This prevents the updater from promoting a newly-downloaded patch +// to "current_boot" when subsequent engines are still running the original +// snapshot that was selected at process init time. +TEST_F(ShellTest, ShorebirdUpdaterReportsOnlyOnceForMultipleShells) { + auto mock = std::make_unique(); + auto* mock_ptr = mock.get(); + shorebird::Updater::SetInstanceForTesting(std::move(mock)); + shorebird::Updater::ResetLaunchStateForTesting(); + + auto settings = CreateSettingsForFixture(); + + // Create first shell โ€” gets Start + Success + auto task_runners1 = GetTaskRunnersForFixture(); + auto shell1 = CreateShell(settings, task_runners1); + ASSERT_TRUE(shell1); + EXPECT_EQ(mock_ptr->launch_start_count(), 1); + EXPECT_EQ(mock_ptr->launch_success_count(), 1); + + // Create second shell โ€” guarded, no additional Start or Success calls. + auto task_runners2 = GetTaskRunnersForFixture(); + auto shell2 = CreateShell(settings, task_runners2); + ASSERT_TRUE(shell2); + EXPECT_EQ(mock_ptr->launch_start_count(), 1); + EXPECT_EQ(mock_ptr->launch_success_count(), 1); + + // Only one Start+Success pair in the call log. + const auto& log = mock_ptr->call_log(); + ASSERT_EQ(log.size(), 2u); + EXPECT_EQ(log[0], "ReportLaunchStart"); + EXPECT_EQ(log[1], "ReportLaunchSuccess"); + + DestroyShell(std::move(shell1), task_runners1); + DestroyShell(std::move(shell2), task_runners2); + + shorebird::Updater::ResetLaunchStateForTesting(); + shorebird::Updater::ResetInstanceForTesting(); +} + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/shell/common/shorebird/BUILD.gn b/engine/src/flutter/shell/common/shorebird/BUILD.gn new file mode 100644 index 0000000000000..83d433b59872e --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/BUILD.gn @@ -0,0 +1,165 @@ +import("//flutter/common/config.gni") +import("//flutter/shell/common/shorebird/build_rust_updater.gni") +import("//flutter/testing/testing.gni") + +if (is_ios || is_mac) { + import("//build/config/darwin/darwin_sdk.gni") +} + +source_set("snapshots_data_handle") { + sources = [ + "snapshots_data_handle.cc", + "snapshots_data_handle.h", + ] + + deps = [ + "//flutter/fml", + "//flutter/runtime", + "//flutter/runtime:libdart", + "//flutter/shell/common", + ] +} + +if (shorebird_updater_supported) { + action("build_rust_updater") { + script = "//flutter/shell/common/shorebird/build_rust_updater.py" + + # The stamp file gives Ninja a deterministic output to depend on, but + # we also have to declare the actual static library so consumers + # referencing it via `libs = [ shorebird_updater_output_lib ]` find a + # rule that produces it. Without the lib in `outputs`, Ninja errors + # with `'cargo_target/.../libupdater.a' missing and no known rule to + # make it` once the cargo target dir lives inside root_out_dir (#124). + _stamp = + "$target_gen_dir/rust_updater_${shorebird_updater_rust_target}.stamp" + outputs = [ + _stamp, + shorebird_updater_output_lib, + ] + + args = [ + "--rust-target", + shorebird_updater_rust_target, + "--manifest-dir", + rebase_path("$shorebird_updater_dir", root_build_dir), + "--target-dir", + rebase_path("$shorebird_updater_target_dir", root_build_dir), + "--output-lib", + rebase_path("$shorebird_updater_output_lib", root_build_dir), + "--stamp", + rebase_path(_stamp, root_build_dir), + ] + + if (is_android) { + args += [ + "--ndk-path", + rebase_path(android_ndk_root, root_build_dir), + "--android-api-level", + "$android_api_level", + ] + } + + if (is_ios) { + # Thread the engine's iOS deployment target into the cargo build so + # the cc crate (compiling C deps like zstd-sys) and rustc's cdylib + # link step both target the same iOS version as the C++ engine. + # Without this, cargo defaults to whatever rustc/cc bake in (rustc's + # built-in target spec for aarch64-apple-ios is iOS 10/11, while cc + # picks up the host SDK), which causes link warnings and unresolved + # symbols like ___chkstk_darwin. + args += [ + "--ios-deployment-target", + ios_deployment_target, + ] + } + + if (is_mac) { + args += [ + "--mac-deployment-target", + mac_deployment_target, + ] + } + + # Declare inputs so Ninja knows when to re-run the action. + inputs = [ + "$shorebird_updater_dir/Cargo.toml", + "$shorebird_updater_dir/Cargo.lock", + "$shorebird_updater_dir/library/Cargo.toml", + "$shorebird_updater_dir/library/build.rs", + "$shorebird_updater_dir/library/cbindgen_dart.toml", + "$shorebird_updater_dir/library/cbindgen_engine.toml", + "$shorebird_updater_dir/library/.cargo/config.toml", + ] + inputs += shorebird_updater_rs_sources + } +} + +# C++ wrapper around the Rust updater C API. +# This provides a testable abstraction layer that can be mocked for testing. +source_set("updater") { + sources = [ + "updater.cc", + "updater.h", + ] + + deps = [ "//flutter/fml" ] + + # For the Rust updater C API (shorebird_report_launch_start, etc.) + include_dirs = [ "//flutter" ] + + if (shorebird_updater_supported) { + deps += [ ":build_rust_updater" ] + libs = [ shorebird_updater_output_lib ] + + if (is_win) { + libs += [ "userenv.lib" ] + } + } +} + +source_set("shorebird") { + sources = [ + "shorebird.cc", + "shorebird.h", + ] + + deps = [ + ":snapshots_data_handle", + ":updater", + "//flutter/fml", + "//flutter/runtime", + "//flutter/runtime:libdart", + "//flutter/shell/common", + "//flutter/shell/platform/embedder:embedder_headers", + ] +} + +if (enable_unittests) { + test_fixtures("shorebird_fixtures") { + fixtures = [] + } + + executable("shorebird_unittests") { + testonly = true + + sources = [ + "hot_restart_unittests.cc", + "patch_cache_unittests.cc", + "shorebird_unittests.cc", + "snapshots_data_handle_unittests.cc", + "updater_unittests.cc", + ] + + deps = [ + ":shorebird", + ":shorebird_fixtures", + ":snapshots_data_handle", + ":updater", + "//flutter/runtime", + "//flutter/runtime/shorebird:patch_cache", + "//flutter/shell/common", + "//flutter/testing", + "//flutter/testing:fixture_test", + ] + } +} diff --git a/engine/src/flutter/shell/common/shorebird/build_rust_updater.gni b/engine/src/flutter/shell/common/shorebird/build_rust_updater.gni new file mode 100644 index 0000000000000..08b72424af7b6 --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/build_rust_updater.gni @@ -0,0 +1,86 @@ +# Copyright 2024 The Shorebird Authors. All rights reserved. +# Use of this source code is governed by a MIT-style license that can be +# found in the LICENSE file. + +# Computes variables needed to build and link the Rust updater library. +# +# Exported variables: +# shorebird_updater_supported - Whether the current platform is supported. +# shorebird_updater_output_lib - Path to the built static library. +# shorebird_updater_rust_target - Rust target triple for the current platform. +# shorebird_updater_rs_sources - List of .rs source files (for GN inputs). + +if (is_android) { + import("//build/config/android/config.gni") +} + +shorebird_updater_dir = "//flutter/third_party/updater" + +# Compute the Rust target triple from the GN target_os/target_cpu. GN +# evaluates BUILD files under every toolchain in use, not just the +# top-level one, so we get called for host/sub toolchains too. Any +# (os, cpu) combo we don't explicitly handle leaves the rust target +# empty and marks the updater unsupported for that toolchain, which is +# the correct behavior โ€” the updater only needs to be built for +# toolchains whose final binary is the engine. +shorebird_updater_rust_target = "" + +if (is_android) { + if (target_cpu == "arm") { + shorebird_updater_rust_target = "armv7-linux-androideabi" + } else if (target_cpu == "arm64") { + shorebird_updater_rust_target = "aarch64-linux-android" + } else if (target_cpu == "x64") { + shorebird_updater_rust_target = "x86_64-linux-android" + } else if (target_cpu == "x86") { + shorebird_updater_rust_target = "i686-linux-android" + } +} else if (is_ios) { + if (target_cpu == "arm64") { + shorebird_updater_rust_target = "aarch64-apple-ios" + } else if (target_cpu == "x64") { + shorebird_updater_rust_target = "x86_64-apple-ios" + } +} else if (is_mac) { + if (target_cpu == "arm64") { + shorebird_updater_rust_target = "aarch64-apple-darwin" + } else if (target_cpu == "x64") { + shorebird_updater_rust_target = "x86_64-apple-darwin" + } +} else if (is_win) { + if (target_cpu == "x64") { + shorebird_updater_rust_target = "x86_64-pc-windows-msvc" + } +} else if (is_linux) { + if (target_cpu == "x64") { + shorebird_updater_rust_target = "x86_64-unknown-linux-gnu" + } +} + +shorebird_updater_supported = shorebird_updater_rust_target != "" + +if (shorebird_updater_supported) { + if (is_win) { + _updater_lib_name = "updater.lib" + } else { + _updater_lib_name = "libupdater.a" + } + + # Cargo target dir lives inside the GN build output dir, not inside the + # source tree. This means: + # - `rm -rf out/` clobbers compiled rust artifacts the same + # way it clobbers compiled C++ artifacts; no special-case cleanup + # in checkout.sh. + # - Each GN config (debug/release/ios/android/etc.) gets its own + # cargo target dir, so they don't step on each other's rlibs. + # - The source checkout stays clean, which is what GN/ninja expects. + shorebird_updater_target_dir = "$root_out_dir/cargo_target" + shorebird_updater_output_lib = "$shorebird_updater_target_dir/$shorebird_updater_rust_target/release/$_updater_lib_name" + + # Glob all .rs source files so the input list stays in sync automatically. + shorebird_updater_rs_sources = + exec_script("//flutter/shell/common/shorebird/list_rust_files.py", + [ rebase_path("$shorebird_updater_dir/library/src", + root_build_dir) ], + "list lines") +} diff --git a/engine/src/flutter/shell/common/shorebird/build_rust_updater.py b/engine/src/flutter/shell/common/shorebird/build_rust_updater.py new file mode 100644 index 0000000000000..aa4d66fe3aed3 --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/build_rust_updater.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +# Copyright 2024 The Shorebird Authors. All rights reserved. +# Use of this source code is governed by a MIT-style license that can be +# found in the LICENSE file. + +"""Build the Rust updater library via cargo, invoked as a GN action.""" + +import argparse +import os +import subprocess +import sys + + +def main(): + parser = argparse.ArgumentParser(description='Build the Rust updater static library.') + parser.add_argument( + '--rust-target', required=True, help='Rust target triple (e.g. aarch64-linux-android)' + ) + parser.add_argument( + '--manifest-dir', required=True, help='Directory containing the workspace Cargo.toml' + ) + parser.add_argument( + '--target-dir', + required=True, + help='Cargo target directory; should live inside the GN build output dir', + ) + parser.add_argument('--output-lib', required=True, help='Expected output library path') + parser.add_argument('--stamp', required=True, help='Stamp file to write on success') + parser.add_argument('--ndk-path', help='Path to the Android NDK (required for Android targets)') + parser.add_argument( + '--android-api-level', type=int, help='Android API level (required for Android targets)' + ) + parser.add_argument( + '--ios-deployment-target', + help='iOS deployment target (e.g. 13.0); required for *-apple-ios targets', + ) + parser.add_argument( + '--mac-deployment-target', + help='macOS deployment target (e.g. 10.15); required for *-apple-darwin targets', + ) + args = parser.parse_args() + + env = os.environ.copy() + + is_android = 'android' in args.rust_target + is_apple_ios = 'apple-ios' in args.rust_target + is_apple_darwin = 'apple-darwin' in args.rust_target + is_msvc = 'pc-windows-msvc' in args.rust_target + + if is_android: + if not args.ndk_path or not args.android_api_level: + print( + 'ERROR: --ndk-path and --android-api-level are required for ' + 'Android targets.', + file=sys.stderr + ) + return 1 + _configure_android_env(env, args.rust_target, args.ndk_path, args.android_api_level) + + if is_apple_ios: + if not args.ios_deployment_target: + print( + 'ERROR: --ios-deployment-target is required for *-apple-ios targets.', + file=sys.stderr, + ) + return 1 + # Setting IPHONEOS_DEPLOYMENT_TARGET makes both the cc crate (compiling + # transitive C deps like zstd-sys) and rustc's cdylib link step honor + # the engine's iOS deployment target instead of falling back to their + # respective defaults (host SDK for cc, target-spec default for rustc). + env['IPHONEOS_DEPLOYMENT_TARGET'] = args.ios_deployment_target + + if is_apple_darwin: + if not args.mac_deployment_target: + print( + 'ERROR: --mac-deployment-target is required for *-apple-darwin targets.', + file=sys.stderr, + ) + return 1 + env['MACOSX_DEPLOYMENT_TARGET'] = args.mac_deployment_target + + if is_msvc: + _configure_msvc_env(env, args.rust_target) + + # GN passes paths relative to the build output dir (which is cwd when + # Ninja runs the action). Resolve them to absolute paths so they work + # regardless of cargo's working directory. + manifest_path = os.path.abspath(os.path.join(args.manifest_dir, 'Cargo.toml')) + target_dir = os.path.abspath(args.target_dir) + output_lib = os.path.abspath(args.output_lib) + + cmd = [ + 'cargo', + 'build', + '--release', + '--target', + args.rust_target, + '--manifest-path', + manifest_path, + '--target-dir', + target_dir, + '-p', + 'updater', + ] + + print(f'Running: {" ".join(cmd)}', flush=True) + result = subprocess.run(cmd, env=env) + if result.returncode != 0: + print(f'ERROR: cargo build failed with exit code {result.returncode}', file=sys.stderr) + return result.returncode + + if not os.path.exists(output_lib): + print(f'ERROR: Expected output library not found: {output_lib}', file=sys.stderr) + return 1 + + # Write stamp file to signal success to Ninja. + with open(args.stamp, 'w') as f: + f.write('') + + return 0 + + +def _configure_msvc_env(env, rust_target): + """Force the cc crate to compile transitive C deps with the static CRT. + + The engine's Windows build uses the static CRT (/MT). The updater's + .cargo/config.toml sets `-C target-feature=+crt-static` so the rlib is + compiled to expect static-CRT linkage. However, cargo populates + CARGO_CFG_TARGET_FEATURE from the rustc target spec's default features, + not from user rustflags, so +crt-static is invisible to build scripts. + The cc crate (used by transitive *-sys deps like zstd-sys to compile + their C sources) therefore falls back to /MD, producing .obj files + full of __imp_* references that the engine's /MT link cannot resolve + (e.g. __imp_clock, __imp__wassert, __imp_qsort_s). + + Force /MT via the per-target CFLAGS env that the cc crate honors. cc + appends these flags at the end of its command line, so cl.exe sees + `/MD ... /MT` and emits warning D9025 ("overriding '/MD' with '/MT'") + and uses the last one -- /MT wins. + """ + triple_env = rust_target.replace('-', '_') + env[f'CFLAGS_{triple_env}'] = '/MT' + env[f'CXXFLAGS_{triple_env}'] = '/MT' + + +def _configure_android_env(env, rust_target, ndk_path, api_level): + """Set environment variables so cargo can cross-compile for Android.""" + # GN passes paths relative to the build output dir. Resolve to absolute + # so that cargo and the cc crate can find the NDK tools. + ndk_path = os.path.abspath(ndk_path) + + # Determine the host platform tag for NDK toolchain paths. + if sys.platform.startswith('linux'): + host_tag = 'linux-x86_64' + elif sys.platform == 'darwin': + host_tag = 'darwin-x86_64' + elif sys.platform == 'win32': + host_tag = 'windows-x86_64' + else: + raise RuntimeError(f'Unsupported host platform: {sys.platform}') + + toolchain_bin = os.path.join(ndk_path, 'toolchains', 'llvm', 'prebuilt', host_tag, 'bin') + + # The NDK clang binary uses a slightly different prefix for armv7. + # Rust target: armv7-linux-androideabi + # NDK prefix: armv7a-linux-androideabi + clang_prefix = rust_target + if rust_target.startswith('armv7-'): + clang_prefix = 'armv7a-' + rust_target[len('armv7-'):] + + clang = os.path.join(toolchain_bin, f'{clang_prefix}{api_level}-clang') + ar = os.path.join(toolchain_bin, 'llvm-ar') + + # Cargo looks for CARGO_TARGET__LINKER where the triple is + # upper-cased with hyphens replaced by underscores. + triple_env = rust_target.upper().replace('-', '_') + env[f'CARGO_TARGET_{triple_env}_LINKER'] = clang + env['CC'] = clang + env['AR'] = ar + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/engine/src/flutter/shell/common/shorebird/hot_restart.cc b/engine/src/flutter/shell/common/shorebird/hot_restart.cc new file mode 100644 index 0000000000000..6ce18d072d078 --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/hot_restart.cc @@ -0,0 +1,224 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/common/shorebird/hot_restart.h" + +#include +#include + +#include "flutter/assets/asset_manager.h" +#include "flutter/fml/logging.h" +#include "flutter/runtime/dart_snapshot.h" +#include "flutter/runtime/dart_vm.h" +#include "flutter/runtime/isolate_configuration.h" +#include "flutter/shell/common/engine.h" +#include "flutter/shell/common/run_configuration.h" +#include "flutter/shell/common/shell.h" +#include "flutter/shell/common/shorebird/updater.h" +#include "third_party/dart/runtime/include/dart_native_api.h" + +namespace flutter { +namespace shorebird { + +namespace { +#if SHOREBIRD_USE_INTERPRETER +constexpr bool kUseInterpreter = true; +#else +constexpr bool kUseInterpreter = false; +#endif +} // namespace + +std::vector LibraryPathsForNextBoot( + const std::string& next_boot_patch_path, + const std::vector& original_libapp_paths, + bool use_interpreter) { + if (next_boot_patch_path.empty()) { + return original_libapp_paths; + } + if (use_interpreter) { + // Mirrors the iOS branch of ConfigureShorebird: the patch is prepended + // so the base library remains visible for VM snapshot resolution. + std::vector paths = {next_boot_patch_path}; + paths.insert(paths.end(), original_libapp_paths.begin(), + original_libapp_paths.end()); + return paths; + } + // Mirrors the Android branch of ConfigureShorebird: the patch library + // provides all snapshot symbols. + return {next_boot_patch_path}; +} + +bool VMSnapshotsIdentical(const DartSnapshot& running_vm_snapshot, + const DartSnapshot& next_vm_snapshot) { + const uint8_t* running_data = running_vm_snapshot.GetDataMapping(); + const uint8_t* next_data = next_vm_snapshot.GetDataMapping(); + if (running_data == nullptr || next_data == nullptr) { + return false; + } + // Same mapping โ€” e.g. both resolved to the App.framework symbols already + // loaded in this process. + if (running_data != next_data) { + int64_t running_data_size = Dart_SnapshotDataSize(running_data); + if (running_data_size <= 0 || + running_data_size != Dart_SnapshotDataSize(next_data)) { + return false; + } + if (memcmp(running_data, next_data, + static_cast(running_data_size)) != 0) { + return false; + } + } + + const uint8_t* running_instrs = running_vm_snapshot.GetInstructionsMapping(); + const uint8_t* next_instrs = next_vm_snapshot.GetInstructionsMapping(); + if (running_instrs == next_instrs) { + return true; + } + if (running_instrs == nullptr || next_instrs == nullptr) { + return false; + } + int64_t running_instrs_size = Dart_SnapshotInstrSize(running_instrs); + if (running_instrs_size <= 0 || + running_instrs_size != Dart_SnapshotInstrSize(next_instrs)) { + return false; + } + return memcmp(running_instrs, next_instrs, + static_cast(running_instrs_size)) == 0; +} + +bool HotRestartEngine(Engine& engine, + const Settings& settings, + const DartSnapshot& running_vm_snapshot) { + auto& updater = Updater::Instance(); + + // Two attempts: first the requested next-boot patch; if relaunching it + // fails, ReportLaunchFailure rolls the updater back, and the second + // attempt boots whatever it fell back to (the last good patch or the base + // release) so the app recovers. + for (int attempt = 0; attempt < 2; ++attempt) { + updater.ValidateNextBootPatch(); + const std::string patch_path = updater.NextBootPatchPath(); + FML_LOG(INFO) << "Shorebird hot restart: relaunching from " + << (patch_path.empty() ? std::string("the base release") + : patch_path); + + Settings restart_settings = settings; + restart_settings.application_library_paths = LibraryPathsForNextBoot( + patch_path, updater.GetAppConfig().original_libapp_paths, + kUseInterpreter); + + // The relaunched isolate keeps running against the VM snapshot this + // process booted with, so the next boot's VM snapshot must be + // identical. It always is for correctly built patches (same + // Flutter/Dart version as the release); this guards against mismatched + // artifacts. Refusing here reports nothing to the updater: the patch + // stays installed and boots normally (with its own VM snapshot) on the + // next app launch. + auto next_vm_snapshot = + DartSnapshot::VMSnapshotFromSettings(restart_settings); + if (!next_vm_snapshot || + !VMSnapshotsIdentical(running_vm_snapshot, *next_vm_snapshot)) { + FML_LOG(ERROR) + << "Shorebird hot restart: the next boot's VM snapshot does not " + "match the running VM snapshot; the patch will instead take " + "effect on the next app launch."; + return false; + } + + // Begin a fresh launch cycle: ReportLaunchStart is re-armed and fires + // during isolate snapshot resolution below (in ResolveIsolateData), + // exactly as it does on a cold boot. If the process dies mid-restart, + // the next cold boot's crash recovery rolls the patch back. + Updater::BeginNewLaunchCycle(); + auto isolate_snapshot = + DartSnapshot::IsolateSnapshotFromSettings(restart_settings); + if (!isolate_snapshot) { + FML_LOG(ERROR) + << "Shorebird hot restart: failed to resolve the isolate snapshot."; + updater.ReportLaunchFailure(); + continue; + } + + auto isolate_configuration = + IsolateConfiguration::InferFromSettings(restart_settings); + RunConfiguration configuration(std::move(isolate_configuration)); + configuration.SetEntrypointAndLibrary(engine.GetLastEntrypoint(), + engine.GetLastEntrypointLibrary()); + configuration.SetEntrypointArgs(engine.GetLastEntrypointArgs()); + configuration.SetEngineId(engine.GetLastEngineId()); + + // Carry over the existing asset resolvers (APK / bundle assets don't + // change across a hot restart) โ€” the same carry-over the debug hot + // restart does in Shell::OnServiceProtocolRunInView. + auto old_asset_manager = engine.GetAssetManager(); + if (old_asset_manager != nullptr) { + for (auto& resolver : old_asset_manager->TakeResolvers()) { + if (resolver->IsValidAfterAssetManagerChange()) { + configuration.AddAssetResolver(std::move(resolver)); + } + } + } + + if (engine.RestartWithSnapshot(std::move(configuration), + std::move(isolate_snapshot))) { + updater.ReportLaunchSuccess(); + FML_LOG(INFO) << "Shorebird hot restart succeeded."; + return true; + } + + FML_LOG(ERROR) << "Shorebird hot restart: relaunch failed."; + updater.ReportLaunchFailure(); + } + + FML_LOG(ERROR) << "Shorebird hot restart could not relaunch the app; the " + "app may be unresponsive until relaunched by the user."; + return false; +} + +} // namespace shorebird + +// Shell's hot restart host registration. Defined here (not in shell.cc) to +// keep the Shorebird-specific logic out of upstream files; these are +// declared as Shell members in shell.h because they need the Shell's +// settings, VM, and engine weak pointer. + +void Shell::ShorebirdRegisterRestartHost() { + // Hot restart is production-only (debug/JIT builds have the development + // hot restart) and only supported for engines initialized through the + // mobile ConfigureShorebird path โ€” see Updater::SetHotRestartSupported. + if (!DartVM::IsRunningPrecompiledCode()) { + return; + } + if (!shorebird::Updater::HotRestartSupported()) { + return; + } + + shorebird::Updater::RegisterRestartHost( + reinterpret_cast(this), + [ui_task_runner = task_runners_.GetUITaskRunner(), + weak_engine = weak_engine_, settings = settings_, + vm_data = vm_->GetVMData()]() { + // Invoked on an arbitrary thread (whichever thread ran the Dart FFI + // call). Restart asynchronously on the UI thread โ€” the same thread + // the debug hot restart runs on โ€” and report the request as + // accepted. + ui_task_runner->PostTask([weak_engine, settings, vm_data]() { + if (!weak_engine) { + FML_LOG(WARNING) + << "Shorebird hot restart: engine was destroyed before the " + "restart could run."; + return; + } + shorebird::HotRestartEngine(*weak_engine.get(), settings, + vm_data->GetVMSnapshot()); + }); + return true; + }); +} + +void Shell::ShorebirdUnregisterRestartHost() { + shorebird::Updater::UnregisterRestartHost(reinterpret_cast(this)); +} + +} // namespace flutter diff --git a/engine/src/flutter/shell/common/shorebird/hot_restart.h b/engine/src/flutter/shell/common/shorebird/hot_restart.h new file mode 100644 index 0000000000000..193098aa467c3 --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/hot_restart.h @@ -0,0 +1,70 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_COMMON_SHOREBIRD_HOT_RESTART_H_ +#define FLUTTER_SHELL_COMMON_SHOREBIRD_HOT_RESTART_H_ + +#include +#include + +#include "flutter/common/settings.h" +#include "flutter/fml/memory/ref_ptr.h" + +namespace flutter { + +class Engine; +class DartSnapshot; + +namespace shorebird { + +/// Hot restart of production (AOT) apps. +/// +/// A hot restart applies a downloaded patch without killing the OS process: +/// the Shell tears down the running root isolate and relaunches it from the +/// current next-boot patch, mirroring what a full app relaunch would boot. +/// +/// Flow: package:shorebird_code_push calls shorebird_restart_app over FFI โ†’ +/// the Rust updater invokes the engine's registered handler +/// (Updater::RequestRestart) โ†’ the single registered host (the Shell) posts +/// HotRestartEngine to its UI task runner. Everything below runs on the UI +/// thread, like the debug-mode hot restart in +/// Shell::OnServiceProtocolRunInView. + +/// The Settings::application_library_paths list to resolve the next boot's +/// snapshots from, mirroring how ConfigureShorebird builds the list at cold +/// boot. `next_boot_patch_path` may be empty (no patch: boot the base +/// release via `original_libapp_paths`). When `use_interpreter` (iOS), the +/// patch is prepended so the base library remains visible for VM snapshot +/// resolution; otherwise (Android) the patch library alone provides all +/// symbols. +std::vector LibraryPathsForNextBoot( + const std::string& next_boot_patch_path, + const std::vector& original_libapp_paths, + bool use_interpreter); + +/// Whether `next_vm_snapshot` is identical to the VM snapshot the running +/// process booted with. A relaunched isolate keeps running against the +/// original VM snapshot, so the patch's isolate snapshot must have been +/// generated against an identical one. Identical Flutter/Dart versions +/// produce identical VM snapshots (Shorebird requires patches to be built +/// with the release's Flutter version); this guards against mismatched +/// artifacts reaching a device. Exposed for testing. +bool VMSnapshotsIdentical(const DartSnapshot& running_vm_snapshot, + const DartSnapshot& next_vm_snapshot); + +/// Tears down `engine`'s root isolate and relaunches it from the current +/// next-boot patch. Must run on the UI thread. `settings` is the Shell's +/// settings; `running_vm_snapshot` is the VM snapshot the process booted +/// with. On failure, reports launch failure to the updater (rolling the +/// patch back) and retries once with the rolled-back selection so the app +/// recovers onto a known-good state. Returns whether an isolate is running +/// when done. +bool HotRestartEngine(Engine& engine, + const Settings& settings, + const DartSnapshot& running_vm_snapshot); + +} // namespace shorebird +} // namespace flutter + +#endif // FLUTTER_SHELL_COMMON_SHOREBIRD_HOT_RESTART_H_ diff --git a/engine/src/flutter/shell/common/shorebird/hot_restart_unittests.cc b/engine/src/flutter/shell/common/shorebird/hot_restart_unittests.cc new file mode 100644 index 0000000000000..50cd0aa354b44 --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/hot_restart_unittests.cc @@ -0,0 +1,80 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/common/shorebird/hot_restart.h" + +#include "flutter/fml/mapping.h" +#include "flutter/runtime/dart_snapshot.h" +#include "gtest/gtest.h" + +namespace flutter { +namespace shorebird { +namespace testing { + +// The library-path lists must mirror how ConfigureShorebird builds them at +// cold boot, so a hot restart resolves the same snapshots a full app +// relaunch would. + +TEST(HotRestart, LibraryPathsWithoutPatchAreTheOriginalPaths) { + const std::vector originals = {"/data/app/lib/libapp.so"}; + EXPECT_EQ(LibraryPathsForNextBoot("", originals, /*use_interpreter=*/false), + originals); + EXPECT_EQ(LibraryPathsForNextBoot("", originals, /*use_interpreter=*/true), + originals); +} + +TEST(HotRestart, LibraryPathsWithPatchReplaceOriginals) { + // Mirrors the Android branch of ConfigureShorebird: the patch library + // provides all snapshot symbols. + const std::vector originals = {"/data/app/lib/libapp.so"}; + const std::vector expected = {"/data/patches/1/dlc.vmcode"}; + EXPECT_EQ(LibraryPathsForNextBoot("/data/patches/1/dlc.vmcode", originals, + /*use_interpreter=*/false), + expected); +} + +TEST(HotRestart, LibraryPathsWithPatchAndInterpreterPrependPatch) { + // Mirrors the iOS branch of ConfigureShorebird: the base library stays in + // the list so VM snapshot resolution can still find it. + const std::vector originals = {"/app/Frameworks/App"}; + const std::vector expected = {"/patches/1/dlc.vmcode", + "/app/Frameworks/App"}; + EXPECT_EQ(LibraryPathsForNextBoot("/patches/1/dlc.vmcode", originals, + /*use_interpreter=*/true), + expected); +} + +TEST(HotRestart, LibraryPathsWithoutPatchOrOriginalsAreEmpty) { + EXPECT_TRUE( + LibraryPathsForNextBoot("", {}, /*use_interpreter=*/false).empty()); +} + +// VMSnapshotsIdentical must accept the same underlying buffers (e.g. both +// snapshots resolved to the App.framework symbols already loaded in this +// process) without parsing them. The byte-comparison path requires real +// snapshot headers and is exercised by device tests. +TEST(HotRestart, VMSnapshotsIdenticalForSameBuffers) { + static const uint8_t data[16] = {0}; + static const uint8_t instructions[16] = {0}; + + auto make_snapshot = [] { + return DartSnapshot::IsolateSnapshotFromMappings( + std::make_shared(data, sizeof(data)), + std::make_shared(instructions, + sizeof(instructions))); + }; + + auto running = make_snapshot(); + auto next = make_snapshot(); + ASSERT_TRUE(running); + ASSERT_TRUE(next); + + // Distinct DartSnapshot objects over the same buffers are identical. + EXPECT_TRUE(VMSnapshotsIdentical(*running, *next)); + EXPECT_TRUE(VMSnapshotsIdentical(*running, *running)); +} + +} // namespace testing +} // namespace shorebird +} // namespace flutter diff --git a/engine/src/flutter/shell/common/shorebird/list_rust_files.py b/engine/src/flutter/shell/common/shorebird/list_rust_files.py new file mode 100644 index 0000000000000..f478d7bd8e176 --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/list_rust_files.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# Copyright 2024 The Shorebird Authors. All rights reserved. +# Use of this source code is governed by a MIT-style license that can be +# found in the LICENSE file. + +"""List Rust source files for GN input tracking. + +Walks a directory tree and prints all .rs files as absolute paths. Used by +GN's exec_script to generate an input list for the Rust updater build action. + +Usage: + python3 list_rust_files.py +""" + +import os +import sys + + +def main(): + directory = os.path.abspath(sys.argv[1]) + for root, _, files in os.walk(directory): + for filename in sorted(files): + if filename.endswith('.rs'): + print(os.path.join(root, filename).replace(os.sep, '/')) + + +if __name__ == '__main__': + main() diff --git a/engine/src/flutter/shell/common/shorebird/patch_cache_unittests.cc b/engine/src/flutter/shell/common/shorebird/patch_cache_unittests.cc new file mode 100644 index 0000000000000..51fc20f9ca562 --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/patch_cache_unittests.cc @@ -0,0 +1,70 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/runtime/shorebird/patch_cache.h" + +#include +#include + +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { + +TEST(PatchCache, InstanceReturnsSameInstance) { + PatchCache& instance1 = PatchCache::Instance(); + PatchCache& instance2 = PatchCache::Instance(); + EXPECT_EQ(&instance1, &instance2); +} + +TEST(TryLoadFromPatch, ReturnsNullptrForEmptyPaths) { + std::vector empty_paths; + auto result = TryLoadFromPatch(empty_paths, "kDartIsolateSnapshotData"); + EXPECT_EQ(result, nullptr); +} + +TEST(TryLoadFromPatch, ReturnsNullptrForNonVmcodePath) { + std::vector paths = {"/path/to/some/file.so"}; + auto result = TryLoadFromPatch(paths, "kDartIsolateSnapshotData"); + EXPECT_EQ(result, nullptr); +} + +TEST(TryLoadFromPatch, ReturnsNullptrForVmSymbol) { + // Even with a .vmcode path, VM symbols should return nullptr + // (we can't actually load the file, but we can verify the symbol check) + std::vector paths = {"/path/to/patch.vmcode"}; + + // VM data symbol should return nullptr (patches don't contain VM snapshots) + auto result_vm_data = TryLoadFromPatch(paths, "kDartVmSnapshotData"); + EXPECT_EQ(result_vm_data, nullptr); + + // VM instructions symbol should return nullptr + auto result_vm_instrs = + TryLoadFromPatch(paths, "kDartVmSnapshotInstructions"); + EXPECT_EQ(result_vm_instrs, nullptr); +} + +TEST(TryLoadFromPatch, ReturnsNullptrForUnknownSymbol) { + std::vector paths = {"/path/to/patch.vmcode"}; + auto result = TryLoadFromPatch(paths, "kSomeUnknownSymbol"); + EXPECT_EQ(result, nullptr); +} + +TEST(TryLoadFromPatch, ChecksOnlyFirstPath) { + // Only the first path should be checked for .vmcode extension + std::vector paths = {"/path/to/regular.so", + "/path/to/patch.vmcode"}; + auto result = TryLoadFromPatch(paths, "kDartIsolateSnapshotData"); + // Should return nullptr because first path is not .vmcode + EXPECT_EQ(result, nullptr); +} + +TEST(PatchCache, GetOrLoadReturnsNullptrForNonexistentFile) { + auto result = + PatchCache::Instance().GetOrLoad("/nonexistent/path/to/file.vmcode"); + EXPECT_EQ(result, nullptr); +} + +} // namespace testing +} // namespace flutter diff --git a/engine/src/flutter/shell/common/shorebird/shorebird.cc b/engine/src/flutter/shell/common/shorebird/shorebird.cc new file mode 100644 index 0000000000000..d3a67a3aae960 --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/shorebird.cc @@ -0,0 +1,338 @@ + +#include "flutter/shell/common/shorebird/shorebird.h" + +#include +#include +#include +#include +#include + +#include "flutter/fml/command_line.h" +#include "flutter/fml/file.h" +#include "flutter/fml/macros.h" +#include "flutter/fml/mapping.h" +#include "flutter/fml/message_loop.h" +#include "flutter/fml/native_library.h" +#include "flutter/fml/paths.h" +#include "flutter/lib/ui/plugins/callback_cache.h" +#include "flutter/runtime/dart_snapshot.h" +#include "flutter/runtime/dart_vm.h" +#include "flutter/shell/common/shell.h" +#include "flutter/shell/common/shorebird/snapshots_data_handle.h" +#include "flutter/shell/common/shorebird/updater.h" +#include "flutter/shell/common/switches.h" +#include "fml/logging.h" +#include "shell/platform/embedder/embedder.h" +#include "third_party/dart/runtime/include/dart_native_api.h" +#include "third_party/dart/runtime/include/dart_tools_api.h" + +// Namespaced to avoid Google style warnings. +namespace flutter { + +// Old Android versions (e.g. the v16 ndk Flutter uses) don't always include a +// getauxval symbol, but the Rust ring crate assumes it exists: +// https://github.com/briansmith/ring/blob/fa25bf3a7403c9fe6458cb87bd8427be41225ca2/src/cpu/arm.rs#L22 +// It uses it to determine if the CPU supports AES instructions. +// Making this a weak symbol allows the linker to use a real version instead +// if it can find one. +// BoringSSL just reads from procfs instead, which is what we would do if +// we needed to implement this ourselves. Implementation looks straightforward: +// https://lwn.net/Articles/519085/ +// https://github.com/google/boringssl/blob/6ab4f0ae7f2db96d240eb61a5a8b4724e5a09b2f/crypto/cpu_arm_linux.c +#if defined(__ANDROID__) && defined(__arm__) +extern "C" __attribute__((weak)) unsigned long getauxval(unsigned long type) { + return 0; +} +#endif + +#if SHOREBIRD_USE_INTERPRETER +// Global references to the base (unpatched) snapshots from the App.framework. +// These are process-global because: +// 1. The Shorebird updater library is a process-global singleton with its own +// internal state. FileCallbacksImpl provides it access to the base snapshot +// data for patch generation/validation. +// 2. The base snapshots are immutable (baked into the IPA) so sharing them +// across isolate groups is safe. +// +// Note: This design doesn't support multiple engines with different base +// snapshots, but I'm not aware of any use cases for that on iOS. +static fml::RefPtr vm_snapshot; +static fml::RefPtr isolate_snapshot; + +void SetBaseSnapshot(Settings& settings) { + // These mappings happen to be to static data in the App.framework, but + // we still need to seem to hold onto the DartSnapshot objects to keep + // the mappings alive. + vm_snapshot = DartSnapshot::VMSnapshotFromSettings(settings); + isolate_snapshot = DartSnapshot::IsolateSnapshotFromSettings(settings); + + // Per-mapping observability for the base snapshot. Logged at every app boot + // so customer syslogs include the exact sizes the on-device base reader + // exposes โ€” directly comparable to the host's `aot_tools dump_blobs` + // extraction the patch was generated against. + const uint8_t* vm_data_ptr = vm_snapshot->GetDataMapping(); + const uint8_t* iso_data_ptr = isolate_snapshot->GetDataMapping(); + const uint8_t* vm_insns_ptr = vm_snapshot->GetInstructionsMapping(); + const uint8_t* iso_insns_ptr = isolate_snapshot->GetInstructionsMapping(); + intptr_t vm_data_size = vm_data_ptr ? Dart_SnapshotDataSize(vm_data_ptr) : -1; + intptr_t iso_data_size = + iso_data_ptr ? Dart_SnapshotDataSize(iso_data_ptr) : -1; + intptr_t vm_insns_size = + vm_insns_ptr ? Dart_SnapshotInstrSize(vm_insns_ptr) : -1; + intptr_t iso_insns_size = + iso_insns_ptr ? Dart_SnapshotInstrSize(iso_insns_ptr) : -1; + FML_LOG(INFO) << "[shorebird] SetBaseSnapshot mappings: " + << "vm_data_size=" << vm_data_size + << " iso_data_size=" << iso_data_size + << " vm_insns_size=" << vm_insns_size + << " iso_insns_size=" << iso_insns_size << " total=" + << (vm_data_size + iso_data_size + vm_insns_size + + iso_insns_size); + + Shorebird_SetBaseSnapshots(isolate_snapshot->GetDataMapping(), + isolate_snapshot->GetInstructionsMapping(), + vm_snapshot->GetDataMapping(), + vm_snapshot->GetInstructionsMapping()); +} +#endif // SHOREBIRD_USE_INTERPRETER + +class FileCallbacksImpl { + public: + static void* Open(); + static uintptr_t Read(void* file, uint8_t* buffer, uintptr_t length); + static int64_t Seek(void* file, int64_t offset, int32_t whence); + static void Close(void* file); +}; + +shorebird::FileCallbacks ShorebirdFileCallbacks() { + return { + .open = FileCallbacksImpl::Open, + .read = FileCallbacksImpl::Read, + .seek = FileCallbacksImpl::Seek, + .close = FileCallbacksImpl::Close, + }; +} + +// Given the contents of a yaml file, return the given value if it exists, +// otherwise return an empty string. +// Does not support nested keys. +std::string GetValueFromYaml(const std::string& yaml, const std::string& key) { + std::stringstream ss(yaml); + std::string line; + std::string prefix = key + ":"; + while (std::getline(ss, line, '\n')) { + if (line.find(prefix) != std::string::npos) { + auto ret = line.substr(line.find(prefix) + prefix.size()); + + // Remove leading and trailing spaces + while (!ret.empty() && std::isspace(ret.front())) { + ret.erase(0, 1); + } + while (!ret.empty() && std::isspace(ret.back())) { + ret.pop_back(); + } + return ret; + } + } + return ""; +} + +/// Newer api, used by Desktop implementations. +/// Does not directly manipulate Settings. +// TODO(eseidel): Consolidate this with the other ConfigureShorebird() API. +bool ConfigureShorebird(const ShorebirdConfigArgs& args, + std::string& patch_path) { + patch_path = args.release_app_library_path; + auto shorebird_updater_dir_name = "shorebird_updater"; + + // Parse app id from shorebird.yaml + std::string app_id = GetValueFromYaml(args.shorebird_yaml, "app_id"); + if (app_id.empty()) { + FML_LOG(ERROR) << "Shorebird updater: appid not found in shorebird.yaml"; + return false; + } + + auto code_cache_dir = fml::paths::JoinPaths( + {std::move(args.code_cache_path), shorebird_updater_dir_name, app_id}); + auto app_storage_dir = fml::paths::JoinPaths( + {std::move(args.app_storage_path), shorebird_updater_dir_name, app_id}); + + fml::CreateDirectory(fml::paths::GetCachesDirectory(), + {shorebird_updater_dir_name}, + fml::FilePermission::kReadWrite); + + // Combine version and version_code into a single string. + // We could also pass these separately through to the updater if needed. + auto release_version = args.release_version.version; + if (!args.release_version.build_number.empty()) { + release_version += "+" + args.release_version.build_number; + } + + shorebird::AppConfig config; + config.release_version = release_version; + config.original_libapp_paths = {args.release_app_library_path}; + config.app_storage_dir = app_storage_dir; + config.code_cache_dir = code_cache_dir; + config.file_callbacks = ShorebirdFileCallbacks(); + config.yaml_config = args.shorebird_yaml; + + bool init_result = shorebird::Updater::Instance().Init(config); + + // We do not support synchronous updates on launch, it's a terrible UX. + // Users can implement custom check-for-updates using + // package:shorebird_code_push. + // https://github.com/shorebirdtech/shorebird/issues/950 + + FML_LOG(INFO) << "Checking for active patch"; + shorebird::Updater::Instance().ValidateNextBootPatch(); + std::string active_path = shorebird::Updater::Instance().NextBootPatchPath(); + if (!active_path.empty()) { + patch_path = active_path; + FML_LOG(INFO) << "Shorebird updater: patch path: " << patch_path; + } else { + FML_LOG(INFO) << "Shorebird updater: no active patch."; + } + + // Note: shorebird_report_launch_start() is now called from TryLoadFromPatch() + // in runtime/shorebird/patch_cache.cc, right before the patched snapshot is + // actually loaded. This fixes issues with FlutterEngineGroup and other cases + // where ConfigureShorebird() is called but no Shell is created. + if (!init_result) { + return false; + } + + if (shorebird::Updater::Instance().ShouldAutoUpdate()) { + FML_LOG(INFO) << "Starting Shorebird update"; + shorebird::Updater::Instance().StartUpdateThread(); + } else { + FML_LOG(INFO) + << "Shorebird auto_update disabled, not checking for updates."; + } + + return true; +} + +/// Older api used by iOS and Android, directly manipulates Settings. +// TODO(eseidel): Consolidate this with the other ConfigureShorebird() API. +void ConfigureShorebird(std::string code_cache_path, + std::string app_storage_path, + Settings& settings, + const std::string& shorebird_yaml, + const std::string& version, + const std::string& version_code) { + // If you are crashing here, you probably are running Shorebird in a Debug + // config, where the AOT snapshot won't be linked into the process, and thus + // lookups will fail. Change your Scheme to Release to fix: + // https://github.com/flutter/flutter/wiki/Debugging-the-engine#debugging-ios-builds-with-xcode + FML_CHECK(DartSnapshot::VMSnapshotFromSettings(settings)) + << "XCode Scheme must be set to Release to use Shorebird"; + + auto shorebird_updater_dir_name = "shorebird_updater"; + + auto code_cache_dir = fml::paths::JoinPaths( + {std::move(code_cache_path), shorebird_updater_dir_name}); + auto app_storage_dir = fml::paths::JoinPaths( + {std::move(app_storage_path), shorebird_updater_dir_name}); + + fml::CreateDirectory(fml::paths::GetCachesDirectory(), + {shorebird_updater_dir_name}, + fml::FilePermission::kReadWrite); + + // Combine version and version_code into a single string. + // We could also pass these separately through to the updater if needed. + shorebird::AppConfig config; + config.release_version = version + "+" + version_code; + config.original_libapp_paths = settings.application_library_paths; + config.app_storage_dir = app_storage_dir; + config.code_cache_dir = code_cache_dir; + config.file_callbacks = ShorebirdFileCallbacks(); + config.yaml_config = shorebird_yaml; + + bool init_result = shorebird::Updater::Instance().Init(config); + + // On the mobile path, snapshots resolve through + // Settings::application_library_paths, which hot restart can re-resolve to + // pick up a newly installed patch. Desktop embedders (the other + // ConfigureShorebird overload) wire AOT data through the embedder API, + // which a restart would not refresh, so they don't enable this. + if (init_result) { + shorebird::Updater::SetHotRestartSupported(true); + } + + // We do not support synchronous updates on launch, it's a terrible UX. + // Users can implement custom check-for-updates using + // package:shorebird_code_push. + // https://github.com/shorebirdtech/shorebird/issues/950 + + // We only set the base snapshot on iOS for now. +#if SHOREBIRD_USE_INTERPRETER + SetBaseSnapshot(settings); +#endif + + shorebird::Updater::Instance().ValidateNextBootPatch(); + std::string active_path = shorebird::Updater::Instance().NextBootPatchPath(); + if (!active_path.empty()) { + FML_LOG(INFO) << "Shorebird updater: active path: " << active_path; + +#if SHOREBIRD_USE_INTERPRETER + // On iOS we add the patch to the front of the list instead of clearing + // the list, to allow dart_snapshot.cc to still find the base snapshot + // for the vm isolate. + settings.application_library_paths.insert( + settings.application_library_paths.begin(), active_path); +#else + settings.application_library_paths.clear(); + settings.application_library_paths.emplace_back(active_path); +#endif + } else { + FML_LOG(INFO) << "Shorebird updater: no active patch."; + } + + // Note: shorebird_report_launch_start() is now called from TryLoadFromPatch() + // in runtime/shorebird/patch_cache.cc, right before the patched snapshot is + // actually loaded. This fixes issues with FlutterEngineGroup and other cases + // where ConfigureShorebird() is called but no Shell is created. + + if (!init_result) { + return; + } + + if (shorebird::Updater::Instance().ShouldAutoUpdate()) { + FML_LOG(INFO) << "Starting Shorebird update"; + shorebird::Updater::Instance().StartUpdateThread(); + } else { + FML_LOG(INFO) + << "Shorebird auto_update disabled, not checking for updates."; + } +} + +void* FileCallbacksImpl::Open() { +#if SHOREBIRD_USE_INTERPRETER + return SnapshotsDataHandle::createForSnapshots(*vm_snapshot, + *isolate_snapshot) + .release(); +#else + // SnapshotsDataHandle exists on all platforms (for testing) but is only used + // on iOS. iOS patches are generated from just the Dart parts of the snapshot, + // excluding the Mach-O specific headers which contain dates and paths that + // make them change on every build. + return nullptr; +#endif // SHOREBIRD_USE_INTERPRETER +} + +uintptr_t FileCallbacksImpl::Read(void* file, + uint8_t* buffer, + uintptr_t length) { + return reinterpret_cast(file)->Read(buffer, length); +} + +int64_t FileCallbacksImpl::Seek(void* file, int64_t offset, int32_t whence) { + // Currently we only support blob handles. + return reinterpret_cast(file)->Seek(offset, whence); +} + +void FileCallbacksImpl::Close(void* file) { + delete reinterpret_cast(file); +} + +} // namespace flutter \ No newline at end of file diff --git a/engine/src/flutter/shell/common/shorebird/shorebird.h b/engine/src/flutter/shell/common/shorebird/shorebird.h new file mode 100644 index 0000000000000..ab5c9162ea0f2 --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/shorebird.h @@ -0,0 +1,59 @@ +#ifndef FLUTTER_SHELL_COMMON_SHOREBIRD_SHOREBIRD_H_ +#define FLUTTER_SHELL_COMMON_SHOREBIRD_SHOREBIRD_H_ + +#include "flutter/common/settings.h" +#include "flutter/fml/memory/ref_ptr.h" +#include "shell/platform/embedder/embedder.h" + +namespace flutter { + +class DartSnapshot; + +/// Version and build number of the release. +/// Used by ShorebirdConfigArgs. +struct ReleaseVersion { + std::string version; + std::string build_number; +}; + +/// Arguments for ConfigureShorebird. +/// Used by Desktop implementations. +struct ShorebirdConfigArgs { + std::string code_cache_path; + std::string app_storage_path; + std::string release_app_library_path; + std::string shorebird_yaml; + ReleaseVersion release_version; + + ShorebirdConfigArgs(std::string code_cache_path, + std::string app_storage_path, + std::string release_app_library_path, + std::string shorebird_yaml, + ReleaseVersion release_version) + : code_cache_path(code_cache_path), + app_storage_path(app_storage_path), + release_app_library_path(release_app_library_path), + shorebird_yaml(shorebird_yaml), + release_version(release_version) {} +}; + +/// Newer api, used by Desktop implementations. +/// Does not directly manipulate Settings. +bool ConfigureShorebird(const ShorebirdConfigArgs& args, + std::string& patch_path); + +/// Older api used by iOS and Android, directly manipulates Settings. +void ConfigureShorebird(std::string code_cache_path, + std::string app_storage_path, + Settings& settings, + const std::string& shorebird_yaml, + const std::string& version, + const std::string& version_code); + +/// Used for reading app_id from shorebird.yaml. +/// Exposed for testing. +std::string GetValueFromYaml(const std::string& yaml, const std::string& key); + +} // namespace flutter + +#endif // FLUTTER_SHELL_COMMON_SHOREBIRD_SHOREBIRD_H_ diff --git a/engine/src/flutter/shell/common/shorebird/shorebird_unittests.cc b/engine/src/flutter/shell/common/shorebird/shorebird_unittests.cc new file mode 100644 index 0000000000000..7c108ac1e6231 --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/shorebird_unittests.cc @@ -0,0 +1,21 @@ +#include "flutter/shell/common/shorebird/shorebird.h" + +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { +TEST(Shorebird, GetValueFromYamlValueExists) { + std::string yaml = "appid: com.example.app\nversion: 1.0.0\n"; + std::string key = "appid"; + std::string value = GetValueFromYaml(yaml, key); + EXPECT_EQ(value, "com.example.app"); +} + +TEST(Shorebird, GetValueFromYamlValueDoesNotExist) { + std::string yaml = "appid: com.example.app\nversion: 1.0.0\n"; + std::string key = "appid2"; + std::string value = GetValueFromYaml(yaml, key); + EXPECT_EQ(value, ""); +} +} // namespace testing +} // namespace flutter \ No newline at end of file diff --git a/engine/src/flutter/shell/common/shorebird/snapshots_data_handle.cc b/engine/src/flutter/shell/common/shorebird/snapshots_data_handle.cc new file mode 100644 index 0000000000000..3f8c7f6763794 --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/snapshots_data_handle.cc @@ -0,0 +1,163 @@ +#include "flutter/shell/common/shorebird/snapshots_data_handle.h" + +#include "third_party/dart/runtime/include/dart_native_api.h" + +namespace flutter { + +static std::unique_ptr DataMapping(const DartSnapshot& snapshot) { + auto ptr = snapshot.GetDataMapping(); + return std::make_unique(ptr, + Dart_SnapshotDataSize(ptr)); +} + +static std::unique_ptr InstructionsMapping( + const DartSnapshot& snapshot) { + auto ptr = snapshot.GetInstructionsMapping(); + return std::make_unique(ptr, + Dart_SnapshotInstrSize(ptr)); +} + +// The size of the snapshot data is the sum of the sizes of the blobs. +size_t SnapshotsDataHandle::FullSize() const { + size_t size = 0; + for (const auto& blob : blobs_) { + size += blob->GetSize(); + } + return size; +} + +// The offset into the snapshots data blobs as though they were a single +// contiguous buffer. +size_t SnapshotsDataHandle::AbsoluteOffsetForIndex(BlobsIndex index) { + if (index.blob >= blobs_.size()) { + FML_LOG(WARNING) << "Blob index " << index.blob + << " is larger than the number of blobs (" << blobs_.size() + << "). Returning full size (" << FullSize() << ")"; + return FullSize(); + } + if (index.offset > blobs_[index.blob]->GetSize()) { + FML_LOG(WARNING) << "Offset for blob " << index.blob << " (" << index.offset + << ") is larger than the blob size (" + << blobs_[index.blob]->GetSize() + << "). Returning index start of next blob"; + return AbsoluteOffsetForIndex({index.blob + 1, 0}); + } + size_t offset = 0; + for (size_t i = 0; i < index.blob; i++) { + offset += blobs_[i]->GetSize(); + } + offset += index.offset; + return offset; +} + +BlobsIndex SnapshotsDataHandle::IndexForAbsoluteOffset(int64_t offset, + BlobsIndex start_index) { + size_t start_offset = AbsoluteOffsetForIndex(start_index); + if (offset < 0) { + if ((size_t)abs(offset) > start_offset) { + FML_LOG(WARNING) + << "Offset is before the beginning of SnapshotsData. Returning 0, 0"; + return {0, 0}; + } + } else if (offset + start_offset >= FullSize()) { + FML_LOG(WARNING) << "Target offset is past the end of SnapshotsData (" + << offset + start_offset << ", blobs size:" << FullSize() + << "). Returning last blob index and offset"; + return {blobs_.size(), blobs_.back()->GetSize()}; + } + + size_t dest_offset = start_offset + offset; + BlobsIndex index = {0, 0}; + for (const auto& blob : blobs_) { + if (dest_offset < blob->GetSize()) { + // The remaining offset is within this blob. + index.offset = dest_offset; + break; + } + + index.blob++; + dest_offset -= blob->GetSize(); + } + return index; +} + +std::unique_ptr SnapshotsDataHandle::createForSnapshots( + const DartSnapshot& vm_snapshot, + const DartSnapshot& isolate_snapshot) { + // This needs to match the order in which the blobs are written out in + // analyze_snapshot --dump_blobs + auto vm_data = DataMapping(vm_snapshot); + auto iso_data = DataMapping(isolate_snapshot); + auto vm_insns = InstructionsMapping(vm_snapshot); + auto iso_insns = InstructionsMapping(isolate_snapshot); + + // Per-blob observability for the base byte stream the updater is about to + // patch against. Logged at every patch-apply attempt so customer syslogs + // include the exact sizes the bipatch state machine sees โ€” directly + // comparable to the host's `aot_tools dump_blobs` extraction. + FML_LOG(INFO) << "[shorebird] SnapshotsDataHandle blob sizes: vm_data=" + << vm_data->GetSize() << "b iso_data=" << iso_data->GetSize() + << "b vm_instructions=" << vm_insns->GetSize() + << "b iso_instructions=" << iso_insns->GetSize() << "b total=" + << (vm_data->GetSize() + iso_data->GetSize() + + vm_insns->GetSize() + iso_insns->GetSize()) + << "b"; + + std::vector> blobs; + blobs.push_back(std::move(vm_data)); + blobs.push_back(std::move(iso_data)); + blobs.push_back(std::move(vm_insns)); + blobs.push_back(std::move(iso_insns)); + return std::make_unique(std::move(blobs)); +} + +uintptr_t SnapshotsDataHandle::Read(uint8_t* buffer, uintptr_t length) { + uintptr_t bytes_read = 0; + // Copy current blob from current offset and possibly into the next blob + // until we have read length bytes. + while (bytes_read < length) { + if (current_index_.blob >= blobs_.size()) { + // We have read all blobs. + break; + } + intptr_t remaining_blob_length = + blobs_[current_index_.blob]->GetSize() - current_index_.offset; + if (remaining_blob_length <= 0) { + // We have read all bytes in this blob. + current_index_.blob++; + current_index_.offset = 0; + continue; + } + intptr_t bytes_to_read = fmin(length - bytes_read, remaining_blob_length); + memcpy(buffer + bytes_read, + blobs_[current_index_.blob]->GetMapping() + current_index_.offset, + bytes_to_read); + bytes_read += bytes_to_read; + current_index_.offset += bytes_to_read; + } + + return bytes_read; +} + +int64_t SnapshotsDataHandle::Seek(int64_t offset, int32_t whence) { + BlobsIndex start_index; + switch (whence) { + case SEEK_CUR: + start_index = current_index_; + break; + case SEEK_SET: + start_index = {0, 0}; + break; + case SEEK_END: + start_index = {blobs_.size(), blobs_.back()->GetSize()}; + break; + default: + FML_CHECK(false) << "Unrecognized whence value in Seek: " << whence; + } + current_index_ = IndexForAbsoluteOffset(offset, start_index); + // Per POSIX/Rust io::Seek contract, return the new absolute position from + // the start of the stream, not the offset within the current blob. + return static_cast(AbsoluteOffsetForIndex(current_index_)); +} + +} // namespace flutter \ No newline at end of file diff --git a/engine/src/flutter/shell/common/shorebird/snapshots_data_handle.h b/engine/src/flutter/shell/common/shorebird/snapshots_data_handle.h new file mode 100644 index 0000000000000..50c4b8179b412 --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/snapshots_data_handle.h @@ -0,0 +1,48 @@ +#ifndef FLUTTER_SHELL_COMMON_SHOREBIRD_SNAPSHOTS_DATA_HANDLE_H_ +#define FLUTTER_SHELL_COMMON_SHOREBIRD_SNAPSHOTS_DATA_HANDLE_H_ + +#include +#include "flutter/fml/file.h" +#include "flutter/runtime/dart_snapshot.h" +#include "third_party/dart/runtime/include/dart_tools_api.h" + +namespace flutter { + +// An offset into an indexed collection of buffers. blob is the index of the +// buffer, and offset is the offset into that buffer. +struct BlobsIndex { + size_t blob; + size_t offset; +}; + +// Implements a POSIX file I/O interface which allows us to provide the four +// data blobs of a Dart snapshot (vm_data, vm_instructions, isolate_data, +// isolate_instructions) to Rust as though it were a single piece of memory. +class SnapshotsDataHandle { + public: + // This would ideally be private, but we need to be able to call it from the + // static createForSnapshots method. + explicit SnapshotsDataHandle(std::vector> blobs) + : blobs_(std::move(blobs)) {} + + static std::unique_ptr createForSnapshots( + const DartSnapshot& vm_snapshot, + const DartSnapshot& isolate_snapshot); + + uintptr_t Read(uint8_t* buffer, uintptr_t length); + int64_t Seek(int64_t offset, int32_t whence); + + // The sum of all the blobs' sizes. + size_t FullSize() const; + + private: + size_t AbsoluteOffsetForIndex(BlobsIndex index); + BlobsIndex IndexForAbsoluteOffset(int64_t offset, BlobsIndex startIndex); + + BlobsIndex current_index_ = {0, 0}; + std::vector> blobs_; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_COMMON_SHOREBIRD_SNAPSHOTS_DATA_HANDLE_H_ diff --git a/engine/src/flutter/shell/common/shorebird/snapshots_data_handle_unittests.cc b/engine/src/flutter/shell/common/shorebird/snapshots_data_handle_unittests.cc new file mode 100644 index 0000000000000..2acf44a7b8973 --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/snapshots_data_handle_unittests.cc @@ -0,0 +1,177 @@ +#include +#include +#include + +#include "flutter/shell/common/shorebird/snapshots_data_handle.h" + +#include "flutter/fml/mapping.h" +#include "flutter/runtime/dart_snapshot.h" +#include "flutter/testing/testing.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "testing/fixture_test.h" + +namespace flutter { +namespace testing { + +std::unique_ptr MakeHandle( + std::vector& blobs) { + // Map the strings into non-owned mappings: + std::vector> mappings = {}; + for (auto& blob : blobs) { + std::unique_ptr mapping = + std::make_unique( + reinterpret_cast(blob.data()), blob.size()); + mappings.push_back(std::move(mapping)); + } + auto handle = + std::make_unique(std::move(mappings)); + return handle; +} + +TEST(SnapshotsDataHandle, Read) { + std::vector blobs = {"abc", "def", "ghi", "jkl"}; + std::unique_ptr blobs_handle = MakeHandle(blobs); + + const size_t buffer_size = 12; + uint8_t buffer[buffer_size]; + std::fill(buffer, buffer + buffer_size, 0); + blobs_handle->Read(buffer, 6); + + EXPECT_EQ(buffer[0], 'a'); + EXPECT_EQ(buffer[1], 'b'); + EXPECT_EQ(buffer[2], 'c'); + EXPECT_EQ(buffer[3], 'd'); + EXPECT_EQ(buffer[4], 'e'); + EXPECT_EQ(buffer[5], 'f'); + + // Only the first 6 bytes should have been read. + EXPECT_EQ(buffer[6], 0); +} + +TEST(SnapshotsDataHandle, ReadAfterSeekWithPositiveOffset) { + std::vector blobs = {"abc", "def", "ghi", "jkl"}; + std::unique_ptr blobs_handle = MakeHandle(blobs); + + const size_t buffer_size = 20; + uint8_t buffer[buffer_size]; + std::fill(buffer, buffer + buffer_size, 0); + + blobs_handle->Seek(4, SEEK_CUR); + blobs_handle->Read(buffer, 6); + + EXPECT_EQ(buffer[0], 'e'); + EXPECT_EQ(buffer[1], 'f'); + EXPECT_EQ(buffer[2], 'g'); + EXPECT_EQ(buffer[3], 'h'); + EXPECT_EQ(buffer[4], 'i'); + EXPECT_EQ(buffer[5], 'j'); + + // Only the first 6 bytes should have been read. + EXPECT_EQ(buffer[6], 0); +} + +TEST(SnapshotsDataHandle, ReadAfterSeekWithNegativeOffset) { + std::vector blobs = {"abc", "def", "ghi", "jkl"}; + std::unique_ptr blobs_handle = MakeHandle(blobs); + + const size_t buffer_size = 20; + uint8_t buffer[buffer_size]; + std::fill(buffer, buffer + buffer_size, 0); + + blobs_handle->Read(buffer, 5); + EXPECT_EQ(buffer[0], 'a'); + EXPECT_EQ(buffer[1], 'b'); + EXPECT_EQ(buffer[2], 'c'); + EXPECT_EQ(buffer[3], 'd'); + EXPECT_EQ(buffer[4], 'e'); + EXPECT_EQ(buffer[5], 0); + + // Reset buffer + std::fill(buffer, buffer + buffer_size, 0); + + // Read 5, seeked back 4, should start reading at offset 1 ('b') + blobs_handle->Seek(-4, SEEK_CUR); + blobs_handle->Read(buffer, 6); + + EXPECT_EQ(buffer[0], 'b'); + EXPECT_EQ(buffer[1], 'c'); + EXPECT_EQ(buffer[2], 'd'); + EXPECT_EQ(buffer[3], 'e'); + EXPECT_EQ(buffer[4], 'f'); + EXPECT_EQ(buffer[5], 'g'); + EXPECT_EQ(buffer[6], 0); +} + +TEST(SnapshotsDataHandle, SeekPastEnd) { + std::vector blobs = {"abc", "def", "ghi", "jkl"}; + std::unique_ptr blobs_handle = MakeHandle(blobs); + + const size_t buffer_size = 20; + uint8_t buffer[buffer_size]; + std::fill(buffer, buffer + buffer_size, 0); + + // Seek 1 past the end + blobs_handle->Seek(blobs_handle->FullSize() + 1, SEEK_CUR); + + // Seek back 2 bytes and read 2 bytes + blobs_handle->Seek(-2, SEEK_CUR); + blobs_handle->Read(buffer, 2); + + EXPECT_EQ(buffer[0], 'k'); + EXPECT_EQ(buffer[1], 'l'); +} + +TEST(SnapshotsDataHandle, SeekBeforeBeginning) { + std::vector blobs = {"abc", "def", "ghi", "jkl"}; + std::unique_ptr blobs_handle = MakeHandle(blobs); + + const size_t buffer_size = 20; + uint8_t buffer[buffer_size]; + std::fill(buffer, buffer + buffer_size, 0); + + // Seek before the start of the blobs and read the first 2 bytes. + blobs_handle->Seek(-2, SEEK_CUR); + blobs_handle->Read(buffer, 2); + + EXPECT_EQ(buffer[0], 'a'); + EXPECT_EQ(buffer[1], 'b'); +} + +TEST(SnapshotsDataHandle, SeekFromBeginning) { + std::vector blobs = {"abc", "def", "ghi", "jkl"}; + std::unique_ptr blobs_handle = MakeHandle(blobs); + + const size_t buffer_size = 20; + uint8_t buffer[buffer_size]; + std::fill(buffer, buffer + buffer_size, 0); + + // Seek 10 bytes from current (the beginning) + blobs_handle->Seek(10, SEEK_CUR); + + // Seek 2 bytes from the beginning and read 2 bytes + blobs_handle->Seek(2, SEEK_SET); + blobs_handle->Read(buffer, 2); + + EXPECT_EQ(buffer[0], 'c'); + EXPECT_EQ(buffer[1], 'd'); +} + +TEST(SnapshotsDataHandle, SeekFromEnd) { + std::vector blobs = {"abc", "def", "ghi", "jkl"}; + std::unique_ptr blobs_handle = MakeHandle(blobs); + + const size_t buffer_size = 20; + uint8_t buffer[buffer_size]; + std::fill(buffer, buffer + buffer_size, 0); + + // Seek 2 bytes from the end and read 2 bytes + blobs_handle->Seek(-2, SEEK_END); + blobs_handle->Read(buffer, 2); + + EXPECT_EQ(buffer[0], 'k'); + EXPECT_EQ(buffer[1], 'l'); +} + +} // namespace testing +} // namespace flutter diff --git a/engine/src/flutter/shell/common/shorebird/updater.cc b/engine/src/flutter/shell/common/shorebird/updater.cc new file mode 100644 index 0000000000000..a6a059c189b6e --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/updater.cc @@ -0,0 +1,270 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/common/shorebird/updater.h" + +#include "flutter/fml/logging.h" + +#if SHOREBIRD_PLATFORM_SUPPORTED +#include "third_party/updater/library/include/updater_engine.h" +#endif + +namespace flutter { +namespace shorebird { + +// Static member definitions +std::unique_ptr Updater::instance_; +std::mutex Updater::instance_mutex_; +std::atomic Updater::launch_started_{false}; +std::atomic Updater::launch_completed_{false}; +std::mutex Updater::restart_hosts_mutex_; +std::map> Updater::restart_hosts_; +std::atomic Updater::hot_restart_supported_{false}; + +Updater& Updater::Instance() { + std::lock_guard lock(instance_mutex_); + if (!instance_) { +#if SHOREBIRD_PLATFORM_SUPPORTED + instance_ = std::make_unique(); +#else + instance_ = std::make_unique(); +#endif + } + return *instance_; +} + +void Updater::SetInstanceForTesting(std::unique_ptr instance) { + std::lock_guard lock(instance_mutex_); + instance_ = std::move(instance); +} + +void Updater::ResetInstanceForTesting() { + std::lock_guard lock(instance_mutex_); + instance_.reset(); +} + +void Updater::ResetLaunchStateForTesting() { + BeginNewLaunchCycle(); +} + +bool Updater::Init(const AppConfig& config) { + app_config_ = config; + return DoInit(config); +} + +void Updater::BeginNewLaunchCycle() { + launch_started_.store(false); + launch_completed_.store(false); +} + +void Updater::RegisterRestartHost(uintptr_t host_id, + std::function request_restart) { + std::lock_guard lock(restart_hosts_mutex_); + restart_hosts_[host_id] = std::move(request_restart); +} + +void Updater::UnregisterRestartHost(uintptr_t host_id) { + // Blocks while RequestRestart is invoking this host's callback, so a + // callback never runs against a destroyed host. Host callbacks must only + // schedule work (never restart synchronously), both to keep this window + // short and because the request arrives on an arbitrary thread. + std::lock_guard lock(restart_hosts_mutex_); + restart_hosts_.erase(host_id); +} + +bool Updater::RequestRestart() { + std::lock_guard lock(restart_hosts_mutex_); + if (restart_hosts_.empty()) { + FML_LOG(WARNING) << "Shorebird restart requested, but no engine capable " + "of hot restart is running."; + return false; + } + if (restart_hosts_.size() > 1) { + FML_LOG(WARNING) + << "Shorebird restart requested, but multiple Flutter engines are " + "running in this process. Hot restart is not supported with " + "multiple engines; the patch will boot on the next app launch."; + return false; + } + return restart_hosts_.begin()->second(); +} + +void Updater::SetHotRestartSupported(bool supported) { + hot_restart_supported_.store(supported); +} + +bool Updater::HotRestartSupported() { + return hot_restart_supported_.load(); +} + +void Updater::ReportLaunchStart() { + // Guard: only the first engine in a process should promote next_boot โ†’ + // current_boot in the Rust updater. See class-level comment for rationale. + bool expected = false; + if (!launch_started_.compare_exchange_strong(expected, true)) { + return; + } + DoReportLaunchStart(); +} + +void Updater::ReportLaunchSuccess() { + // Guard: only report success once per process. Subsequent engines reuse + // the same patch and don't need to re-confirm the boot. + bool expected = false; + if (!launch_completed_.compare_exchange_strong(expected, true)) { + return; + } + DoReportLaunchSuccess(); +} + +void Updater::ReportLaunchFailure() { + // Guard: only report failure once per process. + bool expected = false; + if (!launch_completed_.compare_exchange_strong(expected, true)) { + return; + } + DoReportLaunchFailure(); +} + +#if SHOREBIRD_PLATFORM_SUPPORTED +// RealUpdater implementation - wraps the Rust C API + +namespace { +// Registered with the Rust updater; invoked (on an arbitrary thread) when +// Dart calls shorebird_restart_app. +bool RestartHandlerTrampoline() { + return Updater::RequestRestart(); +} +} // namespace + +bool RealUpdater::DoInit(const AppConfig& config) { + // Convert paths to C strings + std::vector c_paths; + c_paths.reserve(config.original_libapp_paths.size()); + for (const auto& path : config.original_libapp_paths) { + c_paths.push_back(path.c_str()); + } + + AppParameters params; + params.release_version = config.release_version.c_str(); + params.original_libapp_paths = c_paths.data(); + params.original_libapp_paths_size = static_cast(c_paths.size()); + params.app_storage_dir = config.app_storage_dir.c_str(); + params.code_cache_dir = config.code_cache_dir.c_str(); + + // Convert our FileCallbacks to the Rust struct + ::FileCallbacks rust_callbacks; + rust_callbacks.open = config.file_callbacks.open; + rust_callbacks.read = config.file_callbacks.read; + rust_callbacks.seek = config.file_callbacks.seek; + rust_callbacks.close = config.file_callbacks.close; + + bool result = + shorebird_init(¶ms, rust_callbacks, config.yaml_config.c_str()); + if (result) { + // Let package:shorebird_code_push request hot restarts via + // shorebird_restart_app. Whether a restart can actually be serviced is + // decided per-request by RequestRestart (a single registered host). + shorebird_set_restart_handler(&RestartHandlerTrampoline); + } + return result; +} + +void RealUpdater::ValidateNextBootPatch() { + shorebird_validate_next_boot_patch(); +} + +std::string RealUpdater::NextBootPatchPath() { + char* c_path = shorebird_next_boot_patch_path(); + if (c_path == nullptr) { + return ""; + } + std::string path(c_path); + shorebird_free_string(c_path); + return path; +} + +void RealUpdater::DoReportLaunchStart() { + shorebird_report_launch_start(); +} + +void RealUpdater::DoReportLaunchSuccess() { + shorebird_report_launch_success(); +} + +void RealUpdater::DoReportLaunchFailure() { + shorebird_report_launch_failure(); +} + +bool RealUpdater::ShouldAutoUpdate() { + return shorebird_should_auto_update(); +} + +void RealUpdater::StartUpdateThread() { + shorebird_start_update_thread(); +} +#endif // SHOREBIRD_PLATFORM_SUPPORTED + +// MockUpdater implementation - for testing + +bool MockUpdater::DoInit(const AppConfig& config) { + init_count_++; + last_release_version_ = config.release_version; + last_yaml_config_ = config.yaml_config; + call_log_.push_back("Init"); + return init_result_; +} + +void MockUpdater::ValidateNextBootPatch() { + validate_count_++; + call_log_.push_back("ValidateNextBootPatch"); +} + +std::string MockUpdater::NextBootPatchPath() { + call_log_.push_back("NextBootPatchPath"); + return next_boot_patch_path_; +} + +void MockUpdater::DoReportLaunchStart() { + launch_start_count_++; + call_log_.push_back("ReportLaunchStart"); +} + +void MockUpdater::DoReportLaunchSuccess() { + launch_success_count_++; + call_log_.push_back("ReportLaunchSuccess"); +} + +void MockUpdater::DoReportLaunchFailure() { + launch_failure_count_++; + call_log_.push_back("ReportLaunchFailure"); +} + +bool MockUpdater::ShouldAutoUpdate() { + call_log_.push_back("ShouldAutoUpdate"); + return should_auto_update_; +} + +void MockUpdater::StartUpdateThread() { + start_update_thread_count_++; + call_log_.push_back("StartUpdateThread"); +} + +void MockUpdater::Reset() { + init_count_ = 0; + validate_count_ = 0; + launch_start_count_ = 0; + launch_success_count_ = 0; + launch_failure_count_ = 0; + start_update_thread_count_ = 0; + init_result_ = true; + should_auto_update_ = false; + next_boot_patch_path_.clear(); + last_release_version_.clear(); + last_yaml_config_.clear(); + call_log_.clear(); +} + +} // namespace shorebird +} // namespace flutter diff --git a/engine/src/flutter/shell/common/shorebird/updater.h b/engine/src/flutter/shell/common/shorebird/updater.h new file mode 100644 index 0000000000000..db0f44895aabb --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/updater.h @@ -0,0 +1,279 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_COMMON_SHOREBIRD_UPDATER_H_ +#define FLUTTER_SHELL_COMMON_SHOREBIRD_UPDATER_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace flutter { +namespace shorebird { + +/// File callbacks for iOS patch loading. +/// Mirrors the FileCallbacks struct from the Rust updater. +struct FileCallbacks { + void* (*open)(void); + uintptr_t (*read)(void* file_handle, uint8_t* buffer, uintptr_t count); + int64_t (*seek)(void* file_handle, int64_t offset, int32_t whence); + void (*close)(void* file_handle); +}; + +/// Configuration for initializing the Shorebird updater. +struct AppConfig { + /// Version string for this release (e.g., "1.0.0+1"). + std::string release_version; + + /// Paths to the original AOT libraries (libapp.so on Android, App.framework + /// on iOS). + std::vector original_libapp_paths; + + /// Directory for persistent updater state (survives app updates). + std::string app_storage_dir; + + /// Directory for cached artifacts (cleared on app updates). + std::string code_cache_dir; + + /// Callbacks for iOS patch file access (can be null callbacks on Android). + FileCallbacks file_callbacks; + + /// YAML configuration from shorebird.yaml. + std::string yaml_config; +}; + +/// Abstract interface for the Shorebird updater. +/// +/// This abstraction allows for: +/// 1. Mocking in tests without requiring the real Rust library +/// 2. Future migration from Rust to C++ implementation +/// 3. Test instrumentation (call counting, logging) +/// +/// ## Launch lifecycle (start/success/failure) +/// +/// The Rust updater uses a start/success/failure protocol to detect crashes: +/// - `ReportLaunchStart` copies `next_boot` โ†’ `current_boot` in the Rust +/// state. If the app crashes before `ReportLaunchSuccess`, the updater +/// assumes the patch caused the crash and rolls back on the next launch. +/// +/// These calls are guarded to execute at most once per process because: +/// 1. The Rust updater is a process-global singleton โ€” calling +/// `report_launch_start` multiple times would repeatedly copy `next_boot` +/// โ†’ `current_boot`, which could promote a newly-downloaded (but not yet +/// booted) patch to "current" even though the running engine loaded the +/// old snapshot. +/// 2. In add-to-app, multiple FlutterEngines may be created and destroyed +/// within a single process. Each engine creation resolves snapshots and +/// constructs a Shell, but we must only report launch start/success once +/// โ€” for the first engine that actually boots. Without this guard, a +/// background update that completes between engine creations would get +/// promoted to "current" by the second engine's `ReportLaunchStart`, +/// even though that engine is still running the old snapshot. +/// +/// Tests can call `ResetLaunchStateForTesting()` to re-enable the guards. +class Updater { + public: + virtual ~Updater() = default; + + /// Initialize the updater with configuration. + /// Retains a copy of `config` (see `GetAppConfig`) and calls the + /// implementation's `DoInit`. + /// @param config Configuration containing release version, paths, and + /// callbacks + /// @return true if initialization succeeded + bool Init(const AppConfig& config); + + /// The configuration passed to the last successful `Init` call. Hot + /// restart uses `original_libapp_paths` to fall back to the base release + /// when the running patch has been rolled back. + const AppConfig& GetAppConfig() const { return app_config_; } + + /// Validate the next boot patch. If invalid, falls back to last good state. + virtual void ValidateNextBootPatch() = 0; + + /// Get the path to the patch that will boot on next run. + /// @return Path to patch, or empty string if no patch available + virtual std::string NextBootPatchPath() = 0; + + // Boot lifecycle methods โ€” guarded to run at most once per launch cycle. + // Callers may call these freely; subsequent calls after the first are + // silently ignored. A process normally has exactly one launch cycle; hot + // restart begins another via `BeginNewLaunchCycle`. + void ReportLaunchStart(); + void ReportLaunchSuccess(); + void ReportLaunchFailure(); + + /// Re-arms the launch lifecycle guards so a hot restart can report a new + /// launch cycle (start, then success/failure) to the Rust updater โ€” + /// exactly like a fresh process boot. Only the hot restart host may call + /// this, immediately before re-resolving the isolate snapshot. + static void BeginNewLaunchCycle(); + + // Hot restart hosts. + // + // A host is a Shell that can tear down and relaunch its Dart isolate from + // the current next-boot patch. Hosts register at Shell setup and + // unregister at Shell destruction. `RequestRestart` is invoked (on an + // arbitrary thread) when Dart calls `shorebird_restart_app`; the host's + // callback must schedule the restart asynchronously and return whether it + // was scheduled. Restarting is only supported with exactly one live host: + // with multiple engines in one process (add-to-app, FlutterEngineGroup) + // there is no safe way to restart them all against a shared updater + // state, so the request is rejected. + static void RegisterRestartHost(uintptr_t host_id, + std::function request_restart); + static void UnregisterRestartHost(uintptr_t host_id); + static bool RequestRestart(); + + /// Marks hot restart as supported for this process. Set by the mobile + /// (Android/iOS) ConfigureShorebird path, where snapshot re-resolution via + /// `Settings::application_library_paths` picks up a newly installed patch. + /// Desktop embedders resolve AOT data through the embedder API instead; a + /// restart there would silently relaunch the old snapshot, so they do not + /// set this. + static void SetHotRestartSupported(bool supported); + static bool HotRestartSupported(); + + // Update checking + virtual bool ShouldAutoUpdate() = 0; + virtual void StartUpdateThread() = 0; + + // Singleton access + static Updater& Instance(); + + // Test support - allows injecting a mock implementation + static void SetInstanceForTesting(std::unique_ptr instance); + static void ResetInstanceForTesting(); + + /// Resets the once-per-launch-cycle guards so tests can verify + /// start/success/failure calls on fresh Updater instances. + static void ResetLaunchStateForTesting(); + + protected: + Updater() = default; + + // Subclass hooks โ€” called by the public guarded methods above. + virtual bool DoInit(const AppConfig& config) = 0; + virtual void DoReportLaunchStart() = 0; + virtual void DoReportLaunchSuccess() = 0; + virtual void DoReportLaunchFailure() = 0; + + private: + AppConfig app_config_; + + static std::unique_ptr instance_; + static std::mutex instance_mutex_; + + // Once-per-launch-cycle guards. See `BeginNewLaunchCycle`. + static std::atomic launch_started_; + static std::atomic launch_completed_; + + // Hot restart host registry. + static std::mutex restart_hosts_mutex_; + static std::map> restart_hosts_; + static std::atomic hot_restart_supported_; +}; + +/// No-op implementation for unsupported platforms. +/// All methods are safe to call but do nothing. +class NoOpUpdater : public Updater { + public: + NoOpUpdater() = default; + ~NoOpUpdater() override = default; + + bool DoInit(const AppConfig& config) override { return true; } + void ValidateNextBootPatch() override {} + std::string NextBootPatchPath() override { return ""; } + void DoReportLaunchStart() override {} + void DoReportLaunchSuccess() override {} + void DoReportLaunchFailure() override {} + bool ShouldAutoUpdate() override { return false; } + void StartUpdateThread() override {} +}; + +#if SHOREBIRD_PLATFORM_SUPPORTED +/// Production implementation that wraps the Rust updater C API. +/// Only available on supported platforms (Android, iOS, macOS, Windows, Linux). +class RealUpdater : public Updater { + public: + RealUpdater() = default; + ~RealUpdater() override = default; + + bool DoInit(const AppConfig& config) override; + void ValidateNextBootPatch() override; + std::string NextBootPatchPath() override; + void DoReportLaunchStart() override; + void DoReportLaunchSuccess() override; + void DoReportLaunchFailure() override; + bool ShouldAutoUpdate() override; + void StartUpdateThread() override; +}; +#endif // SHOREBIRD_PLATFORM_SUPPORTED + +/// Mock implementation for testing. +/// Tracks call counts and can be queried to verify behavior. +class MockUpdater : public Updater { + public: + MockUpdater() = default; + ~MockUpdater() override = default; + + bool DoInit(const AppConfig& config) override; + void ValidateNextBootPatch() override; + std::string NextBootPatchPath() override; + void DoReportLaunchStart() override; + void DoReportLaunchSuccess() override; + void DoReportLaunchFailure() override; + bool ShouldAutoUpdate() override; + void StartUpdateThread() override; + + // Test accessors + int init_count() const { return init_count_; } + int validate_count() const { return validate_count_; } + int launch_start_count() const { return launch_start_count_; } + int launch_success_count() const { return launch_success_count_; } + int launch_failure_count() const { return launch_failure_count_; } + int start_update_thread_count() const { return start_update_thread_count_; } + const std::vector& call_log() const { return call_log_; } + + // Last init parameters (for verification) + const std::string& last_release_version() const { + return last_release_version_; + } + const std::string& last_yaml_config() const { return last_yaml_config_; } + + // Test configuration + void set_init_result(bool value) { init_result_ = value; } + void set_should_auto_update(bool value) { should_auto_update_ = value; } + void set_next_boot_patch_path(const std::string& path) { + next_boot_patch_path_ = path; + } + + // Reset all counters and logs + void Reset(); + + private: + int init_count_ = 0; + int validate_count_ = 0; + int launch_start_count_ = 0; + int launch_success_count_ = 0; + int launch_failure_count_ = 0; + int start_update_thread_count_ = 0; + bool init_result_ = true; + bool should_auto_update_ = false; + std::string next_boot_patch_path_; + std::string last_release_version_; + std::string last_yaml_config_; + std::vector call_log_; +}; + +} // namespace shorebird +} // namespace flutter + +#endif // FLUTTER_SHELL_COMMON_SHOREBIRD_UPDATER_H_ diff --git a/engine/src/flutter/shell/common/shorebird/updater_unittests.cc b/engine/src/flutter/shell/common/shorebird/updater_unittests.cc new file mode 100644 index 0000000000000..70b110dcbef72 --- /dev/null +++ b/engine/src/flutter/shell/common/shorebird/updater_unittests.cc @@ -0,0 +1,294 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/common/shorebird/updater.h" + +#include "gtest/gtest.h" + +namespace flutter { +namespace shorebird { +namespace testing { + +class UpdaterTest : public ::testing::Test { + protected: + void SetUp() override { + // Install a mock for each test and reset the once-per-process guards + // so each test starts with a clean slate. + auto mock = std::make_unique(); + mock_ = mock.get(); + Updater::SetInstanceForTesting(std::move(mock)); + Updater::ResetLaunchStateForTesting(); + } + + void TearDown() override { + mock_ = nullptr; + Updater::ResetInstanceForTesting(); + Updater::ResetLaunchStateForTesting(); + } + + MockUpdater* mock_ = nullptr; +}; + +// ReportLaunchStart is guarded to run at most once per process. +// The second call should be silently ignored. +TEST_F(UpdaterTest, ReportLaunchStartOnlyCallsOnce) { + EXPECT_EQ(mock_->launch_start_count(), 0); + + Updater::Instance().ReportLaunchStart(); + EXPECT_EQ(mock_->launch_start_count(), 1); + + // Second call is a no-op due to the once-per-process guard. + Updater::Instance().ReportLaunchStart(); + EXPECT_EQ(mock_->launch_start_count(), 1); +} + +TEST_F(UpdaterTest, ReportLaunchSuccessOnlyCallsOnce) { + EXPECT_EQ(mock_->launch_success_count(), 0); + + Updater::Instance().ReportLaunchSuccess(); + EXPECT_EQ(mock_->launch_success_count(), 1); + + // Second call is a no-op. + Updater::Instance().ReportLaunchSuccess(); + EXPECT_EQ(mock_->launch_success_count(), 1); +} + +TEST_F(UpdaterTest, ReportLaunchFailureOnlyCallsOnce) { + EXPECT_EQ(mock_->launch_failure_count(), 0); + + Updater::Instance().ReportLaunchFailure(); + EXPECT_EQ(mock_->launch_failure_count(), 1); + + // Second call is a no-op. + Updater::Instance().ReportLaunchFailure(); + EXPECT_EQ(mock_->launch_failure_count(), 1); +} + +TEST_F(UpdaterTest, MockUpdaterTracksShouldAutoUpdate) { + mock_->set_should_auto_update(false); + EXPECT_FALSE(Updater::Instance().ShouldAutoUpdate()); + + mock_->set_should_auto_update(true); + EXPECT_TRUE(Updater::Instance().ShouldAutoUpdate()); +} + +TEST_F(UpdaterTest, MockUpdaterTracksStartUpdateThreadCalls) { + EXPECT_EQ(mock_->start_update_thread_count(), 0); + + Updater::Instance().StartUpdateThread(); + EXPECT_EQ(mock_->start_update_thread_count(), 1); +} + +TEST_F(UpdaterTest, MockUpdaterCallLogRecordsSequence) { + EXPECT_TRUE(mock_->call_log().empty()); + + Updater::Instance().ReportLaunchStart(); + Updater::Instance().ShouldAutoUpdate(); + Updater::Instance().ReportLaunchSuccess(); + + const auto& log = mock_->call_log(); + ASSERT_EQ(log.size(), 3u); + EXPECT_EQ(log[0], "ReportLaunchStart"); + EXPECT_EQ(log[1], "ShouldAutoUpdate"); + EXPECT_EQ(log[2], "ReportLaunchSuccess"); +} + +TEST_F(UpdaterTest, MockUpdaterResetClearsState) { + Updater::Instance().ReportLaunchStart(); + Updater::Instance().ReportLaunchSuccess(); + mock_->set_should_auto_update(true); + + EXPECT_EQ(mock_->launch_start_count(), 1); + EXPECT_EQ(mock_->launch_success_count(), 1); + EXPECT_TRUE(mock_->ShouldAutoUpdate()); + + mock_->Reset(); + + EXPECT_EQ(mock_->launch_start_count(), 0); + EXPECT_EQ(mock_->launch_success_count(), 0); + // Check call_log before ShouldAutoUpdate() since the method adds to call_log + EXPECT_TRUE(mock_->call_log().empty()); + EXPECT_FALSE(mock_->ShouldAutoUpdate()); +} + +// ReportLaunchStart and ReportLaunchSuccess are paired once per process. +// The Rust updater no-ops both when no patch is booting. +TEST_F(UpdaterTest, LaunchStartAndSuccessArePairedOncePerProcess) { + Updater::Instance().ReportLaunchStart(); + Updater::Instance().ReportLaunchSuccess(); + + EXPECT_EQ(mock_->launch_start_count(), 1); + EXPECT_EQ(mock_->launch_success_count(), 1); + const auto& log = mock_->call_log(); + ASSERT_EQ(log.size(), 2u); + EXPECT_EQ(log[0], "ReportLaunchStart"); + EXPECT_EQ(log[1], "ReportLaunchSuccess"); +} + +// ReportLaunchStart and ReportLaunchFailure are paired once per process. +TEST_F(UpdaterTest, LaunchStartAndFailureArePairedOncePerProcess) { + Updater::Instance().ReportLaunchStart(); + Updater::Instance().ReportLaunchFailure(); + + EXPECT_EQ(mock_->launch_start_count(), 1); + EXPECT_EQ(mock_->launch_failure_count(), 1); + const auto& log = mock_->call_log(); + ASSERT_EQ(log.size(), 2u); + EXPECT_EQ(log[0], "ReportLaunchStart"); + EXPECT_EQ(log[1], "ReportLaunchFailure"); +} + +// Simulates the add-to-app scenario: multiple engines call ReportLaunchStart +// and ReportLaunchSuccess, but only the first should actually reach the +// updater. This prevents the Rust updater from promoting a newly-downloaded +// patch to "current_boot" when subsequent engines are still running the +// original snapshot. +TEST_F(UpdaterTest, MultipleEnginesOnlyReportOnce) { + // First engine boots. + Updater::Instance().ReportLaunchStart(); + Updater::Instance().ReportLaunchSuccess(); + + // Second engine boots โ€” these should be no-ops. + Updater::Instance().ReportLaunchStart(); + Updater::Instance().ReportLaunchSuccess(); + + EXPECT_EQ(mock_->launch_start_count(), 1); + EXPECT_EQ(mock_->launch_success_count(), 1); + + const auto& log = mock_->call_log(); + ASSERT_EQ(log.size(), 2u); + EXPECT_EQ(log[0], "ReportLaunchStart"); + EXPECT_EQ(log[1], "ReportLaunchSuccess"); +} + +// ResetLaunchStateForTesting re-enables the guards, allowing tests to +// verify launch calls on a fresh state. +TEST_F(UpdaterTest, ResetLaunchStateReenablesGuards) { + Updater::Instance().ReportLaunchStart(); + Updater::Instance().ReportLaunchSuccess(); + EXPECT_EQ(mock_->launch_start_count(), 1); + EXPECT_EQ(mock_->launch_success_count(), 1); + + Updater::ResetLaunchStateForTesting(); + + Updater::Instance().ReportLaunchStart(); + Updater::Instance().ReportLaunchSuccess(); + EXPECT_EQ(mock_->launch_start_count(), 2); + EXPECT_EQ(mock_->launch_success_count(), 2); +} + +// BeginNewLaunchCycle re-arms the launch guards so a hot restart can report +// a full second launch cycle (start, then success/failure) โ€” exactly like a +// fresh process boot. This is the engine-side half of the hot restart +// launch-cycle contract (the Rust half is covered by hot_restart_tests in +// the updater repo). +TEST_F(UpdaterTest, BeginNewLaunchCycleAllowsSecondLaunchCycle) { + // Cold boot. + Updater::Instance().ReportLaunchStart(); + Updater::Instance().ReportLaunchSuccess(); + + // Hot restart begins a new launch cycle. + Updater::BeginNewLaunchCycle(); + Updater::Instance().ReportLaunchStart(); + Updater::Instance().ReportLaunchFailure(); + + EXPECT_EQ(mock_->launch_start_count(), 2); + EXPECT_EQ(mock_->launch_success_count(), 1); + EXPECT_EQ(mock_->launch_failure_count(), 1); + + const auto& log = mock_->call_log(); + ASSERT_EQ(log.size(), 4u); + EXPECT_EQ(log[0], "ReportLaunchStart"); + EXPECT_EQ(log[1], "ReportLaunchSuccess"); + EXPECT_EQ(log[2], "ReportLaunchStart"); + EXPECT_EQ(log[3], "ReportLaunchFailure"); +} + +// Init retains the configuration so the hot restart can fall back to the +// original libapp paths when no patch is installed. +TEST_F(UpdaterTest, InitRetainsAppConfig) { + AppConfig config; + config.release_version = "1.2.3+4"; + config.original_libapp_paths = {"/data/app/libapp.so"}; + config.yaml_config = "app_id: foo"; + + EXPECT_TRUE(Updater::Instance().Init(config)); + + const AppConfig& stored = Updater::Instance().GetAppConfig(); + EXPECT_EQ(stored.release_version, "1.2.3+4"); + ASSERT_EQ(stored.original_libapp_paths.size(), 1u); + EXPECT_EQ(stored.original_libapp_paths[0], "/data/app/libapp.so"); + EXPECT_EQ(mock_->init_count(), 1); +} + +class RestartHostTest : public ::testing::Test { + protected: + void TearDown() override { + // The registry is process-global; leave it empty for other tests. + Updater::UnregisterRestartHost(1); + Updater::UnregisterRestartHost(2); + Updater::SetHotRestartSupported(false); + } +}; + +// With no registered host (no shell capable of restarting), a restart +// request is rejected. +TEST_F(RestartHostTest, RequestRestartWithoutHostIsRejected) { + EXPECT_FALSE(Updater::RequestRestart()); +} + +// With exactly one host, the request is forwarded and its result relayed. +TEST_F(RestartHostTest, RequestRestartForwardsToSingleHost) { + int requests = 0; + Updater::RegisterRestartHost(1, [&requests]() { + requests++; + return true; + }); + EXPECT_TRUE(Updater::RequestRestart()); + EXPECT_EQ(requests, 1); + + Updater::RegisterRestartHost(1, []() { return false; }); + EXPECT_FALSE(Updater::RequestRestart()); + EXPECT_EQ(requests, 1); +} + +// With more than one live host (add-to-app / FlutterEngineGroup), restart +// is unsupported and the request is rejected without invoking any host. +TEST_F(RestartHostTest, RequestRestartWithMultipleHostsIsRejected) { + int requests = 0; + auto host = [&requests]() { + requests++; + return true; + }; + Updater::RegisterRestartHost(1, host); + Updater::RegisterRestartHost(2, host); + EXPECT_FALSE(Updater::RequestRestart()); + EXPECT_EQ(requests, 0); + + // Back to one host (e.g. the other engine was destroyed): supported again. + Updater::UnregisterRestartHost(2); + EXPECT_TRUE(Updater::RequestRestart()); + EXPECT_EQ(requests, 1); +} + +// Unregistering an unknown host is a no-op (a shell that never registered +// still unregisters in its destructor). +TEST_F(RestartHostTest, UnregisterUnknownHostIsNoOp) { + Updater::UnregisterRestartHost(42); + EXPECT_FALSE(Updater::RequestRestart()); +} + +// The hot-restart-supported flag is process-global, set only by the mobile +// ConfigureShorebird path. +TEST_F(RestartHostTest, HotRestartSupportedFlag) { + EXPECT_FALSE(Updater::HotRestartSupported()); + Updater::SetHotRestartSupported(true); + EXPECT_TRUE(Updater::HotRestartSupported()); + Updater::SetHotRestartSupported(false); + EXPECT_FALSE(Updater::HotRestartSupported()); +} + +} // namespace testing +} // namespace shorebird +} // namespace flutter diff --git a/engine/src/flutter/shell/platform/android/BUILD.gn b/engine/src/flutter/shell/platform/android/BUILD.gn index c15ac362d7d8a..37ea5b0430e44 100644 --- a/engine/src/flutter/shell/platform/android/BUILD.gn +++ b/engine/src/flutter/shell/platform/android/BUILD.gn @@ -171,6 +171,7 @@ source_set("flutter_shell_native_src") { "//flutter/runtime", "//flutter/runtime:libdart", "//flutter/shell/common", + "//flutter/shell/common/shorebird", "//flutter/shell/platform/android/context", "//flutter/shell/platform/android/external_view_embedder", "//flutter/shell/platform/android/jni", @@ -809,8 +810,10 @@ if (target_cpu != "x86") { } } -if (host_os == "linux" && - (target_cpu == "x64" || target_cpu == "arm64" || target_cpu == "riscv64")) { +# Build analyze_snapshot for 64-bit target CPUs on linux. +# Or always targeting arm64 for Shorebird builds. +if ((host_os == "linux" && (target_cpu == "x64" || target_cpu == "riscv64")) || + target_cpu == "arm64") { zip_bundle("analyze_snapshot") { deps = [ "$dart_src/runtime/bin:analyze_snapshot($host_toolchain)" ] diff --git a/engine/src/flutter/shell/platform/android/android_exports.lst b/engine/src/flutter/shell/platform/android/android_exports.lst index 198bff773dd74..349b25e04930f 100644 --- a/engine/src/flutter/shell/platform/android/android_exports.lst +++ b/engine/src/flutter/shell/platform/android/android_exports.lst @@ -11,6 +11,15 @@ _binary_icudtl_dat_size; InternalFlutterGpu*; kInternalFlutterGpu*; + shorebird_init; + shorebird_free_string; + shorebird_free_update_result; + shorebird_check_for_downloadable_update; + shorebird_update_with_result; + shorebird_next_boot_patch_number; + shorebird_current_boot_patch_number; + shorebird_restart_app; + shorebird_validate_next_boot_patch; local: *; }; diff --git a/engine/src/flutter/shell/platform/android/flutter_main.cc b/engine/src/flutter/shell/platform/android/flutter_main.cc index d14ac40029645..45e369f5b3a9d 100644 --- a/engine/src/flutter/shell/platform/android/flutter_main.cc +++ b/engine/src/flutter/shell/platform/android/flutter_main.cc @@ -20,6 +20,7 @@ #include "flutter/fml/platform/android/paths_android.h" #include "flutter/lib/ui/plugins/callback_cache.h" #include "flutter/runtime/dart_vm.h" +#include "flutter/shell/common/shorebird/shorebird.h" #include "flutter/shell/common/switches.h" #include "flutter/shell/platform/android/android_context_vk_impeller.h" #include "flutter/shell/platform/android/android_rendering_selector.h" @@ -93,6 +94,9 @@ void FlutterMain::Init(JNIEnv* env, jstring kernelPath, jstring appStoragePath, jstring engineCachesPath, + jstring shorebirdYaml, + jstring version, + jstring versionCode, jlong initTimeMillis, jint api_level) { std::vector args; @@ -151,8 +155,18 @@ void FlutterMain::Init(JNIEnv* env, flutter::DartCallbackCache::SetCachePath( fml::jni::JavaStringToString(env, appStoragePath)); - fml::paths::InitializeAndroidCachesPath( - fml::jni::JavaStringToString(env, engineCachesPath)); + auto code_cache_path = fml::jni::JavaStringToString(env, engineCachesPath); + auto app_storage_path = fml::jni::JavaStringToString(env, appStoragePath); + fml::paths::InitializeAndroidCachesPath(code_cache_path); + +#if FLUTTER_RELEASE + std::string shorebird_yaml = fml::jni::JavaStringToString(env, shorebirdYaml); + std::string version_string = fml::jni::JavaStringToString(env, version); + std::string version_code_string = + fml::jni::JavaStringToString(env, versionCode); + ConfigureShorebird(code_cache_path, app_storage_path, settings, + shorebird_yaml, version_string, version_code_string); +#endif flutter::DartCallbackCache::LoadCacheFromDisk(); @@ -245,6 +259,7 @@ bool FlutterMain::Register(JNIEnv* env) { { .name = "nativeInit", .signature = "(Landroid/content/Context;[Ljava/lang/String;Ljava/" + "lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/" "lang/String;Ljava/lang/String;Ljava/lang/String;JI)V", .fnPtr = reinterpret_cast(&Init), }, diff --git a/engine/src/flutter/shell/platform/android/flutter_main.h b/engine/src/flutter/shell/platform/android/flutter_main.h index dda959801bc32..cf03f889ec30e 100644 --- a/engine/src/flutter/shell/platform/android/flutter_main.h +++ b/engine/src/flutter/shell/platform/android/flutter_main.h @@ -44,6 +44,9 @@ class FlutterMain { jstring kernelPath, jstring appStoragePath, jstring engineCachesPath, + jstring shorebirdYaml, + jstring version, + jstring versionCode, jlong initTimeMillis, jint api_level); diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java index 520f55ab6ccf0..a5dd8fda9b050 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -194,6 +194,12 @@ void synchronizePressingKey( } } + // Virtual keyboards (like Gboard) often keep meta bits active without sending + // corresponding physical key events. For these, we trust the meta state + // and skip synthesizing physical modifier keys to avoid "stuck" modifiers. + boolean isVirtualKeyboard = + event.getDeviceId() == android.view.KeyCharacterMap.VIRTUAL_KEYBOARD; + // Fill the rest of the pre-event states to match the true state. if (truePressed) { // It is required that at least one key is pressed. @@ -201,14 +207,14 @@ void synchronizePressingKey( if (preEventStates[keyIdx] != null) { continue; } - if (postEventAnyPressed) { + if (postEventAnyPressed || isVirtualKeyboard) { preEventStates[keyIdx] = nowStates[keyIdx]; } else { preEventStates[keyIdx] = true; postEventAnyPressed = true; } } - if (!postEventAnyPressed) { + if (!postEventAnyPressed && !isVirtualKeyboard) { preEventStates[0] = true; } } else { diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index 59dc281909838..8570bad6d8b95 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -8,6 +8,8 @@ import android.annotation.SuppressLint; import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.SurfaceTexture; @@ -42,6 +44,10 @@ import io.flutter.view.AccessibilityBridge; import io.flutter.view.FlutterCallbackInformation; import io.flutter.view.TextureRegistry; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -178,6 +184,9 @@ private static native void nativeInit( @Nullable String bundlePath, @NonNull String appStoragePath, @NonNull String engineCachesPath, + @Nullable String shorebirdYaml, + @Nullable String version, + @Nullable String versionCode, long initTimeMillis, int apiLevel); @@ -206,8 +215,47 @@ public void init( Log.w(TAG, "FlutterJNI.init called more than once"); } + String version = null; + String versionCode = null; + try { + PackageInfo packageInfo = + context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + version = packageInfo.versionName; + if (Build.VERSION.SDK_INT >= API_LEVELS.API_28) { + versionCode = String.valueOf(packageInfo.getLongVersionCode()); + } else { + versionCode = String.valueOf(packageInfo.versionCode); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Failed to read app version. Shorebird updater can't run.", e); + } + + String shorebirdYaml = null; + try { + InputStream yaml = context.getAssets().open("flutter_assets/shorebird.yaml"); + BufferedReader r = new BufferedReader(new InputStreamReader(yaml)); + StringBuilder total = new StringBuilder(); + for (String line; (line = r.readLine()) != null; ) { + total.append(line).append('\n'); + } + shorebirdYaml = total.toString(); + Log.d(TAG, "shorebird.yaml: " + shorebirdYaml); + } catch (IOException e) { + Log.e(TAG, "Failed to load shorebird.yaml", e); + Log.e(TAG, "Did you remember to include shorebird.yaml in your pubspec.yaml's assets?"); + } + FlutterJNI.nativeInit( - context, args, bundlePath, appStoragePath, engineCachesPath, initTimeMillis, apiLevel); + context, + args, + bundlePath, + appStoragePath, + engineCachesPath, + shorebirdYaml, + version, + versionCode, + initTimeMillis, + apiLevel); FlutterJNI.initCalled = true; } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java index 75fb8106732f6..1dd3b78dfe4cc 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java @@ -785,9 +785,9 @@ private void cleanup() { } @RequiresApi(API_LEVELS.API_33) - private void waitOnFence(Image image) { - try { - SyncFence fence = image.getFence(); + @VisibleForTesting + void waitOnFence(Image image) { + try (SyncFence fence = image.getFence()) { fence.awaitForever(); } catch (IOException e) { // Drop. @@ -1066,8 +1066,7 @@ public void pushImage(Image image) { @RequiresApi(API_LEVELS.API_33) private void waitOnFence(Image image) { - try { - SyncFence fence = image.getFence(); + try (SyncFence fence = image.getFence()) { fence.awaitForever(); } catch (IOException e) { // Drop. diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java index aabdc12ac403e..50b63f2f5e7d9 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java @@ -456,6 +456,10 @@ private void restoreSystemChromeSystemUIOverlays() { * io.flutter.embedding.android.FlutterFragmentActivity}. */ private void enableEdgeToEdge() { + // Clear previously applied system UI visibility flags to ensure that + // WindowInsetsControllerCompat can correctly configure the window. + activity.getWindow().getDecorView().setSystemUiVisibility(0); + WindowCompat.setDecorFitsSystemWindows(activity.getWindow(), false); } diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java index c347cae538b5f..ec74f5e8df73c 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -799,6 +799,100 @@ public void tapLowerA() { calls.clear(); } + @Test + public void virtualKeyboardShiftUpClearsState() { + final KeyboardTester tester = new KeyboardTester(); + final ArrayList calls = new ArrayList<>(); + + tester.recordEmbedderCallsTo(calls); + tester.respondToTextInputWith(true); // Suppress redispatching + + final long virtualShiftLeftPhysicalKey = KEYCODE_SHIFT_LEFT | KeyboardMap.kAndroidPlane; + + // 1. Simulate ShiftLeft DOWN from virtual keyboard (scancode = 0) + assertTrue( + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_DOWN, + 0, + KEYCODE_SHIFT_LEFT, + 0, + '\0', + META_SHIFT_ON, + KeyCharacterMap.VIRTUAL_KEYBOARD, + 0))); + + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData( + Type.kDown, + virtualShiftLeftPhysicalKey, + LOGICAL_SHIFT_LEFT, + null, + false, + DeviceType.kKeyboard), + }); + calls.clear(); + + // 2. Simulate ShiftLeft UP from virtual keyboard (scancode = 0) with meta bit still active + assertTrue( + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_UP, + 0, + KEYCODE_SHIFT_LEFT, + 0, + '\0', + META_SHIFT_ON, + KeyCharacterMap.VIRTUAL_KEYBOARD, + 0))); + + // Verify that it does NOT synthesize SHIFT_RIGHT DOWN. + // It should only send SHIFT_LEFT UP. + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData( + Type.kUp, + virtualShiftLeftPhysicalKey, + LOGICAL_SHIFT_LEFT, + null, + false, + DeviceType.kKeyboard), + }); + calls.clear(); + + // 3. Simulate Backspace DOWN from virtual keyboard (scancode = 0) with meta bit still active + final long virtualBackspacePhysicalKey = KEYCODE_DEL | KeyboardMap.kAndroidPlane; + assertTrue( + tester.keyboardManager.handleEvent( + new FakeKeyEvent( + ACTION_DOWN, + 0, + KEYCODE_DEL, + 0, + '\0', + META_SHIFT_ON, + KeyCharacterMap.VIRTUAL_KEYBOARD, + 0))); + + // Verify that it does NOT synthesize SHIFT_LEFT DOWN or SHIFT_RIGHT DOWN. + // It should only send Backspace DOWN. + verifyEmbedderEvents( + calls, + new KeyData[] { + buildKeyData( + Type.kDown, + virtualBackspacePhysicalKey, + LOGICAL_BACKSPACE, + null, + false, + DeviceType.kKeyboard), + }); + calls.clear(); + } + @Test public void tapUpperA() { final KeyboardTester tester = new KeyboardTester(); diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java index f8339a6022223..9be4c08bd0228 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java @@ -28,6 +28,7 @@ import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.SurfaceTexture; +import android.hardware.SyncFence; import android.media.Image; import android.media.ImageReader; import android.os.Looper; @@ -1074,4 +1075,16 @@ public void restoreSurfaceProducers_restoresImageReaderSurfaceProducersAsIfAppli assertFalse(imageReaderProducer1.notifiedDestroy); assertFalse(imageReaderProducer2.notifiedDestroy); } + + @Test + public void waitOnFence_closesFence() throws Exception { + FlutterRenderer.ImageReaderSurfaceProducer producer = + (FlutterRenderer.ImageReaderSurfaceProducer) + engineRule.getFlutterEngine().getRenderer().createSurfaceProducer(); + Image image = mock(Image.class); + SyncFence fence = mock(SyncFence.class); + when(image.getFence()).thenReturn(fence); + producer.waitOnFence(image); + verify(fence, times(1)).close(); + } } diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java index 00e729ad0bca3..34c5ec9f001d4 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java @@ -643,6 +643,34 @@ public void updateSystemUiOverlays_inEdgeToEdge_reappliesModernApi() { } } + @SuppressWarnings("deprecation") + @Config(sdk = API_LEVELS.API_29) + @Test + public void switchFromAnyModeToEdgeToEdge_clearsSystemUiVisibility() { + PlatformChannel.SystemUiMode[] modes = { + PlatformChannel.SystemUiMode.LEAN_BACK, + PlatformChannel.SystemUiMode.IMMERSIVE, + PlatformChannel.SystemUiMode.IMMERSIVE_STICKY + }; + try (MockedStatic windowCompatMock = mockStatic(WindowCompat.class)) { + for (PlatformChannel.SystemUiMode mode : modes) { + View fakeDecorView = mock(View.class); + Window fakeWindow = mock(Window.class); + Activity mockActivity = mock(Activity.class); + when(fakeWindow.getDecorView()).thenReturn(fakeDecorView); + when(mockActivity.getWindow()).thenReturn(fakeWindow); + PlatformPlugin platformPlugin = new PlatformPlugin(mockActivity, mockPlatformChannel); + + platformPlugin.mPlatformMessageHandler.showSystemUiMode(mode); + platformPlugin.mPlatformMessageHandler.showSystemUiMode( + PlatformChannel.SystemUiMode.EDGE_TO_EDGE); + + verify(fakeDecorView).setSystemUiVisibility(0); + windowCompatMock.verify(() -> WindowCompat.setDecorFitsSystemWindows(fakeWindow, false)); + } + } + } + @SuppressWarnings("deprecation") // FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_NAVIGATION @Config(sdk = API_LEVELS.API_29) diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java b/engine/src/flutter/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java index 35ef6731d6ec8..eabf3f95355e5 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java @@ -26,6 +26,19 @@ public FakeKeyEvent( this.character = character; } + public FakeKeyEvent( + int action, + int scancode, + int code, + int repeat, + char character, + int metaState, + int deviceId, + int source) { + super(0, 0, action, code, repeat, metaState, deviceId, scancode, 0, source); + this.character = character; + } + private char character = 0; public final int getUnicodeChar() { diff --git a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn index 2930099a5fe05..a50b691edffe2 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn @@ -195,12 +195,14 @@ source_set("flutter_framework_source") { deps = [ ":ios_gpu_configuration", + "$dart_src/runtime/bin:elf_loader", "//flutter/common", "//flutter/common/graphics", "//flutter/fml", "//flutter/lib/ui", "//flutter/runtime", "//flutter/shell/common", + "//flutter/shell/common/shorebird", "//flutter/shell/platform/common:common_cpp_input", "//flutter/shell/platform/darwin/common", "//flutter/shell/platform/darwin/common:framework_common", diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm index 86418369f9979..1d81b5f3cbf67 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm @@ -13,6 +13,8 @@ #include "flutter/common/constants.h" #include "flutter/fml/build_config.h" +#include "flutter/fml/paths.h" +#include "flutter/shell/common/shorebird/shorebird.h" #include "flutter/shell/common/switches.h" #import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h" #include "flutter/shell/platform/darwin/common/command_line.h" @@ -92,10 +94,12 @@ static BOOL DoesHardwareSupportWideGamut() { } if (flutter::DartVM::IsRunningPrecompiledCode()) { + NSLog(@"SANITY CHECK: Running precompiled code."); if (hasExplicitBundle) { NSString* executablePath = bundle.executablePath; if ([[NSFileManager defaultManager] fileExistsAtPath:executablePath]) { settings.application_library_paths.push_back(executablePath.UTF8String); + NSLog(@"Using precompiled library from %@", executablePath); } } @@ -107,6 +111,7 @@ static BOOL DoesHardwareSupportWideGamut() { NSString* executablePath = [NSBundle bundleWithPath:libraryPath].executablePath; if (executablePath.length > 0) { settings.application_library_paths.push_back(executablePath.UTF8String); + NSLog(@"Using library from %@", libraryPath); } } } @@ -121,6 +126,7 @@ static BOOL DoesHardwareSupportWideGamut() { [NSBundle bundleWithPath:applicationFrameworkPath].executablePath; if (executablePath.length > 0) { settings.application_library_paths.push_back(executablePath.UTF8String); + NSLog(@"Using App.framework from %@", applicationFrameworkPath); } } } @@ -152,6 +158,33 @@ static BOOL DoesHardwareSupportWideGamut() { } } + NSString* assetsPath = [NSString stringWithUTF8String:settings.assets_path.c_str()]; + NSLog(@"ASSET PATH %@", assetsPath); + + // FIXME: This may not be the correct path (e.g., should it include the organization id?) + // See + // https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW13 + // /private/var/mobile/Containers/Data/Application/264477BF-6E38-47C9-AAD9-532BB842F197/Library/Application + // Support/shorebird/shorebird_updater + std::string cache_path = + fml::paths::JoinPaths({getenv("HOME"), "Library/Application Support/shorebird"}); + NSURL* shorebirdYamlPath = [NSURL URLWithString:@"shorebird.yaml" + relativeToURL:[NSURL fileURLWithPath:assetsPath]]; + NSString* appVersion = [mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + NSString* appBuildNumber = [mainBundle objectForInfoDictionaryKey:@"CFBundleVersion"]; + NSString* shorebirdYamlContents = [NSString stringWithContentsOfURL:shorebirdYamlPath + encoding:NSUTF8StringEncoding + error:nil]; + if (shorebirdYamlContents != nil) { + // Note: we intentionally pass cache_path twice. We provide two different directories + // to ConfigureShorebird because Android differentiates between data that persists + // between releases and data that does not. iOS does not make this distinction. + flutter::ConfigureShorebird(cache_path, cache_path, settings, shorebirdYamlContents.UTF8String, + appVersion.UTF8String, appBuildNumber.UTF8String); + } else { + NSLog(@"Failed to find shorebird.yaml, not starting updater."); + } + // Domain network configuration // Disabled in https://github.com/flutter/flutter/issues/72723. // Re-enable in https://github.com/flutter/flutter/issues/54448. diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 831568de7759c..64a3dcbfb2268 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -39,6 +39,7 @@ #import "flutter/third_party/spring_animation/spring_animation.h" FLUTTER_ASSERT_ARC +#import static constexpr int kMicrosecondsPerSecond = 1000 * 1000; static constexpr CGFloat kScrollViewContentSize = 2.0; diff --git a/engine/src/flutter/shell/platform/darwin/macos/BUILD.gn b/engine/src/flutter/shell/platform/darwin/macos/BUILD.gn index ea069e2674648..d37ae587a8000 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/macos/BUILD.gn @@ -145,12 +145,14 @@ source_set("flutter_framework_source") { ":macos_gpu_configuration", "//flutter/flow", "//flutter/fml", + "//flutter/shell/common/shorebird", "//flutter/shell/platform/common:common_cpp_accessibility", "//flutter/shell/platform/common:common_cpp_core", "//flutter/shell/platform/common:common_cpp_enums", "//flutter/shell/platform/common:common_cpp_input", "//flutter/shell/platform/common:common_cpp_isolate_scope", "//flutter/shell/platform/common:common_cpp_switches", + "//flutter/shell/platform/darwin/common", "//flutter/shell/platform/darwin/common:availability_version_check", "//flutter/shell/platform/darwin/common:framework_common", "//flutter/shell/platform/darwin/graphics", diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index f83245afeaf8f..f4be157c5ef74 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -11,6 +11,8 @@ #include #include "flutter/common/constants.h" +#include "flutter/fml/paths.h" +#include "flutter/shell/common/shorebird/shorebird.h" #include "flutter/shell/platform/common/app_lifecycle_state.h" #include "flutter/shell/platform/common/engine_switches.h" #include "flutter/shell/platform/embedder/embedder.h" @@ -656,6 +658,40 @@ - (void)onFocusChangeRequest:(const FlutterViewFocusChangeRequest*)request { } } +- (BOOL)configureShorebird:(NSString**)patchPath { + NSLog(@"[shorebird] setting up non-linker shorebird"); + NSString* bundlePath = + [[NSBundle bundleWithURL:[NSBundle.mainBundle.privateFrameworksURL + URLByAppendingPathComponent:@"App.framework"]] bundlePath]; + bundlePath = [bundlePath stringByAppendingString:@"/App"]; + NSString* assetsPath = _project.assetsPath; + NSURL* shorebirdYamlPath = [NSURL URLWithString:@"shorebird.yaml" + relativeToURL:[NSURL fileURLWithPath:assetsPath]]; + NSString* shorebirdYamlContents = [NSString stringWithContentsOfURL:shorebirdYamlPath + encoding:NSUTF8StringEncoding + error:nil]; + NSString* appVersion = + [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + NSString* appBuildNumber = [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleVersion"]; + std::string cache_path = + fml::paths::JoinPaths({getenv("HOME"), "Library", "Application Support", "shorebird"}); + flutter::ReleaseVersion release_version = {appVersion.UTF8String, appBuildNumber.UTF8String}; + flutter::ShorebirdConfigArgs shorebird_args(cache_path, cache_path, bundlePath.UTF8String, + shorebirdYamlContents.UTF8String, release_version); + NSLog(@"[shorebird] calling ConfigureShorebird"); + std::string patch_path; + auto res = flutter::ConfigureShorebird(shorebird_args, patch_path); + if (!res) { + NSLog(@"[shorebird] ConfigureShorebird failed"); + return NO; + } + + NSLog(@"[shorebird] ConfigureShorebird success!"); + *patchPath = [NSString stringWithUTF8String:patch_path.c_str()]; + NSLog(@"[shorebird] patchPath: %@", *patchPath); + return YES; +} + - (BOOL)runWithEntrypoint:(NSString*)entrypoint { if (self.running) { return NO; @@ -677,8 +713,11 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { std::find(switches.begin(), switches.end(), "--enable-impeller=true") != switches.end()) { switches.push_back("--enable-impeller=true"); } - switches.push_back(_project.enableSDFs ? "--impeller-use-sdfs=true" - : "--impeller-use-sdfs=false"); + + if (_project.enableSDFs || + std::find(switches.begin(), switches.end(), "--impeller-use-sdfs=true") != switches.end()) { + switches.push_back("--impeller-use-sdfs=true"); + } if (_project.enableFlutterGPU || std::find(switches.begin(), switches.end(), "--enable-flutter-gpu=true") != switches.end()) { @@ -756,7 +795,19 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { }; flutterArguments.custom_task_runners = &custom_task_runners; - [self loadAOTData:_project.assetsPath]; + NSString* elfPath; + BOOL configureShorebirdRes = [self configureShorebird:&elfPath]; + if (!configureShorebirdRes) { + // No patch exists, or we failed to configure shorebird. This is a fallback. + // Upstream, this code lives in -(void)loadAOTData:. + // + // This is the location where the test fixture places the snapshot file. + // For applications built by Flutter tool, this is in "App.framework". + elfPath = [NSString pathWithComponents:@[ _project.assetsPath, @"app_elf_snapshot.so" ]]; + } + + [self loadAOTData:elfPath]; + if (_aotData) { flutterArguments.aot_data = _aotData; } @@ -780,6 +831,7 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { }; FlutterRendererConfig rendererConfig = [_renderer createRendererConfig]; + FlutterEngineResult result = _embedderAPI.Initialize( FLUTTER_ENGINE_VERSION, &rendererConfig, &flutterArguments, (__bridge void*)(self), &_engine); if (result != kSuccess) { @@ -809,7 +861,7 @@ - (BOOL)runWithEntrypoint:(NSString*)entrypoint { return YES; } -- (void)loadAOTData:(NSString*)assetsDir { +- (void)loadAOTData:(NSString*)elfPath { if (!_embedderAPI.RunsAOTCompiledDartCode()) { return; } @@ -817,11 +869,8 @@ - (void)loadAOTData:(NSString*)assetsDir { BOOL isDirOut = false; // required for NSFileManager fileExistsAtPath. NSFileManager* fileManager = [NSFileManager defaultManager]; - // This is the location where the test fixture places the snapshot file. - // For applications built by Flutter tool, this is in "App.framework". - NSString* elfPath = [NSString pathWithComponents:@[ assetsDir, @"app_elf_snapshot.so" ]]; - if (![fileManager fileExistsAtPath:elfPath isDirectory:&isDirOut]) { + FML_LOG(INFO) << "in loadAOTData, elfPath does not exist: " << elfPath.UTF8String; return; } diff --git a/engine/src/flutter/shell/platform/linux/BUILD.gn b/engine/src/flutter/shell/platform/linux/BUILD.gn index d64d76019c3e5..979949848c4f7 100644 --- a/engine/src/flutter/shell/platform/linux/BUILD.gn +++ b/engine/src/flutter/shell/platform/linux/BUILD.gn @@ -158,6 +158,7 @@ source_set("flutter_linux_sources") { "fl_settings_channel.cc", "fl_settings_handler.cc", "fl_settings_portal.cc", + "fl_shorebird.cc", "fl_socket_accessible.cc", "fl_standard_message_codec.cc", "fl_standard_method_codec.cc", @@ -187,12 +188,14 @@ source_set("flutter_linux_sources") { deps = [ "$dart_src/runtime:dart_api", "//flutter/fml", + "//flutter/shell/common/shorebird", "//flutter/shell/platform/common:common_cpp_enums", "//flutter/shell/platform/common:common_cpp_input", "//flutter/shell/platform/common:common_cpp_isolate_scope", "//flutter/shell/platform/common:common_cpp_switches", "//flutter/shell/platform/embedder:embedder_headers", "//flutter/third_party/rapidjson", + "//flutter/third_party/tonic", ] } diff --git a/engine/src/flutter/shell/platform/linux/fl_accessible_text_field.cc b/engine/src/flutter/shell/platform/linux/fl_accessible_text_field.cc index 17d0a0d763dc4..ce8ace383a161 100644 --- a/engine/src/flutter/shell/platform/linux/fl_accessible_text_field.cc +++ b/engine/src/flutter/shell/platform/linux/fl_accessible_text_field.cc @@ -39,10 +39,13 @@ static gchar* get_substring(FlAccessibleTextField* self, glong start, glong end) { const gchar* value = gtk_entry_buffer_get_text(self->buffer); + glong length = g_utf8_strlen(value, -1); if (end == -1) { // g_utf8_substring() accepts -1 since 2.72 - end = g_utf8_strlen(value, -1); + end = length; } + start = CLAMP(start, 0, length); + end = CLAMP(end, start, length); return g_utf8_substring(value, start, end); } @@ -76,16 +79,20 @@ static gchar* get_string_at_offset(FlAccessibleTextField* self, const PangoLogAttr* attrs = pango_layout_get_log_attrs_readonly(layout, &n_attrs); + start = CLAMP(start, 0, MAX(n_attrs - 1, 0)); + end = CLAMP(end, 0, MAX(n_attrs - 1, 0)); + while (start > 0 && !is_start(&attrs[start])) { --start; } - if (start_offset != nullptr) { - *start_offset = start; - } while (end < n_attrs && !is_end(&attrs[end])) { ++end; } + + if (start_offset != nullptr) { + *start_offset = start; + } if (end_offset != nullptr) { *end_offset = end; } diff --git a/engine/src/flutter/shell/platform/linux/fl_accessible_text_field_test.cc b/engine/src/flutter/shell/platform/linux/fl_accessible_text_field_test.cc index 90be661eb82c7..2cbdb890609ae 100644 --- a/engine/src/flutter/shell/platform/linux/fl_accessible_text_field_test.cc +++ b/engine/src/flutter/shell/platform/linux/fl_accessible_text_field_test.cc @@ -218,6 +218,33 @@ TEST(FlAccessibleTextFieldTest, GetText) { EXPECT_STREQ(tt, "tt"); } +// Tests AtkText::get_text with out-of-bounds offsets. +TEST(FlAccessibleTextFieldTest, GetTextBoundsChecking) { + g_autoptr(FlDartProject) project = fl_dart_project_new(); + g_autoptr(FlEngine) engine = fl_engine_new(project); + g_autoptr(FlAccessibleNode) node = + fl_accessible_text_field_new(engine, 123, 1); + + fl_accessible_node_set_value(node, "Hello"); + + // start beyond end of text + g_autofree gchar* beyond = atk_text_get_text(ATK_TEXT(node), 100, -1); + EXPECT_STREQ(beyond, ""); + + // end beyond text length + g_autofree gchar* end_beyond = atk_text_get_text(ATK_TEXT(node), 2, 100); + EXPECT_STREQ(end_beyond, "llo"); + + // both beyond text length + g_autofree gchar* both_beyond = atk_text_get_text(ATK_TEXT(node), 50, 100); + EXPECT_STREQ(both_beyond, ""); + + // empty buffer + fl_accessible_node_set_value(node, ""); + g_autofree gchar* empty_beyond = atk_text_get_text(ATK_TEXT(node), 5, 10); + EXPECT_STREQ(empty_beyond, ""); +} + // Tests AtkText::get_caret_offset. TEST(FlAccessibleTextFieldTest, GetCaretOffset) { g_autoptr(FlDartProject) project = fl_dart_project_new(); @@ -696,4 +723,115 @@ TEST(FlAccessibleTextFieldTest, TextBoundary) { EXPECT_EQ(end_offset, 70); } +// Tests that get_string_at_offset handles offset beyond text length. +TEST(FlAccessibleTextFieldTest, TextBoundaryOffsetBeyondEnd) { + g_autoptr(FlDartProject) project = fl_dart_project_new(); + g_autoptr(FlEngine) engine = fl_engine_new(project); + g_autoptr(FlAccessibleNode) node = + fl_accessible_text_field_new(engine, 123, 1); + + fl_accessible_node_set_value(node, "Hello"); + + // Offset well beyond the text length should not crash and should return + // a valid result clamped to the text boundaries. + gint start_offset = -1, end_offset = -1; + g_autofree gchar* char_result = atk_text_get_string_at_offset( + ATK_TEXT(node), 100, ATK_TEXT_GRANULARITY_CHAR, &start_offset, + &end_offset); + EXPECT_NE(char_result, nullptr); + EXPECT_GE(start_offset, 0); + EXPECT_LE(end_offset, 5); + + g_autofree gchar* word_result = atk_text_get_string_at_offset( + ATK_TEXT(node), 100, ATK_TEXT_GRANULARITY_WORD, &start_offset, + &end_offset); + EXPECT_NE(word_result, nullptr); + EXPECT_GE(start_offset, 0); + EXPECT_LE(end_offset, 5); + + g_autofree gchar* sentence_result = atk_text_get_string_at_offset( + ATK_TEXT(node), 100, ATK_TEXT_GRANULARITY_SENTENCE, &start_offset, + &end_offset); + EXPECT_NE(sentence_result, nullptr); + EXPECT_GE(start_offset, 0); + EXPECT_LE(end_offset, 5); +} + +// Tests that get_string_at_offset handles offset at position zero. +TEST(FlAccessibleTextFieldTest, TextBoundaryOffsetAtStart) { + g_autoptr(FlDartProject) project = fl_dart_project_new(); + g_autoptr(FlEngine) engine = fl_engine_new(project); + g_autoptr(FlAccessibleNode) node = + fl_accessible_text_field_new(engine, 123, 1); + + fl_accessible_node_set_value(node, "Hello"); + + // Offset at zero should return the first character/word. + gint start_offset = -1, end_offset = -1; + g_autofree gchar* char_result = atk_text_get_string_at_offset( + ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_CHAR, &start_offset, &end_offset); + EXPECT_NE(char_result, nullptr); + EXPECT_EQ(start_offset, 0); + EXPECT_EQ(end_offset, 1); + EXPECT_STREQ(char_result, "H"); + + g_autofree gchar* word_result = atk_text_get_string_at_offset( + ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_WORD, &start_offset, &end_offset); + EXPECT_NE(word_result, nullptr); + EXPECT_EQ(start_offset, 0); + EXPECT_EQ(end_offset, 5); + EXPECT_STREQ(word_result, "Hello"); +} + +// Tests that get_string_at_offset handles empty text. +TEST(FlAccessibleTextFieldTest, TextBoundaryEmptyText) { + g_autoptr(FlDartProject) project = fl_dart_project_new(); + g_autoptr(FlEngine) engine = fl_engine_new(project); + g_autoptr(FlAccessibleNode) node = + fl_accessible_text_field_new(engine, 123, 1); + + // Empty text - should not crash. + gint start_offset = -1, end_offset = -1; + g_autofree gchar* char_result = atk_text_get_string_at_offset( + ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_CHAR, &start_offset, &end_offset); + EXPECT_NE(char_result, nullptr); + EXPECT_EQ(start_offset, 0); + EXPECT_STREQ(char_result, ""); + + g_autofree gchar* word_result = atk_text_get_string_at_offset( + ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_WORD, &start_offset, &end_offset); + EXPECT_NE(word_result, nullptr); + EXPECT_EQ(start_offset, 0); + EXPECT_STREQ(word_result, ""); +} + +// Tests that get_string_at_offset handles offset at exact text length. +TEST(FlAccessibleTextFieldTest, TextBoundaryOffsetAtEnd) { + g_autoptr(FlDartProject) project = fl_dart_project_new(); + g_autoptr(FlEngine) engine = fl_engine_new(project); + g_autoptr(FlAccessibleNode) node = + fl_accessible_text_field_new(engine, 123, 1); + + fl_accessible_node_set_value(node, "Hello world"); + + // Offset at exactly the character count (one past last char). + gint char_count = atk_text_get_character_count(ATK_TEXT(node)); + EXPECT_EQ(char_count, 11); + + gint start_offset = -1, end_offset = -1; + g_autofree gchar* char_result = atk_text_get_string_at_offset( + ATK_TEXT(node), char_count, ATK_TEXT_GRANULARITY_CHAR, &start_offset, + &end_offset); + EXPECT_NE(char_result, nullptr); + EXPECT_GE(start_offset, 0); + EXPECT_LE(end_offset, char_count); + + g_autofree gchar* word_result = atk_text_get_string_at_offset( + ATK_TEXT(node), char_count, ATK_TEXT_GRANULARITY_WORD, &start_offset, + &end_offset); + EXPECT_NE(word_result, nullptr); + EXPECT_GE(start_offset, 0); + EXPECT_LE(end_offset, char_count); +} + // NOLINTEND(clang-analyzer-core.StackAddressEscape) diff --git a/engine/src/flutter/shell/platform/linux/fl_engine.cc b/engine/src/flutter/shell/platform/linux/fl_engine.cc index 6ccdc39d51f83..62c302cdec509 100644 --- a/engine/src/flutter/shell/platform/linux/fl_engine.cc +++ b/engine/src/flutter/shell/platform/linux/fl_engine.cc @@ -10,6 +10,7 @@ #include #include "flutter/common/constants.h" +#include "flutter/fml/logging.h" #include "flutter/shell/platform/common/engine_switches.h" #include "flutter/shell/platform/embedder/embedder.h" #include "flutter/shell/platform/linux/fl_accessibility_handler.h" @@ -24,6 +25,7 @@ #include "flutter/shell/platform/linux/fl_platform_handler.h" #include "flutter/shell/platform/linux/fl_plugin_registrar_private.h" #include "flutter/shell/platform/linux/fl_settings_handler.h" +#include "flutter/shell/platform/linux/fl_shorebird.h" #include "flutter/shell/platform/linux/fl_texture_gl_private.h" #include "flutter/shell/platform/linux/fl_texture_registrar_private.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_plugin_registry.h" @@ -815,6 +817,9 @@ gboolean fl_engine_start(FlEngine* self, GError** error) { g_autoptr(GPtrArray) command_line_args = g_ptr_array_new_with_free_func(g_free); + // FlutterProjectArgs expects a full argv, so when processing it for flags + // the first item is treated as the executable and ignored. Add a dummy + // value so that all switches are used. g_ptr_array_insert(command_line_args, 0, g_strdup("flutter")); for (const auto& env_switch : flutter::GetSwitchesFromEnvironment()) { g_ptr_array_add(command_line_args, g_strdup(env_switch.c_str())); @@ -852,9 +857,22 @@ gboolean fl_engine_start(FlEngine* self, GError** error) { args.compositor = &compositor; if (self->embedder_api.RunsAOTCompiledDartCode()) { + // This struct contains raw C strings and needs to have its lifetime scoped + // to this block. FlutterEngineAOTDataSource source = {}; source.type = kFlutterEngineAOTDataSourceTypeElfPath; - source.elf_path = fl_dart_project_get_aot_library_path(self->project); + std::string patch_path; + auto setup_shorebird_result = + flutter::SetUpShorebird(args.assets_path, patch_path); + if (setup_shorebird_result) { + // If we have a patch installed, we replace the default AOT library path + // with the patch path here. + source.elf_path = patch_path.c_str(); + } else { + FML_LOG(ERROR) << "Failed to configure Shorebird."; + source.elf_path = fl_dart_project_get_aot_library_path(self->project); + } + if (self->embedder_api.CreateAOTData(&source, &self->aot_data) != kSuccess) { g_set_error(error, fl_engine_error_quark(), FL_ENGINE_ERROR_FAILED, diff --git a/engine/src/flutter/shell/platform/linux/fl_shorebird.cc b/engine/src/flutter/shell/platform/linux/fl_shorebird.cc new file mode 100644 index 0000000000000..eb913ad720c61 --- /dev/null +++ b/engine/src/flutter/shell/platform/linux/fl_shorebird.cc @@ -0,0 +1,56 @@ +#include "flutter/shell/platform/linux/fl_shorebird.h" + +#include +#include +#include + +#include "flutter/fml/file.h" +#include "flutter/fml/logging.h" +#include "flutter/fml/paths.h" +#include "flutter/shell/common/shorebird/shorebird.h" +#include "rapidjson/document.h" +#include "third_party/tonic/filesystem/filesystem/file.h" + +// Namespaced to avoid Google style warnings. +namespace flutter { + +gboolean SetUpShorebird(const char* assets_path, std::string& patch_path) { + auto shorebird_yaml_path = + fml::paths::JoinPaths({assets_path, "shorebird.yaml"}); + std::string shorebird_yaml_contents(""); + if (!filesystem::ReadFileToString(shorebird_yaml_path, + &shorebird_yaml_contents)) { + FML_LOG(ERROR) << "Failed to read shorebird.yaml."; + return false; + } + + std::string code_cache_path = + fml::paths::JoinPaths({g_get_home_dir(), ".shorebird_cache"}); + auto executable_location = fml::paths::GetExecutableDirectoryPath().second; + auto app_path = + fml::paths::JoinPaths({executable_location, "lib", "libapp.so"}); + auto version_json_path = fml::paths::JoinPaths({assets_path, "version.json"}); + std::ifstream input(version_json_path); + if (!input) { + return false; + } + std::string json_contents{std::istreambuf_iterator(input), + std::istreambuf_iterator()}; + + rapidjson::Document json_doc; + json_doc.Parse(json_contents.c_str()); + if (json_doc.HasParseError()) { + // Could not parse version file, aborting. + return false; + } + + const auto version_map = json_doc.GetObject(); + ReleaseVersion release_version{version_map["version"].GetString(), + version_map["build_number"].GetString()}; + + ShorebirdConfigArgs shorebird_args(code_cache_path, code_cache_path, app_path, + shorebird_yaml_contents, release_version); + return ConfigureShorebird(shorebird_args, patch_path); +} + +} // namespace flutter diff --git a/engine/src/flutter/shell/platform/linux/fl_shorebird.h b/engine/src/flutter/shell/platform/linux/fl_shorebird.h new file mode 100644 index 0000000000000..e38965776ac97 --- /dev/null +++ b/engine/src/flutter/shell/platform/linux/fl_shorebird.h @@ -0,0 +1,13 @@ +#ifndef FLUTTER_SHELL_PLATFORM_LINUX_FL_SHOREBIRD_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_SHOREBIRD_H_ + +#include +#include + +namespace flutter { + +gboolean SetUpShorebird(const char* assets_path, std::string& patch_path); + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_SHOREBIRD_H_ diff --git a/engine/src/flutter/shell/platform/windows/BUILD.gn b/engine/src/flutter/shell/platform/windows/BUILD.gn index 09b52a96fb0b1..55318930541b3 100644 --- a/engine/src/flutter/shell/platform/windows/BUILD.gn +++ b/engine/src/flutter/shell/platform/windows/BUILD.gn @@ -174,6 +174,7 @@ source_set("flutter_windows_source") { ":flutter_windows_headers", "//flutter/fml", "//flutter/impeller/renderer/backend/gles", + "//flutter/shell/common/shorebird", "//flutter/shell/geometry", "//flutter/shell/platform/common:common_cpp", "//flutter/shell/platform/common:common_cpp_input", @@ -189,6 +190,7 @@ source_set("flutter_windows_source") { "//flutter/third_party/angle:libEGL_static", "//flutter/third_party/angle:libGLESv2_static", "//flutter/third_party/rapidjson", + "//flutter/third_party/tonic", ] } @@ -200,6 +202,11 @@ copy("publish_headers_windows") { deps = [ "//flutter/shell/platform/common:publish_headers" ] } +copy("updater_exports_windows") { + sources = [ "flutter_windows.dll.def" ] + outputs = [ "$root_out_dir/{{source_file_part}}" ] +} + shared_library("flutter_windows") { deps = [ ":flutter_windows_source" ] @@ -315,6 +322,7 @@ group("windows") { deps = [ ":flutter_windows", ":publish_headers_windows", + ":updater_exports_windows", "//flutter/shell/platform/windows/client_wrapper:publish_wrapper_windows", ] diff --git a/engine/src/flutter/shell/platform/windows/client_wrapper/flutter_view_unittests.cc b/engine/src/flutter/shell/platform/windows/client_wrapper/flutter_view_unittests.cc index 95489fc6e76dc..450654be657e2 100644 --- a/engine/src/flutter/shell/platform/windows/client_wrapper/flutter_view_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/client_wrapper/flutter_view_unittests.cc @@ -21,8 +21,9 @@ class TestWindowsApi : public testing::StubFlutterWindowsApi { return reinterpret_cast(8); } - IDXGIAdapter* EngineGetGraphicsAdapter() override { - return reinterpret_cast(9); + bool EngineGetGraphicsAdapter(IDXGIAdapter** adapter_out) override { + *adapter_out = reinterpret_cast(9); + return true; } }; diff --git a/engine/src/flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h b/engine/src/flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h index 0a3701cea9ebb..5cc73b8c7c156 100644 --- a/engine/src/flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h +++ b/engine/src/flutter/shell/platform/windows/client_wrapper/include/flutter/flutter_engine.h @@ -94,9 +94,12 @@ class FlutterEngine : public PluginRegistry { WPARAM wparam, LPARAM lparam); - // Returns the DXGI adapter used for rendering or nullptr in case of error. - IDXGIAdapter* GetGraphicsAdapter() { - return FlutterDesktopEngineGetGraphicsAdapter(engine_); + // Retrieves the DXGI adapter used for rendering. Returns true if the adapter + // was successfully retrieved, or false if an error occured. + // The caller must provide a valid pointer to an IDXGIAdapter* and is + // responsible for releasing the adapter. + bool GetGraphicsAdapter(IDXGIAdapter** adapter_out) { + return FlutterDesktopEngineGetGraphicsAdapter(engine_, adapter_out); } private: diff --git a/engine/src/flutter/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc b/engine/src/flutter/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc index e0b3ae03bb512..7f5c2bc722502 100644 --- a/engine/src/flutter/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc +++ b/engine/src/flutter/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.cc @@ -168,12 +168,12 @@ IDXGIAdapter* FlutterDesktopViewGetGraphicsAdapter(FlutterDesktopViewRef view) { return nullptr; } -IDXGIAdapter* FlutterDesktopEngineGetGraphicsAdapter( - FlutterDesktopEngineRef engine) { +bool FlutterDesktopEngineGetGraphicsAdapter(FlutterDesktopEngineRef engine, + IDXGIAdapter** adapter_out) { if (s_stub_implementation) { - return s_stub_implementation->EngineGetGraphicsAdapter(); + return s_stub_implementation->EngineGetGraphicsAdapter(adapter_out); } - return nullptr; + return false; } bool FlutterDesktopEngineProcessExternalWindowMessage( diff --git a/engine/src/flutter/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h b/engine/src/flutter/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h index 834bb86b69cad..21dc0409b95de 100644 --- a/engine/src/flutter/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h +++ b/engine/src/flutter/shell/platform/windows/client_wrapper/testing/stub_flutter_windows_api.h @@ -87,8 +87,9 @@ class StubFlutterWindowsApi { } // Called for FlutterDesktopEngineGetGraphicsAdapter. - virtual IDXGIAdapter* EngineGetGraphicsAdapter() { - return reinterpret_cast(3); + virtual bool EngineGetGraphicsAdapter(IDXGIAdapter** adapter_out) { + *adapter_out = reinterpret_cast(3); + return true; } // Called for FlutterDesktopPluginRegistrarGetView. diff --git a/engine/src/flutter/shell/platform/windows/flutter_project_bundle.h b/engine/src/flutter/shell/platform/windows/flutter_project_bundle.h index ec465208ae8f1..b0417568ac19e 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_project_bundle.h +++ b/engine/src/flutter/shell/platform/windows/flutter_project_bundle.h @@ -62,6 +62,10 @@ class FlutterProjectBundle { // Sets engine switches. void SetSwitches(const std::vector& switches); + void SetAotLibraryPath(const std::filesystem::path& aot_library_path) { + aot_library_path_ = aot_library_path; + } + // Attempts to load AOT data for this bundle. The returned data must be // retained until any engine instance it is passed to has been shut down. // diff --git a/engine/src/flutter/shell/platform/windows/flutter_window.cc b/engine/src/flutter/shell/platform/windows/flutter_window.cc index 06e7577c3d855..6f0d73f0522f9 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_window.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_window.cc @@ -545,10 +545,10 @@ FlutterWindow::HandleMessage(UINT const message, case WM_POINTERUPDATE: case WM_POINTERUP: case WM_POINTERLEAVE: { - x_pos = GET_X_LPARAM(lparam); - y_pos = GET_Y_LPARAM(lparam); - auto const x = static_cast(x_pos); - auto const y = static_cast(y_pos); + POINT pt = {GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)}; + ScreenToClient(window_handle_, &pt); + auto const x = static_cast(pt.x); + auto const y = static_cast(pt.y); auto const pointerId = GET_POINTERID_WPARAM(wparam); POINTER_INFO pointerInfo; if (windows_proc_table_->GetPointerInfo(pointerId, &pointerInfo)) { diff --git a/engine/src/flutter/shell/platform/windows/flutter_window_unittests.cc b/engine/src/flutter/shell/platform/windows/flutter_window_unittests.cc index 350e3d700b3f4..65ef8934ef3f3 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_window_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_window_unittests.cc @@ -83,6 +83,30 @@ class MockFlutterWindow : public FlutterWindow { FML_DISALLOW_COPY_AND_ASSIGN(MockFlutterWindow); }; +LRESULT InjectPointerMessageWithClientPoint(MockFlutterWindow& window, + UINT message, + WPARAM wparam, + int x, + int y) { + HWND flutter_window = window.FlutterWindow::GetWindowHandle(); + HWND parent_window = + CreateWindowEx(0, L"STATIC", L"", WS_POPUP, 100, 100, 300, 300, nullptr, + nullptr, GetModuleHandle(nullptr), nullptr); + EXPECT_NE(parent_window, nullptr); + EXPECT_NE(SetParent(flutter_window, parent_window), nullptr); + EXPECT_TRUE(SetWindowPos(flutter_window, nullptr, 0, 0, 100, 100, + SWP_NOZORDER | SWP_NOACTIVATE)); + + POINT point = {x, y}; + EXPECT_TRUE(ClientToScreen(flutter_window, &point)); + LRESULT result = + window.InjectWindowMessage(message, wparam, MAKELPARAM(point.x, point.y)); + + EXPECT_NE(SetParent(flutter_window, HWND_MESSAGE), nullptr); + DestroyWindow(parent_window); + return result; +} + class MockFlutterWindowsView : public FlutterWindowsView { public: MockFlutterWindowsView(FlutterWindowsEngine* engine, @@ -297,9 +321,8 @@ TEST_F(FlutterWindowTest, OnStylusPointerDown) { UINT32 pointerId = 1; WPARAM wparam = static_cast(pointerId); - LPARAM lparam = MAKELPARAM(10, 10); - - win32window.InjectWindowMessage(WM_POINTERDOWN, wparam, lparam); + InjectPointerMessageWithClientPoint(win32window, WM_POINTERDOWN, wparam, 10, + 10); } TEST_F(FlutterWindowTest, OnStylusPointerMove) { @@ -335,9 +358,8 @@ TEST_F(FlutterWindowTest, OnStylusPointerMove) { UINT32 pointerId = 1; WPARAM wparam = static_cast(pointerId); - LPARAM lparam = MAKELPARAM(15, 20); - - win32window.InjectWindowMessage(WM_POINTERUPDATE, wparam, lparam); + InjectPointerMessageWithClientPoint(win32window, WM_POINTERUPDATE, wparam, 15, + 20); } TEST_F(FlutterWindowTest, OnStylusPointerUp) { @@ -373,9 +395,8 @@ TEST_F(FlutterWindowTest, OnStylusPointerUp) { UINT32 pointerId = 1; WPARAM wparam = static_cast(pointerId); - LPARAM lparam = MAKELPARAM(25, 30); - - win32window.InjectWindowMessage(WM_POINTERUP, wparam, lparam); + InjectPointerMessageWithClientPoint(win32window, WM_POINTERUP, wparam, 25, + 30); } TEST_F(FlutterWindowTest, OnStylusPointerLeave) { @@ -410,9 +431,8 @@ TEST_F(FlutterWindowTest, OnStylusPointerLeave) { UINT32 pointerId = 1; WPARAM wparam = static_cast(pointerId); - LPARAM lparam = MAKELPARAM(35, 40); - - win32window.InjectWindowMessage(WM_POINTERLEAVE, wparam, lparam); + InjectPointerMessageWithClientPoint(win32window, WM_POINTERLEAVE, wparam, 35, + 40); } TEST_F(FlutterWindowTest, OnStylusPointerHover) { @@ -454,8 +474,8 @@ TEST_F(FlutterWindowTest, OnStylusPointerHover) { UINT32 pointerId = 1; WPARAM wparam = static_cast(pointerId); - LPARAM lparam = MAKELPARAM(45, 50); - win32window.InjectWindowMessage(WM_POINTERDOWN, wparam, lparam); + InjectPointerMessageWithClientPoint(win32window, WM_POINTERDOWN, wparam, 45, + 50); // Now expect OnPointerMove to be called for hover events EXPECT_CALL(delegate, OnPointerMove(45, 50, kFlutterPointerDeviceKindStylus, @@ -463,7 +483,8 @@ TEST_F(FlutterWindowTest, OnStylusPointerHover) { .Times(1); // Inject WM_POINTERUPDATE message (hover event) - win32window.InjectWindowMessage(WM_POINTERUPDATE, wparam, lparam); + InjectPointerMessageWithClientPoint(win32window, WM_POINTERUPDATE, wparam, 45, + 50); } TEST_F(FlutterWindowTest, OnMousePointerDown) { @@ -490,9 +511,8 @@ TEST_F(FlutterWindowTest, OnMousePointerDown) { UINT32 pointerId = 1; WPARAM wparam = static_cast(pointerId); - LPARAM lparam = MAKELPARAM(45, 50); - - win32window.InjectWindowMessage(WM_POINTERDOWN, wparam, lparam); + InjectPointerMessageWithClientPoint(win32window, WM_POINTERDOWN, wparam, 45, + 50); } TEST_F(FlutterWindowTest, OnTouchPointerDown) { @@ -519,9 +539,57 @@ TEST_F(FlutterWindowTest, OnTouchPointerDown) { UINT32 pointerId = 1; WPARAM wparam = static_cast(pointerId); - LPARAM lparam = MAKELPARAM(55, 60); + InjectPointerMessageWithClientPoint(win32window, WM_POINTERDOWN, wparam, 55, + 60); +} + +TEST_F(FlutterWindowTest, PointerMessageScreenCoordinatesAreConvertedToClient) { + constexpr int kClientX = 15; + constexpr int kClientY = 20; + + auto mock_proc_table = std::make_shared(); + + EXPECT_CALL(*mock_proc_table, GetPointerInfo(_, _)) + .WillRepeatedly([](UINT32 pointer_id, POINTER_INFO* pointer_info) { + if (pointer_info != nullptr) { + pointer_info->pointerType = PT_TOUCH; + pointer_info->pointerId = pointer_id; + pointer_info->pointerFlags = POINTER_FLAG_INCONTACT; + } + return TRUE; + }); + + MockFlutterWindow win32window(100, 100, mock_proc_table); + MockWindowBindingHandlerDelegate delegate; + EXPECT_CALL(win32window, OnWindowStateEvent).Times(AnyNumber()); + EXPECT_CALL(delegate, OnWindowStateEvent).Times(AnyNumber()); + win32window.SetView(&delegate); + + HWND parent_window = + CreateWindowEx(0, L"STATIC", L"", WS_POPUP, 100, 100, 300, 300, nullptr, + nullptr, GetModuleHandle(nullptr), nullptr); + ASSERT_NE(parent_window, nullptr); + + HWND flutter_window = win32window.FlutterWindow::GetWindowHandle(); + ASSERT_NE(flutter_window, nullptr); + ASSERT_NE(SetParent(flutter_window, parent_window), nullptr); + ASSERT_TRUE(SetWindowPos(flutter_window, nullptr, 25, 35, 100, 100, + SWP_NOZORDER | SWP_NOACTIVATE)); + + POINT pointer_point = {kClientX, kClientY}; + ASSERT_TRUE(ClientToScreen(flutter_window, &pointer_point)); + + EXPECT_CALL(delegate, + OnPointerDown(kClientX, kClientY, kFlutterPointerDeviceKindTouch, + kDefaultPointerDeviceId, + kFlutterPointerButtonMousePrimary, 0, 0)) + .Times(1); + + win32window.InjectWindowMessage(WM_POINTERDOWN, /*wparam=*/1, + MAKELPARAM(pointer_point.x, pointer_point.y)); - win32window.InjectWindowMessage(WM_POINTERDOWN, wparam, lparam); + EXPECT_NE(SetParent(flutter_window, HWND_MESSAGE), nullptr); + DestroyWindow(parent_window); } // Tests that calls to OnScroll in turn calls GetScrollOffsetMultiplier diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows.cc b/engine/src/flutter/shell/platform/windows/flutter_windows.cc index 54576345723b1..2e0a0c3c9cfa7 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows.cc @@ -256,24 +256,28 @@ HWND FlutterDesktopViewGetHWND(FlutterDesktopViewRef view) { IDXGIAdapter* FlutterDesktopViewGetGraphicsAdapter(FlutterDesktopViewRef view) { auto engine = ViewFromHandle(view)->GetEngine(); - return FlutterDesktopEngineGetGraphicsAdapter(HandleForEngine(engine)); + IDXGIAdapter* adapter; + if (!FlutterDesktopEngineGetGraphicsAdapter(HandleForEngine(engine), + &adapter)) { + return nullptr; + } + return adapter; } -IDXGIAdapter* FlutterDesktopEngineGetGraphicsAdapter( - FlutterDesktopEngineRef engine) { +bool FlutterDesktopEngineGetGraphicsAdapter(FlutterDesktopEngineRef engine, + IDXGIAdapter** adapter_out) { auto egl_manager = EngineFromHandle(engine)->egl_manager(); if (egl_manager) { Microsoft::WRL::ComPtr d3d_device; Microsoft::WRL::ComPtr dxgi_device; if (egl_manager->GetDevice(d3d_device.GetAddressOf()) && SUCCEEDED(d3d_device.As(&dxgi_device))) { - IDXGIAdapter* adapter; - if (SUCCEEDED(dxgi_device->GetAdapter(&adapter))) { - return adapter; + if (SUCCEEDED(dxgi_device->GetAdapter(adapter_out))) { + return true; } } } - return nullptr; + return false; } bool FlutterDesktopEngineProcessExternalWindowMessage( diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows.dll.def b/engine/src/flutter/shell/platform/windows/flutter_windows.dll.def new file mode 100644 index 0000000000000..c1325428fa1d8 --- /dev/null +++ b/engine/src/flutter/shell/platform/windows/flutter_windows.dll.def @@ -0,0 +1,15 @@ +EXPORTS + shorebird_check_for_downloadable_update = shorebird_check_for_downloadable_update + shorebird_current_boot_patch_number = shorebird_current_boot_patch_number + shorebird_free_string = shorebird_free_string + shorebird_free_update_result = shorebird_free_update_result + shorebird_init = shorebird_init + shorebird_next_boot_patch_number = shorebird_next_boot_patch_number + shorebird_next_boot_patch_path = shorebird_next_boot_patch_path + shorebird_validate_next_boot_patch = shorebird_validate_next_boot_patch + shorebird_report_launch_failure = shorebird_report_launch_failure + shorebird_report_launch_start = shorebird_report_launch_start + shorebird_report_launch_success = shorebird_report_launch_success + shorebird_should_auto_update = shorebird_should_auto_update + shorebird_start_update_thread = shorebird_start_update_thread + shorebird_update_with_result = shorebird_update_with_result \ No newline at end of file diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc index 785a83d642da6..5dfaabcc3078b 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc @@ -5,15 +5,20 @@ #include "flutter/shell/platform/windows/flutter_windows_engine.h" #include +#include +#include +#include #include #include #include +#include #include "flutter/fml/logging.h" #include "flutter/fml/paths.h" #include "flutter/fml/platform/win/wstring_conversion.h" #include "flutter/fml/synchronization/waitable_event.h" +#include "flutter/shell/common/shorebird/shorebird.h" #include "flutter/shell/platform/common/client_wrapper/binary_messenger_impl.h" #include "flutter/shell/platform/common/client_wrapper/include/flutter/standard_message_codec.h" #include "flutter/shell/platform/common/path_utils.h" @@ -29,6 +34,7 @@ #include "flutter/shell/platform/windows/window_manager.h" #include "flutter/third_party/accessibility/ax/ax_node.h" #include "shell/platform/windows/flutter_project_bundle.h" +#include "third_party/tonic/filesystem/filesystem/file.h" // winbase.h defines GetCurrentTime as a macro. #undef GetCurrentTime @@ -269,6 +275,125 @@ bool FlutterWindowsEngine::Run() { return Run(""); } +int GetReleaseVersionAndBuildNumber(ReleaseVersion* release_version) { + char module_path[MAX_PATH]; + // Get the full path of the currently running executable. The return value is + // the size of the string that was copied to the buffer, with -1 indicating + // failure. + if (GetModuleFileNameA(NULL, module_path, MAX_PATH) == -1) { + return -1; + } + + // Get the size of the version information + DWORD handle = -1; + DWORD version_info_size = GetFileVersionInfoSizeA(module_path, &handle); + if (version_info_size == -1) { + return -1; + } + + // Allocate memory for version info + std::unique_ptr version_data(new char[version_info_size]); + if (!GetFileVersionInfoA(module_path, handle, version_info_size, + version_data.get())) { + return -1; + } + + // Adopted from + // https://learn.microsoft.com/en-us/windows/win32/api/winver/nf-winver-verqueryvaluea + // Get the translation table + struct LANGANDCODEPAGE { + WORD wLanguage; + WORD wCodePage; + }* lpTranslate; + + UINT cbTranslate = 0; + if (!VerQueryValueA(version_data.get(), "\\VarFileInfo\\Translation", + (LPVOID*)&lpTranslate, &cbTranslate)) { + FML_LOG(ERROR) << "Error: Unable to get translation info."; + return -1; + } + + // Construct the query string using the first translation found + char subBlock[64]; + sprintf_s(subBlock, "\\StringFileInfo\\%04x%04x\\ProductVersion", + lpTranslate[0].wLanguage, lpTranslate[0].wCodePage); + + LPSTR versionString = nullptr; + UINT size = 0; + if (!VerQueryValueA(version_data.get(), subBlock, (LPVOID*)&versionString, + &size)) { + return -1; + } + + if (!versionString) { + return -1; + } + + // The version string is in the format of "1.0.0+1", with the label ("+1") + // being optional. + auto version = std::string(versionString); + auto plusPos = version.find("+"); + if (plusPos != std::string::npos) { + auto semVer = version.substr(0, plusPos); + auto patch = version.substr(plusPos + 1, version.length()); + release_version->version = semVer; + release_version->build_number = patch; + } else { + release_version->version = version; + } + + return kSuccess; +} + +bool GetLocalAppDataPath(std::string& outPath) { + PWSTR path = nullptr; + HRESULT result = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, NULL, &path); + if (!SUCCEEDED(result)) { + return false; + } + + std::wstring widePath(path); + std::string localAppDataPath(widePath.begin(), widePath.end()); + // The calling process is responsible for freeing this resource + // https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath + CoTaskMemFree(path); + outPath = localAppDataPath; + return true; +} + +bool SetUpShorebird(std::string assets_path_string, std::string& patch_path) { + auto shorebird_yaml_path = + fml::paths::JoinPaths({assets_path_string, "shorebird.yaml"}); + std::string shorebird_yaml_contents(""); + if (!filesystem::ReadFileToString(shorebird_yaml_path, + &shorebird_yaml_contents)) { + FML_LOG(ERROR) << "Failed to read shorebird.yaml."; + return false; + } + + std::string code_cache_path; + if (!GetLocalAppDataPath(code_cache_path)) { + FML_LOG(ERROR) << "Failed to retrieve the local AppData directory."; + return false; + } + + auto executable_location = fml::paths::GetExecutableDirectoryPath().second; + auto app_path = + fml::paths::JoinPaths({executable_location, "data", "app.so"}); + ReleaseVersion release_version; + auto release_version_result = + GetReleaseVersionAndBuildNumber(&release_version); + if (release_version_result != kSuccess) { + FML_LOG(ERROR) + << "Failed to retrieve the release version and build number."; + return false; + } + + ShorebirdConfigArgs shorebird_args(code_cache_path, code_cache_path, app_path, + shorebird_yaml_contents, release_version); + return ConfigureShorebird(shorebird_args, patch_path); +} + bool FlutterWindowsEngine::Run(std::string_view entrypoint) { if (!project_->HasValidPaths()) { FML_LOG(ERROR) << "Missing or unresolvable paths to assets."; @@ -276,6 +401,19 @@ bool FlutterWindowsEngine::Run(std::string_view entrypoint) { } std::string assets_path_string = fml::PathToUtf8(project_->assets_path()); std::string icu_path_string = fml::PathToUtf8(project_->icu_path()); + + std::string patch_path; + auto setup_shorebird_result = SetUpShorebird(assets_path_string, patch_path); + if (setup_shorebird_result) { + // If we have a patch installed, we replace the default AOT library path + // with the patch path here. + FML_LOG(INFO) << "Setting project patch path: " << patch_path; + project_->SetAotLibraryPath(patch_path); + } else { + FML_LOG(ERROR) << "Failed to configure Shorebird."; + } + + // This loads AOT data from the project_'s aot_library_path_. if (embedder_api_.RunsAOTCompiledDartCode()) { aot_data_ = project_->LoadAotData(embedder_api_); if (!aot_data_) { @@ -406,6 +544,15 @@ bool FlutterWindowsEngine::Run(std::string_view entrypoint) { host->root_isolate_create_callback_(); } }; + // Copied from shell\platform\darwin\macos\framework\Source\FlutterEngine.mm + // Writes log messages to stdout. + args.log_message_callback = [](const char* tag, const char* message, + void* user_data) { + if (tag && tag[0]) { + std::cout << tag << ": "; + } + std::cout << message << std::endl; + }; args.channel_update_callback = [](const FlutterChannelUpdate* update, void* user_data) { auto host = static_cast(user_data); diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_unittests.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_unittests.cc index 8e22db94c3657..9ed9b6bf66155 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_unittests.cc @@ -439,8 +439,9 @@ TEST_F(WindowsTest, GetEngineGraphicsAdapter) { ASSERT_NE(controller, nullptr); auto engine = FlutterDesktopViewControllerGetEngine(controller.get()); - Microsoft::WRL::ComPtr dxgi_adapter; - dxgi_adapter = FlutterDesktopEngineGetGraphicsAdapter(engine); + // Can't use smart pointer because the result is not a real COM object. + IDXGIAdapter* dxgi_adapter; + ASSERT_TRUE(FlutterDesktopEngineGetGraphicsAdapter(engine, &dxgi_adapter)); ASSERT_NE(dxgi_adapter, nullptr); DXGI_ADAPTER_DESC desc{}; ASSERT_TRUE(SUCCEEDED(dxgi_adapter->GetDesc(&desc))); @@ -504,8 +505,9 @@ TEST_F(WindowsTest, GetEngineGraphicsAdapterWithLowPowerPreference) { ASSERT_NE(controller, nullptr); auto engine = FlutterDesktopViewControllerGetEngine(controller.get()); - Microsoft::WRL::ComPtr dxgi_adapter; - dxgi_adapter = FlutterDesktopEngineGetGraphicsAdapter(engine); + // Can't use smart pointer because the result is not a real COM object. + IDXGIAdapter* dxgi_adapter; + ASSERT_TRUE(FlutterDesktopEngineGetGraphicsAdapter(engine, &dxgi_adapter)); ASSERT_NE(dxgi_adapter, nullptr); DXGI_ADAPTER_DESC desc{}; ASSERT_TRUE(SUCCEEDED(dxgi_adapter->GetDesc(&desc))); @@ -527,8 +529,9 @@ TEST_F(WindowsTest, GetEngineGraphicsAdapterWithHighPerformancePreference) { ASSERT_NE(controller, nullptr); auto engine = FlutterDesktopViewControllerGetEngine(controller.get()); - Microsoft::WRL::ComPtr dxgi_adapter; - dxgi_adapter = FlutterDesktopEngineGetGraphicsAdapter(engine); + // Can't use smart pointer because the result is not a real COM object. + IDXGIAdapter* dxgi_adapter; + ASSERT_TRUE(FlutterDesktopEngineGetGraphicsAdapter(engine, &dxgi_adapter)); ASSERT_NE(dxgi_adapter, nullptr); DXGI_ADAPTER_DESC desc{}; ASSERT_TRUE(SUCCEEDED(dxgi_adapter->GetDesc(&desc))); diff --git a/engine/src/flutter/shell/platform/windows/public/flutter_windows.h b/engine/src/flutter/shell/platform/windows/public/flutter_windows.h index 02a73521722a3..527566b4ec5b5 100644 --- a/engine/src/flutter/shell/platform/windows/public/flutter_windows.h +++ b/engine/src/flutter/shell/platform/windows/public/flutter_windows.h @@ -261,13 +261,18 @@ FLUTTER_EXPORT void FlutterDesktopEngineSetNextFrameCallback( FLUTTER_EXPORT HWND FlutterDesktopViewGetHWND(FlutterDesktopViewRef view); // Returns the DXGI adapter used for rendering or nullptr in case of error. +// The caller must release the adapter. // DEPRECATED: Use |FlutterDesktopEngineGetGraphicsAdapter| instead. FLUTTER_EXPORT IDXGIAdapter* FlutterDesktopViewGetGraphicsAdapter( FlutterDesktopViewRef view); -// Returns the DXGI adapter used for rendering or nullptr in case of error. -FLUTTER_EXPORT IDXGIAdapter* FlutterDesktopEngineGetGraphicsAdapter( - FlutterDesktopEngineRef engine); +// Retrieves the DXGI adapter used for rendering. Returns true if the adapter +// was successfully retrieved, or false if an error occured. +// The caller must provide a valid pointer to an IDXGIAdapter* and is +// responsible for releasing the adapter. +FLUTTER_EXPORT bool FlutterDesktopEngineGetGraphicsAdapter( + FlutterDesktopEngineRef engine, + IDXGIAdapter** adapter_out); // Called to pass an external window message to the engine for lifecycle // state updates. Non-Flutter windows must call this method in their WndProc diff --git a/engine/src/flutter/sky/tools/create_ios_framework.py b/engine/src/flutter/sky/tools/create_ios_framework.py index d87e0f91bff49..0ead267e9609e 100644 --- a/engine/src/flutter/sky/tools/create_ios_framework.py +++ b/engine/src/flutter/sky/tools/create_ios_framework.py @@ -19,7 +19,8 @@ def main(): parser = argparse.ArgumentParser( description=( 'Creates Flutter.framework, Flutter.xcframework and ' - 'copies architecture-dependent gen_snapshot binaries to output dir' + 'copies architecture-dependent analyze_snapshot and gen_snapshot ' + 'binaries to output dir' ) ) @@ -89,13 +90,17 @@ def main(): '%s_extension_safe' % simulator_x64_out_dir, '%s_extension_safe' % simulator_arm64_out_dir ) - # Copy gen_snapshot binary to destination directory. + # Copy analyze_snapshot and gen_snapshot binaries to destination directory. if arm64_out_dir: gen_snapshot = os.path.join(arm64_out_dir, 'universal', 'gen_snapshot_arm64') + analyze_snapshot = os.path.join(arm64_out_dir, 'analyze_snapshot_arm64') sky_utils.copy_binary(gen_snapshot, os.path.join(dst, 'gen_snapshot_arm64')) + sky_utils.copy_binary(analyze_snapshot, os.path.join(dst, 'analyze_snapshot_arm64')) if x64_out_dir: gen_snapshot = os.path.join(x64_out_dir, 'universal', 'gen_snapshot_x64') + analyze_snapshot = os.path.join(x64_out_dir, 'analyze_snapshot_x64') sky_utils.copy_binary(gen_snapshot, os.path.join(dst, 'gen_snapshot_x64')) + sky_utils.copy_binary(analyze_snapshot, os.path.join(dst, 'analyze_snapshot_x64')) zip_archive(dst, args) return 0 @@ -177,7 +182,7 @@ def zip_archive(dst, args): # See: https://github.com/flutter/flutter/blob/62382c7b83a16b3f48dc06c19a47f6b8667005a5/dev/bots/suite_runners/run_verify_binaries_codesigned_tests.dart#L82-L130 # Binaries that must be codesigned and require entitlements for particular APIs. - with_entitlements = ['gen_snapshot_arm64'] + with_entitlements = ['analyze_snapshot_arm64', 'gen_snapshot_arm64'] with_entitlements_file = os.path.join(dst, 'entitlements.txt') sky_utils.write_codesign_config(with_entitlements_file, with_entitlements) @@ -211,6 +216,7 @@ def zip_archive(dst, args): # pylint: enable=line-too-long zip_contents = [ + 'analyze_snapshot_arm64', 'gen_snapshot_arm64', 'Flutter.xcframework', 'entitlements.txt', diff --git a/engine/src/flutter/testing/ios_scenario_app/bin/run_ios_tests.dart b/engine/src/flutter/testing/ios_scenario_app/bin/run_ios_tests.dart index 65f0ec4c6a9f1..8b0309385990e 100644 --- a/engine/src/flutter/testing/ios_scenario_app/bin/run_ios_tests.dart +++ b/engine/src/flutter/testing/ios_scenario_app/bin/run_ios_tests.dart @@ -186,12 +186,12 @@ final _args = ArgParser() ..addOption( 'os-runtime', help: 'The OS runtime of the iOS simulator device to use.', - defaultsTo: 'com.apple.CoreSimulator.SimRuntime.iOS-18-2', + defaultsTo: 'com.apple.CoreSimulator.SimRuntime.iOS-26-2', ) ..addOption( 'os-version', help: 'The OS version of the iOS simulator device to use.', - defaultsTo: '18.2', + defaultsTo: '26.2', ) ..addOption( 'dump-xcresult-on-failure', diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj index 550e48546fa59..f46a8bb7920ad 100644 --- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj +++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj @@ -31,8 +31,6 @@ 248D76EF22E388380012F0C1 /* PlatformViewUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 248D76EE22E388380012F0C1 /* PlatformViewUITests.m */; }; 248FDFC422FE7CD0009CC7CD /* FlutterEngineTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 248FDFC322FE7CD0009CC7CD /* FlutterEngineTest.m */; }; 24F1FB89230B4579005ACE7C /* TextPlatformView.m in Sources */ = {isa = PBXBuildFile; fileRef = 24F1FB87230B4579005ACE7C /* TextPlatformView.m */; }; - 3BFD97202A990CF50094F51B /* golden_bogus_font_text_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 3BFD971E2A990CF40094F51B /* golden_bogus_font_text_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - 3BFD97212A990CF50094F51B /* golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 3BFD971F2A990CF40094F51B /* golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; 4F06F1B32473296E000AF246 /* LocalizationInitializationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F06F1B124731F66000AF246 /* LocalizationInitializationTest.m */; }; 6402EBD124147BDA00987DCB /* UnobstructedPlatformViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6402EBD024147BDA00987DCB /* UnobstructedPlatformViewTests.m */; }; 6816DB9E231750ED00A51400 /* GoldenPlatformViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6816DB9D231750ED00A51400 /* GoldenPlatformViewTests.m */; }; @@ -48,41 +46,43 @@ 68C9D8012AD9B0EF00DF9D79 /* DarwinSystemFontTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68C9D8002AD9B0EF00DF9D79 /* DarwinSystemFontTests.m */; }; 68D4017D2564859300ECD91A /* ContinuousTexture.m in Sources */ = {isa = PBXBuildFile; fileRef = 68D4017C2564859300ECD91A /* ContinuousTexture.m */; }; F26F15B8268B6B5600EC54D3 /* iPadGestureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F26F15B7268B6B5500EC54D3 /* iPadGestureTests.m */; }; - F76CA8C92C8BB0E6002A7EF5 /* golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8C62C8BB0E6002A7EF5 /* golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8CA2C8BB0E6002A7EF5 /* golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8C82C8BB0E6002A7EF5 /* golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8CB2C8BB0E6002A7EF5 /* golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8C52C8BB0E6002A7EF5 /* golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8CC2C8BB0E6002A7EF5 /* golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8C22C8BB0E6002A7EF5 /* golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8CD2C8BB0E6002A7EF5 /* golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8C02C8BB0E6002A7EF5 /* golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8CE2C8BB0E6002A7EF5 /* golden_platform_view_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8C42C8BB0E6002A7EF5 /* golden_platform_view_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8CF2C8BB0E6002A7EF5 /* golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8C32C8BB0E6002A7EF5 /* golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8D02C8BB0E6002A7EF5 /* golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8BF2C8BB0E6002A7EF5 /* golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8D12C8BB0E6002A7EF5 /* golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8C12C8BB0E6002A7EF5 /* golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8D22C8BB0E6002A7EF5 /* golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8C72C8BB0E6002A7EF5 /* golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8E02C8BB574002A7EF5 /* golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8D92C8BB574002A7EF5 /* golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8E12C8BB574002A7EF5 /* golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8DF2C8BB574002A7EF5 /* golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8E22C8BB574002A7EF5 /* golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8DB2C8BB574002A7EF5 /* golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8E32C8BB574002A7EF5 /* golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8D82C8BB574002A7EF5 /* golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8E42C8BB574002A7EF5 /* golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8DD2C8BB574002A7EF5 /* golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8E52C8BB574002A7EF5 /* golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8D62C8BB574002A7EF5 /* golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8E62C8BB574002A7EF5 /* golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8D72C8BB574002A7EF5 /* golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8E72C8BB574002A7EF5 /* golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8DC2C8BB574002A7EF5 /* golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8E82C8BB574002A7EF5 /* golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8DE2C8BB574002A7EF5 /* golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8E92C8BB574002A7EF5 /* golden_darwin_system_font_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8D52C8BB574002A7EF5 /* golden_darwin_system_font_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8EA2C8BB574002A7EF5 /* golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8DA2C8BB574002A7EF5 /* golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8ED2C8BB5E7002A7EF5 /* golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8EB2C8BB5E7002A7EF5 /* golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8F02C8BB623002A7EF5 /* golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8EF2C8BB623002A7EF5 /* golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8F62C8BB73E002A7EF5 /* golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8F52C8BB73E002A7EF5 /* golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8F82C8BB73E002A7EF5 /* golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8F12C8BB73E002A7EF5 /* golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8F92C8BB73E002A7EF5 /* golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8F32C8BB73E002A7EF5 /* golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8FA2C8BB73E002A7EF5 /* golden_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8F42C8BB73E002A7EF5 /* golden_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA8FF2C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8FD2C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA9002C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8FB2C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA9012C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8FC2C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA9022C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA8FE2C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA9042C8BB7FC002A7EF5 /* golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA9032C8BB7FC002A7EF5 /* golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA9092C8BBAD6002A7EF5 /* golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA9062C8BBAD6002A7EF5 /* golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA90A2C8BBAD6002A7EF5 /* golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA9072C8BBAD6002A7EF5 /* golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; - F76CA90B2C8BBAD6002A7EF5 /* golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F76CA9082C8BBAD6002A7EF5 /* golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */; }; + 04EB617CD958B43CC1A80CAE /* golden_bogus_font_text_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 21C15BDB794F61D0706DF179 /* golden_bogus_font_text_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + C3A5F88CC5D39FF4AF7BB0EE /* golden_darwin_system_font_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = AAD032C50FB93A4D46C467D6 /* golden_darwin_system_font_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 8FB292E4E5D5689F5817E8EB /* golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 5BA530B25CC9089C12F2A472 /* golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + F1503890D8006E762FFADC5B /* golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 85DC209DEAFA34849B05F0CF /* golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + CED4285CB2CB52AFEF750401 /* golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 847DC8B5FD365342DD7511A4 /* golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + A96255604D89077EDB44190D /* golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 523225A4D5E8E00FDAE8DB2F /* golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 0B24583753BB258D9C5F3D07 /* golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = A4BB8A1F26131658EAEDCB63 /* golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 9C4FBF02E2C55E598D17AC1A /* golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 51FCA7D3590533EEC7A3D4A0 /* golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + F0B9212D0B7D85187AD6C495 /* golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 26AC1E2A8EA50D93E405B49D /* golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 458A7F4FBC1C057A0CEB212E /* golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = D966254EE8108485EE4E0576 /* golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 8485B0609FCD35387693417B /* golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 5BD7C42F963D99F792F1BD7B /* golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + ACA6436FAE2CC58E937F6474 /* golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 128C0818EE2178D3BA3B0F59 /* golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + A4BAEBB40F17121B1C245691 /* golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = FDCFFA56528B863D5A009BB6 /* golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + AE63A2E21A061413502E8802 /* golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = E94D88641E8692D6CA754665 /* golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + C6A83A6354E040BD4CD665C6 /* golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = A908293A2041E5C40AF4375C /* golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 9910D126B2261FAAB97A44E7 /* golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F2029E1BBD358EE9D69051C6 /* golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 6BE1A09C122A7646184D351E /* golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 4F2C12F613F8121176E6B4E3 /* golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 7E394EACE815CA53336D1206 /* golden_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = B5C0E45EB677CA2966ED4585 /* golden_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + A12704825856BBAC4C987BAA /* golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 9DB5E4AD21B8F78B75DFC9B1 /* golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 1C59B35CA8F3CF2D690BF760 /* golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = EA22E9C7D7457DBEF23F69AA /* golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 9AD2E532A9B00D583711BBF3 /* golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 60BF555B854F01B878DC0F4E /* golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 27CA82E51EB7006E871BC9D9 /* golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = D7E19539C7E4B093765A229F /* golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + B0A2F38E371B2B66E4EC74E6 /* golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 6CE54ED489E05AC7DF73D27A /* golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 49288F6D9BC1E86589E9789E /* golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F5305B6EE42870E78500FDBE /* golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + EEEC9EC0D41BB79C590225ED /* golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 32CBE7032B3ABD72E9A7403A /* golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + A1D6DB6DD6977429A11534F7 /* golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = CC7D8F7F42E606674209ADF9 /* golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + DC42AB4E26AE8B749253D013 /* golden_platform_view_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = F36FE8B64234DBDEFE3ED25E /* golden_platform_view_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 61D7478F383DA9478357C73B /* golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = A51F6151DFE1EA204007B7A4 /* golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 00717ACD5BC8AAA04C7310FB /* golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 86B6AC8C507284FFD9C1A606 /* golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 33F1B733D3392C351A46C1B1 /* golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = A44CEA16419B8C86AF12210F /* golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + B4595BEA1FEC35BE7DBC85D6 /* golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 16D71475DBBD9FE693043611 /* golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 15C743F6B85E3793F73E16D9 /* golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 879E2D5C268BE03DFBEB80F4 /* golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + F0B9195FB6E2E6C6F2A477EC /* golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = BF5CF8B8F0F3EAABB94243F7 /* golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + F576912726BFFF5374B18487 /* golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = BA503B6F4C02051BE0326A85 /* golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 94DE46BF384150908C7D32FC /* golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = D9A554DA82C928C28FF673B2 /* golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + 6745D4FA97ECFFA2BC2A8D39 /* golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = DAF82C07F536190772ADF3E9 /* golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; + B443BE5FE87943056AB8A026 /* golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = A57CEE1E3D292248BC753BEC /* golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -211,8 +211,6 @@ 24D47D1E230CA4480069DD5E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 24F1FB87230B4579005ACE7C /* TextPlatformView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TextPlatformView.m; sourceTree = ""; }; 24F1FB88230B4579005ACE7C /* TextPlatformView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TextPlatformView.h; sourceTree = ""; }; - 3BFD971E2A990CF40094F51B /* golden_bogus_font_text_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_bogus_font_text_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - 3BFD971F2A990CF40094F51B /* golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; 4F06F1B124731F66000AF246 /* LocalizationInitializationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LocalizationInitializationTest.m; sourceTree = ""; }; 6402EBD024147BDA00987DCB /* UnobstructedPlatformViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UnobstructedPlatformViewTests.m; sourceTree = ""; }; 6816DB9C231750ED00A51400 /* GoldenPlatformViewTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GoldenPlatformViewTests.h; sourceTree = ""; }; @@ -234,41 +232,43 @@ 68D4017C2564859300ECD91A /* ContinuousTexture.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ContinuousTexture.m; sourceTree = ""; }; F26F15B7268B6B5500EC54D3 /* iPadGestureTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = iPadGestureTests.m; sourceTree = ""; }; F72114B628EF99F500184A2D /* Info_Skia.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info_Skia.plist; sourceTree = ""; }; - F76CA8BF2C8BB0E6002A7EF5 /* golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8C02C8BB0E6002A7EF5 /* golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8C12C8BB0E6002A7EF5 /* golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8C22C8BB0E6002A7EF5 /* golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8C32C8BB0E6002A7EF5 /* golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8C42C8BB0E6002A7EF5 /* golden_platform_view_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8C52C8BB0E6002A7EF5 /* golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8C62C8BB0E6002A7EF5 /* golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8C72C8BB0E6002A7EF5 /* golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8C82C8BB0E6002A7EF5 /* golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8D52C8BB574002A7EF5 /* golden_darwin_system_font_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_darwin_system_font_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8D62C8BB574002A7EF5 /* golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8D72C8BB574002A7EF5 /* golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8D82C8BB574002A7EF5 /* golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8D92C8BB574002A7EF5 /* golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8DA2C8BB574002A7EF5 /* golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8DB2C8BB574002A7EF5 /* golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8DC2C8BB574002A7EF5 /* golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8DD2C8BB574002A7EF5 /* golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8DE2C8BB574002A7EF5 /* golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8DF2C8BB574002A7EF5 /* golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8EB2C8BB5E7002A7EF5 /* golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8EF2C8BB623002A7EF5 /* golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8F12C8BB73E002A7EF5 /* golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8F32C8BB73E002A7EF5 /* golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8F42C8BB73E002A7EF5 /* golden_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8F52C8BB73E002A7EF5 /* golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8FB2C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8FC2C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8FD2C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA8FE2C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA9032C8BB7FC002A7EF5 /* golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA9062C8BBAD6002A7EF5 /* golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA9072C8BBAD6002A7EF5 /* golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; - F76CA9082C8BBAD6002A7EF5 /* golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png"; sourceTree = ""; }; + 21C15BDB794F61D0706DF179 /* golden_bogus_font_text_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_bogus_font_text_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + AAD032C50FB93A4D46C467D6 /* golden_darwin_system_font_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_darwin_system_font_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + 5BA530B25CC9089C12F2A472 /* golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + 85DC209DEAFA34849B05F0CF /* golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + 847DC8B5FD365342DD7511A4 /* golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + 523225A4D5E8E00FDAE8DB2F /* golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + A4BB8A1F26131658EAEDCB63 /* golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + 51FCA7D3590533EEC7A3D4A0 /* golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + 26AC1E2A8EA50D93E405B49D /* golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + D966254EE8108485EE4E0576 /* golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + 5BD7C42F963D99F792F1BD7B /* golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + 128C0818EE2178D3BA3B0F59 /* golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + FDCFFA56528B863D5A009BB6 /* golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + E94D88641E8692D6CA754665 /* golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + A908293A2041E5C40AF4375C /* golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + F2029E1BBD358EE9D69051C6 /* golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + 4F2C12F613F8121176E6B4E3 /* golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + B5C0E45EB677CA2966ED4585 /* golden_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + 9DB5E4AD21B8F78B75DFC9B1 /* golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + EA22E9C7D7457DBEF23F69AA /* golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + 60BF555B854F01B878DC0F4E /* golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + D7E19539C7E4B093765A229F /* golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + 6CE54ED489E05AC7DF73D27A /* golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + F5305B6EE42870E78500FDBE /* golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + 32CBE7032B3ABD72E9A7403A /* golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + CC7D8F7F42E606674209ADF9 /* golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + F36FE8B64234DBDEFE3ED25E /* golden_platform_view_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + A51F6151DFE1EA204007B7A4 /* golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + 86B6AC8C507284FFD9C1A606 /* golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + A44CEA16419B8C86AF12210F /* golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + 16D71475DBBD9FE693043611 /* golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + 879E2D5C268BE03DFBEB80F4 /* golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + BF5CF8B8F0F3EAABB94243F7 /* golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + BA503B6F4C02051BE0326A85 /* golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + D9A554DA82C928C28FF673B2 /* golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + DAF82C07F536190772ADF3E9 /* golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; + A57CEE1E3D292248BC753BEC /* golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -423,43 +423,43 @@ F7B464DC2759D02B00079189 /* Goldens */ = { isa = PBXGroup; children = ( - 3BFD971E2A990CF40094F51B /* golden_bogus_font_text_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8D52C8BB574002A7EF5 /* golden_darwin_system_font_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8EB2C8BB5E7002A7EF5 /* golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8D62C8BB574002A7EF5 /* golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8BF2C8BB0E6002A7EF5 /* golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8D72C8BB574002A7EF5 /* golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8F12C8BB73E002A7EF5 /* golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8EF2C8BB623002A7EF5 /* golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8D82C8BB574002A7EF5 /* golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8F32C8BB73E002A7EF5 /* golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8C02C8BB0E6002A7EF5 /* golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8C12C8BB0E6002A7EF5 /* golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8C22C8BB0E6002A7EF5 /* golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8D92C8BB574002A7EF5 /* golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8DA2C8BB574002A7EF5 /* golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8C32C8BB0E6002A7EF5 /* golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8DB2C8BB574002A7EF5 /* golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8F42C8BB73E002A7EF5 /* golden_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8FB2C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8FC2C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8FD2C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8FE2C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8DC2C8BB574002A7EF5 /* golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA9062C8BBAD6002A7EF5 /* golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8DD2C8BB574002A7EF5 /* golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA9032C8BB7FC002A7EF5 /* golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8C42C8BB0E6002A7EF5 /* golden_platform_view_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8DE2C8BB574002A7EF5 /* golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8C52C8BB0E6002A7EF5 /* golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - 3BFD971F2A990CF40094F51B /* golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8C62C8BB0E6002A7EF5 /* golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8C72C8BB0E6002A7EF5 /* golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA9072C8BBAD6002A7EF5 /* golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA9082C8BBAD6002A7EF5 /* golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8C82C8BB0E6002A7EF5 /* golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8F52C8BB73E002A7EF5 /* golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, - F76CA8DF2C8BB574002A7EF5 /* golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png */, + 21C15BDB794F61D0706DF179 /* golden_bogus_font_text_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + AAD032C50FB93A4D46C467D6 /* golden_darwin_system_font_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + 5BA530B25CC9089C12F2A472 /* golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + 85DC209DEAFA34849B05F0CF /* golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + 847DC8B5FD365342DD7511A4 /* golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + 523225A4D5E8E00FDAE8DB2F /* golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + A4BB8A1F26131658EAEDCB63 /* golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + 51FCA7D3590533EEC7A3D4A0 /* golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + 26AC1E2A8EA50D93E405B49D /* golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + D966254EE8108485EE4E0576 /* golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + 5BD7C42F963D99F792F1BD7B /* golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + 128C0818EE2178D3BA3B0F59 /* golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + FDCFFA56528B863D5A009BB6 /* golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + E94D88641E8692D6CA754665 /* golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + A908293A2041E5C40AF4375C /* golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + F2029E1BBD358EE9D69051C6 /* golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + 4F2C12F613F8121176E6B4E3 /* golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + B5C0E45EB677CA2966ED4585 /* golden_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + 9DB5E4AD21B8F78B75DFC9B1 /* golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + EA22E9C7D7457DBEF23F69AA /* golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + 60BF555B854F01B878DC0F4E /* golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + D7E19539C7E4B093765A229F /* golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + 6CE54ED489E05AC7DF73D27A /* golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + F5305B6EE42870E78500FDBE /* golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + 32CBE7032B3ABD72E9A7403A /* golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + CC7D8F7F42E606674209ADF9 /* golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + F36FE8B64234DBDEFE3ED25E /* golden_platform_view_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + A51F6151DFE1EA204007B7A4 /* golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + 86B6AC8C507284FFD9C1A606 /* golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + A44CEA16419B8C86AF12210F /* golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + 16D71475DBBD9FE693043611 /* golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + 879E2D5C268BE03DFBEB80F4 /* golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + BF5CF8B8F0F3EAABB94243F7 /* golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + BA503B6F4C02051BE0326A85 /* golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + D9A554DA82C928C28FF673B2 /* golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + DAF82C07F536190772ADF3E9 /* golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, + A57CEE1E3D292248BC753BEC /* golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png */, ); name = Goldens; sourceTree = ""; @@ -629,43 +629,43 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - F76CA8ED2C8BB5E7002A7EF5 /* golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8FF2C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA9002C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA9012C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA9022C8BB7BF002A7EF5 /* golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8E02C8BB574002A7EF5 /* golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8E12C8BB574002A7EF5 /* golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8E22C8BB574002A7EF5 /* golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8E32C8BB574002A7EF5 /* golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8E42C8BB574002A7EF5 /* golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8E52C8BB574002A7EF5 /* golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8E62C8BB574002A7EF5 /* golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8E72C8BB574002A7EF5 /* golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8E82C8BB574002A7EF5 /* golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8E92C8BB574002A7EF5 /* golden_darwin_system_font_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8EA2C8BB574002A7EF5 /* golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - 3BFD97212A990CF50094F51B /* golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8F62C8BB73E002A7EF5 /* golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8F82C8BB73E002A7EF5 /* golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8F92C8BB73E002A7EF5 /* golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA9092C8BBAD6002A7EF5 /* golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA90A2C8BBAD6002A7EF5 /* golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA90B2C8BBAD6002A7EF5 /* golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8FA2C8BB73E002A7EF5 /* golden_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - 3BFD97202A990CF50094F51B /* golden_bogus_font_text_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8C92C8BB0E6002A7EF5 /* golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8CA2C8BB0E6002A7EF5 /* golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8CB2C8BB0E6002A7EF5 /* golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8CC2C8BB0E6002A7EF5 /* golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8CD2C8BB0E6002A7EF5 /* golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8CE2C8BB0E6002A7EF5 /* golden_platform_view_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8CF2C8BB0E6002A7EF5 /* golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8D02C8BB0E6002A7EF5 /* golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8F02C8BB623002A7EF5 /* golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8D12C8BB0E6002A7EF5 /* golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA8D22C8BB0E6002A7EF5 /* golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, - F76CA9042C8BB7FC002A7EF5 /* golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_18.2_simulator.png in Resources */, + 04EB617CD958B43CC1A80CAE /* golden_bogus_font_text_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + C3A5F88CC5D39FF4AF7BB0EE /* golden_darwin_system_font_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 8FB292E4E5D5689F5817E8EB /* golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + F1503890D8006E762FFADC5B /* golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + CED4285CB2CB52AFEF750401 /* golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + A96255604D89077EDB44190D /* golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 0B24583753BB258D9C5F3D07 /* golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 9C4FBF02E2C55E598D17AC1A /* golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + F0B9212D0B7D85187AD6C495 /* golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 458A7F4FBC1C057A0CEB212E /* golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 8485B0609FCD35387693417B /* golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + ACA6436FAE2CC58E937F6474 /* golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + A4BAEBB40F17121B1C245691 /* golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + AE63A2E21A061413502E8802 /* golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + C6A83A6354E040BD4CD665C6 /* golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 9910D126B2261FAAB97A44E7 /* golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 6BE1A09C122A7646184D351E /* golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 7E394EACE815CA53336D1206 /* golden_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + A12704825856BBAC4C987BAA /* golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 1C59B35CA8F3CF2D690BF760 /* golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 9AD2E532A9B00D583711BBF3 /* golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 27CA82E51EB7006E871BC9D9 /* golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + B0A2F38E371B2B66E4EC74E6 /* golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 49288F6D9BC1E86589E9789E /* golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + EEEC9EC0D41BB79C590225ED /* golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + A1D6DB6DD6977429A11534F7 /* golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + DC42AB4E26AE8B749253D013 /* golden_platform_view_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 61D7478F383DA9478357C73B /* golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 00717ACD5BC8AAA04C7310FB /* golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 33F1B733D3392C351A46C1B1 /* golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + B4595BEA1FEC35BE7DBC85D6 /* golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 15C743F6B85E3793F73E16D9 /* golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + F0B9195FB6E2E6C6F2A477EC /* golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + F576912726BFFF5374B18487 /* golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 94DE46BF384150908C7D32FC /* golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + 6745D4FA97ECFFA2BC2A8D39 /* golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, + B443BE5FE87943056AB8A026 /* golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_bogus_font_text_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_bogus_font_text_impeller_iPhone SE (3rd generation)_26.2_simulator.png similarity index 91% rename from engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_bogus_font_text_impeller_iPhone SE (3rd generation)_18.2_simulator.png rename to engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_bogus_font_text_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 7f97a982ad587..f22c39aff98f9 100644 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_bogus_font_text_impeller_iPhone SE (3rd generation)_18.2_simulator.png and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_bogus_font_text_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_darwin_system_font_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_darwin_system_font_impeller_iPhone SE (3rd generation)_26.2_simulator.png similarity index 95% rename from engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_darwin_system_font_impeller_iPhone SE (3rd generation)_18.2_simulator.png rename to engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_darwin_system_font_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 01c7581449a8f..fb09d3ba8b2ec 100644 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_darwin_system_font_impeller_iPhone SE (3rd generation)_18.2_simulator.png and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_darwin_system_font_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index d644afcb4317b..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..06afb25d0ba8a Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_non_full_screen_flutter_view_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index bdcbb5c512795..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..f83029665cbe9 Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png similarity index 61% rename from engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png rename to engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 3ec88f7e21cee..4d818e2723b98 100644 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png similarity index 52% rename from engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png rename to engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png index d044b402c7946..d9500e26249bb 100644 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png similarity index 63% rename from engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png rename to engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 6bbb6e099afe2..9906864189ea3 100644 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_26.2_simulator.png similarity index 95% rename from engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_18.2_simulator.png rename to engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 3843c1796f34b..58da2084bc794 100644 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_18.2_simulator.png and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_after_moved_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png similarity index 89% rename from engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png rename to engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 2870aa165145c..f386b2857bd67 100644 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_after_moved_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index f547c2b6a6b58..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..91cb23ed8d8b5 Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png similarity index 65% rename from engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png rename to engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png index e4d36e505d267..aa55a6673f813 100644 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index 7f962f73dae54..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..eed929a148f02 Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png similarity index 58% rename from engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png rename to engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 902bf6057954c..7d6b8fb76f0ce 100644 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index de57e9e471c4d..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..b2f684257d271 Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index 7dd2a74b05e53..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..c719d9195a249 Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index 004cfc21a3eb5..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..13b165c7b66e5 Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png similarity index 54% rename from engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png rename to engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png index caeb256b905e9..35cce33971b40 100644 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index c8aac80e2c613..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..405c208e1459d Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index 9974d6fcdcf8b..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..9f087b5a3e902 Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index 09811b0e606aa..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..589f8809598be Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index 54edf447cb1e3..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..1522242a18298 Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index e5998b7cead3a..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..45c2924fabb63 Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_large_cliprrect_with_transform_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index e01415ae5543d..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..cc534cd651e16 Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_multiple_background_foreground_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index 3ce08535410ce..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..96a0f9c42a013 Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_multiple_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_26.2_simulator.png similarity index 96% rename from engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_18.2_simulator.png rename to engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_26.2_simulator.png index d4ce8d8735f1b..bce43c70861cf 100644 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_18.2_simulator.png and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_opacity_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index 705954e874158..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..2e8d70ad54f52 Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_rotate_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index 95f7462d58338..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_transform_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..a7f8436fbd148 Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_transform_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index bc32735baa926..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..3eaf21b3a9786 Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_with_negative_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png similarity index 96% rename from engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png rename to engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 095775e251728..1f67d003d296f 100644 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_26.2_simulator.png similarity index 91% rename from engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_18.2_simulator.png rename to engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 7f97a982ad587..f22c39aff98f9 100644 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_18.2_simulator.png and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_spawn_engine_works_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index 1f8e5a8d41275..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..8c6097578a10c Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png similarity index 95% rename from engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png rename to engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 5d7306e5f896c..6971df4a6fc95 100644 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_path_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_26.2_simulator.png similarity index 52% rename from engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_18.2_simulator.png rename to engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_26.2_simulator.png index 7ae00de58a86a..e194008e92f22 100644 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_18.2_simulator.png and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png similarity index 82% rename from engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png rename to engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png index e6329aaade07a..5dd87300ce4d9 100644 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index d1ac7d8cda86a..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..0faf146643db8 Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png deleted file mode 100644 index 2f19c06702052..0000000000000 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_18.2_simulator.png and /dev/null differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png new file mode 100644 index 0000000000000..f2de236bd27b4 Binary files /dev/null and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_view_clip_rrect_multiple_clips_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png similarity index 98% rename from engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png rename to engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png index bd4660b67cae4..e2055a242d973 100644 Binary files a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_18.2_simulator.png and b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/golden_two_platform_views_with_other_backdrop_filter_impeller_iPhone SE (3rd generation)_26.2_simulator.png differ diff --git a/engine/src/flutter/testing/run_tests.py b/engine/src/flutter/testing/run_tests.py index ad378f7ea6b9e..bbf729bf63eb7 100755 --- a/engine/src/flutter/testing/run_tests.py +++ b/engine/src/flutter/testing/run_tests.py @@ -485,6 +485,7 @@ def make_test( make_test('platform_view_android_delegate_unittests'), # https://github.com/flutter/flutter/issues/36295 make_test('shell_unittests'), + make_test('shorebird_unittests'), ] if is_windows(): diff --git a/packages/flutter/test/services/system_chrome_test.dart b/packages/flutter/test/services/system_chrome_test.dart index 94b66d23d1606..1578024bf00a1 100644 --- a/packages/flutter/test/services/system_chrome_test.dart +++ b/packages/flutter/test/services/system_chrome_test.dart @@ -22,6 +22,11 @@ void main() { }, ); + // Start with the overlay style set to dark mode. + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark); + await tester.idle(); + log.clear(); + // The first call is a cache miss and will queue a microtask SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light); expect(tester.binding.microtaskCount, equals(1)); diff --git a/packages/flutter_tools/bin/xcode_backend.dart b/packages/flutter_tools/bin/xcode_backend.dart index 2142ba29bf1c2..9f6c8983a39df 100644 --- a/packages/flutter_tools/bin/xcode_backend.dart +++ b/packages/flutter_tools/bin/xcode_backend.dart @@ -692,6 +692,14 @@ class Context { ); } + // Shorebird-specific build-trace plumbing: mac.dart sets + // SHOREBIRD_TRACE_FILE in the Xcode build environment; here (running + // as an Xcode build phase script) we forward it to flutter assemble. + final String? shorebirdTraceFile = environment['SHOREBIRD_TRACE_FILE']; + if (shorebirdTraceFile != null && shorebirdTraceFile.isNotEmpty) { + flutterArgs.add('--shorebird-trace-file=$shorebirdTraceFile'); + } + if (environment['CODE_SIZE_DIRECTORY'] != null && environment['CODE_SIZE_DIRECTORY']!.isNotEmpty) { flutterArgs.add('-dCodeSizeDirectory=${environment['CODE_SIZE_DIRECTORY']}'); diff --git a/packages/flutter_tools/gradle/aar_init_script.gradle b/packages/flutter_tools/gradle/aar_init_script.gradle index 0d43a9b103fd2..283e0343c28d6 100644 --- a/packages/flutter_tools/gradle/aar_init_script.gradle +++ b/packages/flutter_tools/gradle/aar_init_script.gradle @@ -33,7 +33,7 @@ void configureProject(Project project, String outputDir) { project.publishing { repositories { maven { - url = uri("file://${outputDir}/outputs/repo") + url = project.file("${outputDir}/outputs/repo").toURI() } } } @@ -42,7 +42,7 @@ void configureProject(Project project, String outputDir) { return } - String storageUrl = System.getenv('FLUTTER_STORAGE_BASE_URL') ?: "https://storage.googleapis.com" + String storageUrl = System.getenv('FLUTTER_STORAGE_BASE_URL') ?: "https://download.shorebird.dev" String engineRealm = Paths.get(getFlutterRoot(project), "bin", "cache", "engine.realm") .toFile().text.trim() diff --git a/packages/flutter_tools/gradle/build.gradle.kts b/packages/flutter_tools/gradle/build.gradle.kts index 43f5e8368bc41..9b4516d4ebc5f 100644 --- a/packages/flutter_tools/gradle/build.gradle.kts +++ b/packages/flutter_tools/gradle/build.gradle.kts @@ -66,6 +66,7 @@ dependencies { // * ndkVersion in FlutterExtension in packages/flutter_tools/gradle/src/main/kotlin/FlutterExtension.kt compileOnly("com.android.tools.build:gradle:8.11.1") + implementation("org.yaml:snakeyaml:2.0") testImplementation(kotlin("test")) testImplementation("com.android.tools.build:gradle:8.11.1") testImplementation("org.mockito:mockito-core:5.8.0") diff --git a/packages/flutter_tools/gradle/shorebird_trace_init.gradle b/packages/flutter_tools/gradle/shorebird_trace_init.gradle new file mode 100644 index 0000000000000..8f469f87397bf --- /dev/null +++ b/packages/flutter_tools/gradle/shorebird_trace_init.gradle @@ -0,0 +1,159 @@ +// Emits Chrome Trace Event Format events for every Gradle task so the +// Shorebird build trace (`flutter build ... --shorebird-trace`) can show +// per-task timings alongside flutter tool + flutter assemble spans. +// +// This file is Shorebird-specific โ€” it exists in the Shorebird fork of +// Flutter and has no equivalent upstream. Activated by passing +// `-I=` together with `-Pshorebird.gradle-trace-file=`. +// Output file is consumed by Flutter's gradle.dart and merged into the +// main trace. +// +// Events land on tid=4 (flutter tool = 1, native build outer = 2, flutter +// assemble = 3) so Perfetto shows a clean per-tier layout. + +import groovy.json.JsonOutput +import org.gradle.api.Task +import org.gradle.api.execution.TaskExecutionListener +import org.gradle.api.tasks.TaskState + +def traceFilePath = gradle.startParameter.projectProperties['shorebird.gradle-trace-file'] +if (!traceFilePath) { + traceFilePath = System.getProperty('shorebird.gradle-trace-file') +} +if (!traceFilePath) { + return +} + +// Flow id shared with flutter_tool so Perfetto draws an arrow from the +// parent "gradle " span into our first per-task event. Missing +// when flutter_tool is older than the flow-id plumbing; that's fine, +// the init script just skips the flow-end event in that case. +def gradleFlowIdRaw = gradle.startParameter.projectProperties['shorebird.gradle-trace-flow-id'] +def gradleFlowId = gradleFlowIdRaw ? gradleFlowIdRaw.toLong() : null + +// Real pid of this Gradle JVM โ€” Gradle tasks belong to this process, so +// emitting spans on it matches the process topology. Named via a +// `process_name` metadata event below so Perfetto labels the row +// "gradle" rather than showing a bare number. +def gradlePid = ProcessHandle.current().pid() +def gradleTid = 1 + +def events = Collections.synchronizedList([ + [ + name: 'process_name', + ph: 'M', + pid: gradlePid, + args: [name: 'gradle'], + ], + [ + name: 'thread_name', + ph: 'M', + pid: gradlePid, + tid: gradleTid, + args: [name: 'gradle tasks'], + ], +]) +def firstTaskFlowEmitted = new java.util.concurrent.atomic.AtomicBoolean(false) +def startTimesMicros = Collections.synchronizedMap([:]) + +gradle.addListener(new TaskExecutionListener() { + @Override + void beforeExecute(Task task) { + startTimesMicros[task.path] = System.currentTimeMillis() * 1000L + } + + @Override + void afterExecute(Task task, TaskState state) { + def start = startTimesMicros.remove(task.path) + if (start == null) return + def end = System.currentTimeMillis() * 1000L + def dur = Math.max(0L, end - start) + // Classify the task path into a small set of "kinds" so downstream + // summarizers can bucket without retaining names. Match against the + // task's simple name (last colon segment) so plugin names like + // `:package_info_plus:...` don't accidentally trigger the `packaging` + // bucket. + // + // The kind strings below are a wire-format contract with + // shorebird_cli (which buckets its summary by these values). + // KEEP IN SYNC with the `GradleTaskKind` enum in the + // `shorebird_build_trace` package; adding a new kind means + // landing the matching enum value there AND teaching + // shorebird_cli to bucket it, otherwise it silently falls into + // `other`. + def path = task.path + def lastColon = path.lastIndexOf(':') + def simpleName = (lastColon >= 0 ? path.substring(lastColon + 1) : path).toLowerCase() + + // Order matters: the first regex that matches wins. `kotlin_compile` + // must precede `java_compile` (both match `compile*`). `java_compile` + // must precede the `scaffold` bucket's `prepare*` because AGP's + // javaPreCompileRelease is scaffolding-shaped but semantically javac. + // `flutter_gradle_plugin` must precede `packaging` so tasks like + // `packageFlutterBuild` don't fall through to the `package*` bucket. + def kindRules = [ + [~/^compile.*kotlin.*/, 'kotlin_compile'], + [~/(^compile.*(java|jni).*|.*precompile.*)/, 'java_compile'], + [~/.*aidl.*/, 'aidl'], + [~/(^minify.*|.*r8.*)/, 'r8_minify'], + [~/.*dex.*/, 'dex'], + [~/^lint.*/, 'lint'], + [~/(.*jnilibs.*|.*nativelibs.*|.*link.*native.*)/, 'native_link'], + [~/.*(processresources|mergeresources|mergemanifest|rfile|parserelease|mergerelease|generaterelease).*/, 'resources'], + [~/(^compileflutter.*|.*flutterbuild.*|^flutter.*)/, 'flutter_gradle_plugin'], + [~/^package.*/, 'packaging'], + [~/.*bundle.*/, 'bundle'], + [~/.*transform.*/, 'transform'], + // Catch-all for Gradle's per-plugin / per-variant scaffolding + // (metadata files, proguard rule export, pre-/post-compile + // bookkeeping). Individually small but the long tail adds up on + // plugin-heavy apps. + [~/(.*aarmetadata.*|.*proguard.*|.*validate.*|^check.*|^prepare.*|^generate.*|^copy.*)/, 'gradle_scaffold'], + ] + def kind = kindRules.find { simpleName ==~ it[0] }?.getAt(1) ?: 'other' + + // The task "owner" is the first colon-separated segment. For + // subproject tasks this is typically the plugin name (e.g. + // `:camera_android`). Kept for local debug โ€” the privacy-safe + // summary that Shorebird writes aggregates this out. + def owner = path.startsWith(':') ? path.substring(1).split(':')[0] : '' + + // Emit the flow-end event on the first task, connecting back to + // flutter_tool's "gradle " span. Atomic so we emit at most + // one across concurrent task callbacks. + if (gradleFlowId != null && firstTaskFlowEmitted.compareAndSet(false, true)) { + events.add([ + ph: 'f', + name: 'spawn', + cat: 'flow', + id: gradleFlowId, + ts: start, + pid: gradlePid, + tid: gradleTid, + bp: 'e', + ]) + } + events.add([ + name: path, + cat: 'gradle_task', + ph: 'X', + ts: start, + dur: dur, + pid: gradlePid, + tid: gradleTid, + args: [ + kind: kind, + owner: owner, + skipped: state.skipped, + upToDate: state.upToDate, + fromCache: state.skipMessage == 'FROM-CACHE', + ], + ]) + } +}) + +gradle.buildFinished { + def f = new File(traceFilePath) + f.parentFile?.mkdirs() + f.text = JsonOutput.toJson(events) +} diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt index 7d09171f31e33..7cf22e36a7ab0 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt @@ -6,6 +6,7 @@ package com.flutter.gradle import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.dsl.BuildType +import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.gradle.AbstractAppExtension import com.android.build.gradle.BaseExtension import com.android.build.gradle.LibraryExtension @@ -15,6 +16,7 @@ import com.android.build.gradle.tasks.ProcessAndroidResources import com.flutter.gradle.FlutterPluginConstants.PLATFORM_ABI_LIST import com.flutter.gradle.FlutterPluginUtils.readPropertiesIfExist import com.flutter.gradle.plugins.PluginHandler +import com.flutter.gradle.tasks.CopyFlutterJniLibsTask import com.flutter.gradle.tasks.FlutterTask import org.gradle.api.GradleException import org.gradle.api.Plugin @@ -23,7 +25,6 @@ import org.gradle.api.Task import org.gradle.api.UnknownTaskException import org.gradle.api.file.Directory import org.gradle.api.tasks.Copy -import org.gradle.api.tasks.Sync import org.gradle.api.tasks.TaskProvider import org.gradle.internal.os.OperatingSystem import org.gradle.kotlin.dsl.support.serviceOf @@ -305,15 +306,41 @@ class FlutterPlugin : Plugin { val targetPlatforms: List = FlutterPluginUtils.getTargetPlatforms(projectToAddTasksTo) - // TODO(reidbaker): Migrate to getAndroidApplicationExtension and getAndroidLibraryExtension. - val androidExtension = FlutterPluginUtils.getLegacyAndroidExtension(projectToAddTasksTo) - androidExtension.sourceSets.all { - val sourceSet = this - val jniLibsDir = - projectToAddTasksTo.layout.buildDirectory.dir( - "${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${sourceSet.name}/jniLibs" - ) - sourceSet.jniLibs.srcDir(jniLibsDir.get().asFile) + // The Android Gradle Plugin is always applied to Flutter Android projects, so its components + // extension is expected to be present. Use getByType (not findByType) so a misconfiguration + // fails loudly rather than silently skipping libapp.so registration. + val androidComponents = projectToAddTasksTo.extensions.getByType(AndroidComponentsExtension::class.java) + val targetPlatformsList = targetPlatforms + androidComponents.onVariants { variant -> + val capitalizeVariantName = FlutterPluginUtils.capitalize(variant.name) + val compileTaskName = flutterCompileTaskName(variant.name) + val copyJniLibsTaskProvider: TaskProvider = + projectToAddTasksTo.tasks.register( + "copyJniLibs${FLUTTER_BUILD_PREFIX}$capitalizeVariantName", + CopyFlutterJniLibsTask::class.java + ) { + // The Flutter compile task is registered later (in the legacy + // `applicationVariants` callback in addFlutterDeps) and only for variants that + // are actually built as a Flutter app. It is absent for e.g. an + // `assembleAndroidTest` build, where `shouldConfigureFlutterTask` returns false. + // Look it up tolerantly (findByName, not named) so this task degrades to a no-op + // with empty output instead of failing to be created when there is no Flutter + // build for the variant. See https://github.com/flutter/flutter/issues/188785. + dependsOn(projectToAddTasksTo.tasks.matching { it.name == compileTaskName }) + intermediateDir.set( + projectToAddTasksTo.layout.dir( + projectToAddTasksTo.provider { + val compileTask = projectToAddTasksTo.tasks.findByName(compileTaskName) as? FlutterTask + compileTask?.outputDirectory + } + ) + ) + this.targetPlatforms.set(targetPlatformsList) + } + variant.sources.jniLibs?.addGeneratedSourceDirectory( + copyJniLibsTaskProvider, + CopyFlutterJniLibsTask::destinationDir + ) } val flutterPlugin = this @@ -502,6 +529,16 @@ class FlutterPlugin : Plugin { */ private const val FLUTTER_BUILD_PREFIX: String = "flutterBuild" + /** + * The name of the [FlutterTask] (the `flutter assemble` invocation) for [variantName]. + * + * Built identically by [addFlutterDeps], which registers the task, and by the variant API + * callback in [addFlutterTasks], which references it by name (because that callback runs + * before the task is registered). + */ + private fun flutterCompileTaskName(variantName: String): String = + FlutterPluginUtils.toCamelCase(listOf("compile", FLUTTER_BUILD_PREFIX, variantName)) + /** * Configures flutter default abi support respecting flutter command line flags. */ @@ -608,6 +645,8 @@ class FlutterPlugin : Plugin { val dartDefinesValue: String? = project.findProperty("dart-defines")?.toString() val performanceMeasurementFileValue: String? = project.findProperty("performance-measurement-file")?.toString() + val shorebirdTraceFileValue: String? = + project.findProperty("shorebird-trace-file")?.toString() val codeSizeDirectoryValue: String? = project.findProperty("code-size-directory")?.toString() val deferredComponentsValue: Boolean = @@ -660,14 +699,7 @@ class FlutterPlugin : Plugin { val variantBuildMode: String = FlutterPluginUtils.buildModeFor(variant.buildType) val flavorValue: String = variant.flavorName - val taskName: String = - FlutterPluginUtils.toCamelCase( - listOf( - "compile", - FLUTTER_BUILD_PREFIX, - variant.name - ) - ) + val taskName: String = flutterCompileTaskName(variant.name) // The task provider below will shadow a lot of the variable names, so provide this reference // to access them within that scope. @@ -705,45 +737,13 @@ class FlutterPlugin : Plugin { dartObfuscation = dartObfuscationValue dartDefines = dartDefinesValue performanceMeasurementFile = performanceMeasurementFileValue + shorebirdTraceFile = shorebirdTraceFileValue codeSizeDirectory = codeSizeDirectoryValue deferredComponents = deferredComponentsValue validateDeferredComponents = validateDeferredComponentsValue flavor = flavorValue } val flutterCompileTask: FlutterTask = compileTaskProvider.get() - val jniLibsDir = - project.layout.buildDirectory.dir( - "${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${variant.name}/jniLibs" - ) - val copyJniLibsTaskProvider: TaskProvider = - project.tasks.register( - "copyJniLibs${FLUTTER_BUILD_PREFIX}${FlutterPluginUtils.capitalize(variant.name)}", - Sync::class.java - ) { - dependsOn(flutterCompileTask) - into(jniLibsDir) - targetPlatforms.forEach { targetPlatform -> - val abi: String? = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetPlatform] - from("${flutterCompileTask.intermediateDir}/$abi") { - include("*.so") - rename { filename: String -> "lib$filename" } - into(abi ?: "null") - } - // Copy the native assets created by build.dart and placed in build/native_assets by flutter assemble. - val nativeAssetsDir = - "${flutterCompileTask.intermediateDir}/native_assets/jniLibs/lib" - from("$nativeAssetsDir/$abi") { - include("*.so") - into(abi ?: "null") - } - } - } - val mergeJniLibsTaskName = "merge${FlutterPluginUtils.capitalize(variant.name)}JniLibFolders" - project.tasks.configureEach { - if (name == mergeJniLibsTaskName) { - dependsOn(copyJniLibsTaskProvider) - } - } val copyFlutterAssetsTaskProvider: TaskProvider = project.tasks.register( "copyFlutterAssets${FlutterPluginUtils.capitalize(variant.name)}", diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt index 13eb2be26758b..61e8e3c17c006 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginConstants.kt @@ -21,7 +21,7 @@ object FlutterPluginConstants { const val INTERMEDIATES_DIR = "intermediates" const val FLUTTER_STORAGE_BASE_URL = "FLUTTER_STORAGE_BASE_URL" - const val DEFAULT_MAVEN_HOST = "https://storage.googleapis.com" + const val DEFAULT_MAVEN_HOST = "https://download.shorebird.dev" /** Maps platforms to ABI architectures. */ @JvmStatic val PLATFORM_ARCH_MAP = diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt index 893e338af33af..94376cad6b2fb 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt @@ -62,7 +62,7 @@ object FlutterPluginUtils { * The URL for documentation instructing app developers on how to report incompatible KGP usage to plugin authors. */ internal const val BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS = - "https://docs.flutter.dev/release/breaking-changes/migrate-to-built-in-kotlin/for-app-developers/report-incompatible-kotlin-gradle-plugin-usage-to-plugin-authors" + "https://docs.flutter.dev/release/breaking-changes/migrate-to-built-in-kotlin/for-app-developers#report-incompatible-kotlin-gradle-plugin-usage-to-plugin-authors" /** * Matches the AGP application plugin declaration in Kotlin DSL (`build.gradle.kts`). @@ -656,6 +656,11 @@ object FlutterPluginUtils { } project.gradle.projectsEvaluated { + // Safe to query AGP version after all projects are evaluated. + val agpVersion = VersionFetcher.getAGPVersion(project) + if (agpVersion == null || agpVersion.major < 9) { + return@projectsEvaluated + } if (shouldLogForApp) { project.logger.error( """ diff --git a/packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTask.kt b/packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTask.kt index 1858a139d0e96..96331ef87c94d 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTask.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTask.kt @@ -111,6 +111,12 @@ open class BaseFlutterTask : DefaultTask() { @Input var performanceMeasurementFile: String? = null + // Shorebird-specific: path for the Shorebird build trace. Prefixed so + // it stands out as fork-only surface in this otherwise upstream file. + @Optional + @Input + var shorebirdTraceFile: String? = null + @Optional @Input var deferredComponents: Boolean? = null diff --git a/packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTaskHelper.kt b/packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTaskHelper.kt index 2d16640d324fd..2f5fac2de3a76 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTaskHelper.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/tasks/BaseFlutterTaskHelper.kt @@ -110,6 +110,9 @@ object BaseFlutterTaskHelper { baseFlutterTask.performanceMeasurementFile?.let { args("--performance-measurement-file=$it") } + baseFlutterTask.shorebirdTraceFile?.let { + args("--shorebird-trace-file=$it") + } args("-dTargetFile=${baseFlutterTask.targetPath}") args("-dTargetPlatform=android") args("-dBuildMode=${baseFlutterTask.buildMode}") diff --git a/packages/flutter_tools/gradle/src/main/kotlin/tasks/CopyFlutterJniLibsTask.kt b/packages/flutter_tools/gradle/src/main/kotlin/tasks/CopyFlutterJniLibsTask.kt new file mode 100644 index 0000000000000..6950e827ea633 --- /dev/null +++ b/packages/flutter_tools/gradle/src/main/kotlin/tasks/CopyFlutterJniLibsTask.kt @@ -0,0 +1,78 @@ +// 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. + +package com.flutter.gradle.tasks + +import com.flutter.gradle.FlutterPluginConstants +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import javax.inject.Inject + +/** + * Stages the native libraries produced by the Flutter build (`libapp.so` and any bundled native + * assets) into a dedicated [destinationDir], laid out as `/lib*.so`. + * + * It deliberately writes to its own output directory rather than into the Flutter task's output + * directory. Two earlier approaches dropped `libapp.so` from the APK/app bundle: nesting this output + * inside the Flutter task's output directory created overlapping task outputs that broke Gradle's + * incremental checks (flavored single-ABI rebuilds), and registering the staged directory eagerly as + * a source set `srcDir` captured the build directory before it had been redirected. See + * https://github.com/flutter/flutter/issues/186810 and + * https://github.com/flutter/flutter/issues/187388. + */ +abstract class CopyFlutterJniLibsTask : DefaultTask() { + /** + * The Flutter build output directory (the `flutter assemble` `--output` location). + * + * Optional: it is absent when there is no Flutter compile task for the variant (e.g. an + * `assembleAndroidTest` build), in which case this task stages nothing. See + * https://github.com/flutter/flutter/issues/188785. + */ + @get:Optional + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val intermediateDir: DirectoryProperty + + @get:Input + abstract val targetPlatforms: ListProperty + + @get:OutputDirectory + abstract val destinationDir: DirectoryProperty + + @get:Inject + abstract val fileSystemOperations: FileSystemOperations + + @TaskAction + fun copy() { + fileSystemOperations.sync { + into(destinationDir) + // When there is no Flutter build for this variant (e.g. an assembleAndroidTest build), + // there is nothing to stage; the empty sync simply clears destinationDir. + if (intermediateDir.isPresent) { + targetPlatforms.get().forEach { targetPlatform -> + val abi: String? = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetPlatform] + from(intermediateDir.dir(abi ?: "null")) { + include("*.so") + rename { filename: String -> "lib$filename" } + into(abi ?: "null") + } + val nativeAssetsDir = intermediateDir.dir("native_assets/jniLibs/lib/$abi") + from(nativeAssetsDir) { + include("*.so") + into(abi ?: "null") + } + } + } + } + } +} diff --git a/packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt index 07e4ce26de9a5..aad40b31d3402 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt @@ -53,10 +53,10 @@ private const val FAKE_PROJECT_ROOT_DIR = "/fake/root/dir" // are updated in DependencyVersionChecker.kt // These values should also match the flutter create template values. // In //packages/flutter_tools/lib/src/android/gradle_utils.dart -private const val SUPPORTED_GRADLE_VERSION: String = "8.14" +private const val SUPPORTED_GRADLE_VERSION: String = "9.1.0" private val SUPPORTED_JAVA_VERSION: JavaVersion = JavaVersion.VERSION_17 -private val SUPPORTED_AGP_VERSION: AndroidPluginVersion = AndroidPluginVersion(8, 11, 1) -private const val SUPPORTED_KGP_VERSION: String = "2.2.20" +private val SUPPORTED_AGP_VERSION: AndroidPluginVersion = AndroidPluginVersion(9, 0, 1) +private const val SUPPORTED_KGP_VERSION: String = "2.3.20" private val SUPPORTED_SDK_VERSION: MinSdkVersion = MinSdkVersion("release", 30) class DependencyVersionCheckerTest { diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt index c1cbdceb41c8f..38b916fc19873 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt @@ -59,10 +59,11 @@ class FlutterPluginTest { every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension val mockAndroidComponentsExtension = mockk>(relaxed = true) every { project.extensions.getByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension - every { mockAndroidComponentsExtension.selector() } returns - mockk { - every { all() } returns mockk() - } + every { project.extensions.findByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension + val mockSelector = mockk(relaxed = true) + every { mockAndroidComponentsExtension.selector() } returns mockSelector + every { mockSelector.all() } returns mockSelector + every { mockSelector.withName(any()) } returns mockSelector every { project.extensions.getByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension every { project.extensions.getByType(LibraryExtension::class.java) } returns mockLibraryExtension every { project.extensions.findByName("android") } returns mockAbstractAppExtension @@ -151,10 +152,11 @@ class FlutterPluginTest { every { project.extensions.findByName("android") } returns mockAbstractAppExtension val mockAndroidComponentsExtension = mockk>(relaxed = true) every { project.extensions.getByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension - every { mockAndroidComponentsExtension.selector() } returns - mockk { - every { all() } returns mockk() - } + every { project.extensions.findByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension + val mockSelector = mockk(relaxed = true) + every { mockAndroidComponentsExtension.selector() } returns mockSelector + every { mockSelector.all() } returns mockSelector + every { mockSelector.withName(any()) } returns mockSelector every { project.projectDir } returns projectDir.toFile() every { project.findProperty("flutter.sdk") } returns fakeFlutterSdkDir.toString() every { project.file(fakeFlutterSdkDir.toString()) } returns fakeFlutterSdkDir.toFile() diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt index 28bc9c00d806a..bf282ba0aaf20 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt @@ -4,6 +4,7 @@ package com.flutter.gradle +import com.android.build.api.AndroidPluginVersion import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.gradle.BaseExtension @@ -21,6 +22,7 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkObject import io.mockk.slot +import io.mockk.unmockkObject import io.mockk.verify import org.gradle.api.Action import org.gradle.api.GradleException @@ -35,6 +37,8 @@ import org.gradle.api.plugins.PluginManager import org.gradle.internal.impldep.junit.framework.TestCase.assertFalse import org.gradle.internal.impldep.junit.framework.TestCase.assertTrue import org.jetbrains.kotlin.gradle.plugin.extraProperties +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.io.TempDir @@ -46,6 +50,33 @@ import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Configuration for a mock Gradle subproject. + * + * @property name The name of the subproject. + * @property declarativelyAppliedPlugins Plugins applied via the modern Gradle `plugins {}` block. + * For more details, see [Gradle Plugins Block Docs](https://docs.gradle.org/current/userguide/plugins_intermediate.html#sec:plugins_block). + * @property imperativelyAppliedPlugins Plugins applied via the legacy Gradle `apply plugin:` statement. + * For more details, see [Gradle Old Plugin Application Docs](https://docs.gradle.org/current/userguide/plugins_intermediate.html#sec:old_plugin_application). + */ +private data class SubprojectConfig( + val name: String, + val declarativelyAppliedPlugins: List = emptyList(), + val imperativelyAppliedPlugins: List = emptyList() +) + +private class TestEnvironment( + val appProject: Project, + val plugins: List, + val subprojectsActionSlot: io.mockk.CapturingSlot> = slot(), + val projectsEvaluatedActionSlot: io.mockk.CapturingSlot> = slot() +) { + val appPluginManager: PluginManager get() = appProject.pluginManager + val plugin1Manager: PluginManager get() = plugins[0].pluginManager + val plugin2Manager: PluginManager get() = plugins[1].pluginManager +} class FlutterPluginUtilsTest { companion object { @@ -998,612 +1029,530 @@ class FlutterPluginUtilsTest { @Nested inner class DetectApplyingKotlinGradlePluginTests { - @Test - fun `logs app warning when KGP is only applied in app`( - @TempDir tempDir: Path - ) { - val appDir = tempDir.resolve("app").toFile().apply { mkdirs() } - val appBuildGradleFile = - File(appDir, "build.gradle").apply { - createNewFile() - writeText( - """ - plugins { - id("com.android.application") - id("kotlin-android") - } - """.trimIndent() - ) - } + private val rootProject = mockk() + private val mockGradle = mockk() + private val mockLogger = mockk(relaxed = true) + + // This AGP version will should match the Flutter create template values. + // In //packages/flutter_tools/lib/src/android/gradle_utils.dart + private val templateAgpVersion = AndroidPluginVersion(9, 0, 1) + + private val errorAgpVersion = DependencyVersionChecker.errorAGPVersion + + @BeforeEach + fun setUp() { + mockkObject(VersionFetcher) + } + + @AfterEach + fun tearDown() { + unmockkObject(VersionFetcher) + } - val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() } - val pluginBuildGradleFile = - File(pluginDir, "build.gradle").apply { + private fun createSubproject( + tempDir: Path, + projectName: String, + declarativelyAppliedPlugins: List = emptyList(), + imperativelyAppliedPlugins: List = emptyList() + ): Project { + val projectDir = tempDir.resolve(projectName).toFile().apply { mkdirs() } + val buildGradleFile = + File(projectDir, "build.gradle").apply { createNewFile() - writeText( - """ - plugins { - id("com.android.library") + // Expected output of declarativeBlock if declarativelyAppliedPlugins contains ["kotlin-android"]: + // plugins { + // id("kotlin-android") + // } + val declarativeBlock = + if (declarativelyAppliedPlugins.isNotEmpty()) { + "plugins {\n" + declarativelyAppliedPlugins.joinToString("\n") { " id(\"$it\")" } + "\n}\n" + } else { + "" } - """.trimIndent() - ) + // Expected output of imperativeBlock if imperativelyAppliedPlugins contains ["kotlin-android"]: + // apply plugin: 'kotlin-android' + val imperativeBlock = + if (imperativelyAppliedPlugins.isNotEmpty()) { + imperativelyAppliedPlugins.joinToString("\n") { "apply plugin: '$it'" } + "\n" + } else { + "" + } + writeText(declarativeBlock + imperativeBlock) } + val pluginManager = mockk(relaxed = true) + val project = mockk() + every { project.name } returns projectName + every { project.projectDir } returns projectDir + every { project.buildFile } returns buildGradleFile + every { project.logger } returns mockLogger + every { project.pluginManager } returns pluginManager + every { project.rootProject } returns rootProject + every { project.gradle } returns mockGradle + + val extensions = mockk() + every { extensions.findByType(any>()) } returns null + every { project.extensions } returns extensions + + return project + } - val rootProject = mockk() - val mockGradle = mockk() - val mockLogger = mockk(relaxed = true) - - val appProjectPluginManager = mockk(relaxed = true) - val pluginProjectPluginManager = mockk(relaxed = true) + private fun setupTest( + tempDir: Path, + agpVersion: AndroidPluginVersion, + appConfig: SubprojectConfig, + pluginConfigs: List + ): TestEnvironment { + every { VersionFetcher.getAGPVersion(any()) } returns agpVersion val appProject = - createMockSubproject( - tempDir = tempDir, - buildFile = appBuildGradleFile, - projectName = "app", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = appProjectPluginManager - ) - - val pluginProject = - createMockSubproject( + createSubproject( tempDir = tempDir, - buildFile = pluginBuildGradleFile, - projectName = "plugin", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = pluginProjectPluginManager + projectName = appConfig.name, + declarativelyAppliedPlugins = appConfig.declarativelyAppliedPlugins, + imperativelyAppliedPlugins = appConfig.imperativelyAppliedPlugins ) - val subprojectsActionSlot = slot>() - val projectsEvaluatedActionSlot = slot>() + val pluginProjects = + pluginConfigs.map { config -> + createSubproject( + tempDir = tempDir, + projectName = config.name, + declarativelyAppliedPlugins = config.declarativelyAppliedPlugins, + imperativelyAppliedPlugins = config.imperativelyAppliedPlugins + ) + } - every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit - every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit + val allProjects = setOf(appProject) + pluginProjects + every { rootProject.subprojects } returns allProjects - detectApplyingKotlinGradlePlugin(appProject) + val testProject = TestEnvironment(appProject, pluginProjects) + every { rootProject.subprojects(capture(testProject.subprojectsActionSlot)) } returns Unit + every { mockGradle.projectsEvaluated(capture(testProject.projectsEvaluatedActionSlot)) } returns Unit - verify { rootProject.subprojects(capture(subprojectsActionSlot)) } - subprojectsActionSlot.captured.execute(appProject) - subprojectsActionSlot.captured.execute(pluginProject) + return testProject + } - verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } - projectsEvaluatedActionSlot.captured.execute(mockGradle) + private fun executeDetectApplyingKotlinGradlePlugin(testProject: TestEnvironment) { + detectApplyingKotlinGradlePlugin(testProject.appProject) - verify { - mockLogger.error( - """ - WARNING: Your Android app project: app located at: ${appBuildGradleFile.absolutePath} - applies the Kotlin Gradle Plugin, which will cause build failures in future versions of Flutter. - Please migrate your app to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_APPS - - """.trimIndent() - ) + verify { rootProject.subprojects(capture(testProject.subprojectsActionSlot)) } + testProject.subprojectsActionSlot.captured.execute(testProject.appProject) + for (plugin in testProject.plugins) { + testProject.subprojectsActionSlot.captured.execute(plugin) } - verify(exactly = 0) { - mockLogger.error(match { it.contains("Your app uses the following plugins") }) - } - verify(exactly = 0) { appProjectPluginManager.apply("kotlin-android") } - verify(exactly = 1) { pluginProjectPluginManager.apply("kotlin-android") } + verify { mockGradle.projectsEvaluated(capture(testProject.projectsEvaluatedActionSlot)) } + testProject.projectsEvaluatedActionSlot.captured.execute(mockGradle) } - @Test - fun `logs plugin warning when KGP is only applied in one plugin`( - @TempDir tempDir: Path - ) { - val appDir = tempDir.resolve("app").toFile().apply { mkdirs() } - val appBuildGradleFile = - File(appDir, "build.gradle").apply { - createNewFile() - writeText( - """ - plugins { - id("com.android.application") - } - """.trimIndent() + @Nested + inner class TestLogWarningsWhenAGPis9OrHigher { + @Test + fun `logs app warning when KGP is only applied in app`( + @TempDir tempDir: Path + ) { + val testProject = + setupTest( + tempDir = tempDir, + agpVersion = templateAgpVersion, + appConfig = + SubprojectConfig( + "app", + declarativelyAppliedPlugins = listOf("com.android.application", "kotlin-android") + ), + pluginConfigs = + listOf( + SubprojectConfig( + "plugin", + declarativelyAppliedPlugins = listOf("com.android.library") + ) + ) ) - } - val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() } - val pluginBuildGradleFile = - File(pluginDir, "build.gradle").apply { - createNewFile() - writeText( - """ - plugins { - id("com.android.library") - id("kotlin-android") + val appPluginManager = testProject.appPluginManager + val plugin1Manager = testProject.plugin1Manager + + executeDetectApplyingKotlinGradlePlugin(testProject) + + verify { + mockLogger.error( + match { + it.contains("WARNING: Your Android app project") && + it.contains("applies the Kotlin Gradle Plugin") && + it.contains(BUILT_IN_KOTLIN_DOCS_FOR_APPS) } - """.trimIndent() ) } - val rootProject = mockk() - val mockGradle = mockk() - val mockLogger = mockk(relaxed = true) - - val appProjectPluginManager = mockk(relaxed = true) - val pluginProjectPluginManager = mockk(relaxed = true) - - val appProject = - createMockSubproject( - tempDir = tempDir, - buildFile = appBuildGradleFile, - projectName = "app", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = appProjectPluginManager - ) + verify(exactly = 0) { + mockLogger.error(match { it.contains("WARNING: Your app uses the following plugins") }) + } + verify(exactly = 0) { appPluginManager.apply("kotlin-android") } + verify(exactly = 1) { plugin1Manager.apply("kotlin-android") } + } - val pluginProject = - createMockSubproject( - tempDir = tempDir, - buildFile = pluginBuildGradleFile, - projectName = "plugin", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = pluginProjectPluginManager - ) + @Test + fun `logs plugin warning when KGP is only applied in one plugin`( + @TempDir tempDir: Path + ) { + val testProject = + setupTest( + tempDir = tempDir, + agpVersion = templateAgpVersion, + appConfig = + SubprojectConfig( + "app", + declarativelyAppliedPlugins = listOf("com.android.application") + ), + pluginConfigs = + listOf( + SubprojectConfig( + "plugin", + declarativelyAppliedPlugins = listOf("com.android.library", "kotlin-android") + ) + ) + ) - val subprojectsActionSlot = slot>() - val projectsEvaluatedActionSlot = slot>() - - every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit - every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit - - detectApplyingKotlinGradlePlugin(appProject) - - verify { rootProject.subprojects(capture(subprojectsActionSlot)) } - subprojectsActionSlot.captured.execute(appProject) - subprojectsActionSlot.captured.execute(pluginProject) - - verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } - projectsEvaluatedActionSlot.captured.execute(mockGradle) - - verify { - mockLogger.error( - """ - WARNING: Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP): plugin - Future versions of Flutter will fail to build if your app uses plugins that apply KGP. - - Please check the changelogs of these plugins and upgrade to a version that supports Built-in Kotlin. - If no such version exists, report the issue to the plugin. If necessary, here is a guide on filing - an issue against a plugin: $BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS - - If you are a plugin author, please migrate your plugin to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS - """.trimIndent() - ) - } + val appPluginManager = testProject.appPluginManager + val plugin1Manager = testProject.plugin1Manager - verify(exactly = 0) { - mockLogger.error(match { it.contains("Your Android app project") }) - } - verify(exactly = 1) { appProjectPluginManager.apply("kotlin-android") } - verify(exactly = 0) { pluginProjectPluginManager.apply("kotlin-android") } - } + executeDetectApplyingKotlinGradlePlugin(testProject) - @Test - fun `logs app and plugin warning when KGP is applied in both app and plugins`( - @TempDir tempDir: Path - ) { - val appDir = tempDir.resolve("app").toFile().apply { mkdirs() } - val appBuildGradleFile = - File(appDir, "build.gradle").apply { - createNewFile() - writeText( - """ - plugins { - id("com.android.application") - id("kotlin-android") + verify { + mockLogger.error( + match { + it.contains("WARNING: Your app uses the following plugins") && + it.contains(BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS) && + it.contains(BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS) } - """.trimIndent() ) } - val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() } - val pluginBuildGradleFile = - File(pluginDir, "build.gradle").apply { - createNewFile() - writeText( - """ - plugins { - id("com.android.library") - id("kotlin-android") - } - """.trimIndent() - ) + verify(exactly = 0) { + mockLogger.error(match { it.contains("WARNING: Your Android app project") }) } + verify(exactly = 1) { appPluginManager.apply("kotlin-android") } + verify(exactly = 0) { plugin1Manager.apply("kotlin-android") } + } - val rootProject = mockk() - val mockGradle = mockk() - val mockLogger = mockk(relaxed = true) - - val appProjectPluginManager = mockk(relaxed = true) - val pluginProjectPluginManager = mockk(relaxed = true) - - val appProject = - createMockSubproject( - tempDir = tempDir, - buildFile = appBuildGradleFile, - projectName = "app", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = appProjectPluginManager - ) - - val pluginProject = - createMockSubproject( - tempDir = tempDir, - buildFile = pluginBuildGradleFile, - projectName = "plugin", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = pluginProjectPluginManager - ) - - val subprojectsActionSlot = slot>() - val projectsEvaluatedActionSlot = slot>() + @Test + fun `logs app and plugin warning when KGP is applied in both app and plugins`( + @TempDir tempDir: Path + ) { + val testProject = + setupTest( + tempDir = tempDir, + agpVersion = templateAgpVersion, + appConfig = + SubprojectConfig( + "app", + declarativelyAppliedPlugins = listOf("com.android.application", "kotlin-android") + ), + pluginConfigs = + listOf( + SubprojectConfig( + "plugin", + declarativelyAppliedPlugins = listOf("com.android.library", "kotlin-android") + ) + ) + ) - every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit - every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit + val appPluginManager = testProject.appPluginManager + val plugin1Manager = testProject.plugin1Manager - detectApplyingKotlinGradlePlugin(appProject) + executeDetectApplyingKotlinGradlePlugin(testProject) - verify { rootProject.subprojects(capture(subprojectsActionSlot)) } - subprojectsActionSlot.captured.execute(appProject) - subprojectsActionSlot.captured.execute(pluginProject) + verify { + mockLogger.error( + match { + it.contains("WARNING: Your Android app project") && + it.contains("applies the Kotlin Gradle Plugin") && + it.contains(BUILT_IN_KOTLIN_DOCS_FOR_APPS) + } + ) + } - verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } - projectsEvaluatedActionSlot.captured.execute(mockGradle) + verify { + mockLogger.error( + match { + it.contains("WARNING: Your app uses the following plugins") && + it.contains(BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS) && + it.contains(BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS) + } + ) + } - verify { - mockLogger.error( - """ - WARNING: Your Android app project: app located at: ${appBuildGradleFile.absolutePath} - applies the Kotlin Gradle Plugin, which will cause build failures in future versions of Flutter. - Please migrate your app to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_APPS - - """.trimIndent() - ) + verify(exactly = 0) { appPluginManager.apply("kotlin-android") } + verify(exactly = 0) { plugin1Manager.apply("kotlin-android") } } - verify { - mockLogger.error( - """ - WARNING: Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP): plugin - Future versions of Flutter will fail to build if your app uses plugins that apply KGP. - - Please check the changelogs of these plugins and upgrade to a version that supports Built-in Kotlin. - If no such version exists, report the issue to the plugin. If necessary, here is a guide on filing - an issue against a plugin: $BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS - - If you are a plugin author, please migrate your plugin to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS - """.trimIndent() - ) - } + @Test + fun `logs app and plugin warning when imperative KGP configuration is applied in both app and plugins`( + @TempDir tempDir: Path + ) { + val testProject = + setupTest( + tempDir = tempDir, + agpVersion = templateAgpVersion, + appConfig = + SubprojectConfig( + "app", + imperativelyAppliedPlugins = + listOf( + "com.android.application", + "kotlin-android" + ) + ), + pluginConfigs = + listOf( + SubprojectConfig( + "plugin1", + imperativelyAppliedPlugins = + listOf( + "com.android.library", + "kotlin-android" + ) + ), + SubprojectConfig( + "plugin2", + imperativelyAppliedPlugins = + listOf( + "com.android.library", + "kotlin-android" + ) + ) + ) + ) - verify(exactly = 0) { appProjectPluginManager.apply("kotlin-android") } - verify(exactly = 0) { pluginProjectPluginManager.apply("kotlin-android") } - } + val appPluginManager = testProject.appPluginManager + val plugin1Manager = testProject.plugin1Manager + val plugin2Manager = testProject.plugin2Manager - @Test - fun `logs app and plugin warning when legacy KGP configuration is applied in both app and plugins`( - @TempDir tempDir: Path - ) { - val appDir = tempDir.resolve("app").toFile().apply { mkdirs() } - val appBuildGradleFile = - File(appDir, "build.gradle").apply { - createNewFile() - writeText( - """ - apply plugin: 'com.android.application' - apply plugin: 'kotlin-android' - """.trimIndent() + executeDetectApplyingKotlinGradlePlugin(testProject) + + verify { + mockLogger.error( + match { + it.contains("WARNING: Your Android app project") && + it.contains("applies the Kotlin Gradle Plugin") && + it.contains(BUILT_IN_KOTLIN_DOCS_FOR_APPS) + } ) } - val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() } - val pluginBuildGradleFile = - File(pluginDir, "build.gradle").apply { - createNewFile() - writeText( - """ - apply plugin: 'com.android.library' - apply plugin: 'kotlin-android' - """.trimIndent() + verify { + mockLogger.error( + match { + it.contains("WARNING: Your app uses the following plugins") && + it.contains(BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS) && + it.contains(BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS) + } ) } - val rootProject = mockk() - val mockGradle = mockk() - val mockLogger = mockk(relaxed = true) + verify(exactly = 0) { appPluginManager.apply("kotlin-android") } + verify(exactly = 0) { plugin1Manager.apply("kotlin-android") } + verify(exactly = 0) { plugin2Manager.apply("kotlin-android") } + } - val appProjectPluginManager = mockk(relaxed = true) - val pluginProjectOnePluginManager = mockk(relaxed = true) - val pluginProjectTwoPluginManager = mockk(relaxed = true) + @Test + fun `does not log when migrated to Built-in Kotlin`( + @TempDir tempDir: Path + ) { + val testProject = + setupTest( + tempDir = tempDir, + agpVersion = templateAgpVersion, + appConfig = + SubprojectConfig( + "app", + imperativelyAppliedPlugins = listOf("com.android.application") + ), + pluginConfigs = + listOf( + SubprojectConfig( + "plugin", + imperativelyAppliedPlugins = listOf("com.android.library") + ) + ) + ) - val appProject = - createMockSubproject( - tempDir = tempDir, - buildFile = appBuildGradleFile, - projectName = "app", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = appProjectPluginManager - ) + val appPluginManager = testProject.appPluginManager + val plugin1Manager = testProject.plugin1Manager - val pluginProjectOne = - createMockSubproject( - tempDir = tempDir, - buildFile = pluginBuildGradleFile, - projectName = "plugin1", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = pluginProjectOnePluginManager - ) + executeDetectApplyingKotlinGradlePlugin(testProject) - val pluginProjectTwo = - createMockSubproject( - tempDir = tempDir, - buildFile = pluginBuildGradleFile, - projectName = "plugin2", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = pluginProjectTwoPluginManager - ) + verify(exactly = 0) { + mockLogger.error(any()) + } - val subprojectsActionSlot = slot>() - val projectsEvaluatedActionSlot = slot>() + verify(exactly = 1) { appPluginManager.apply("kotlin-android") } + verify(exactly = 1) { plugin1Manager.apply("kotlin-android") } + } - every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit - every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit + @Test + fun `logs KGP warning when KGP attempts to but fails to apply`( + @TempDir tempDir: Path + ) { + val testProject = + setupTest( + tempDir = tempDir, + agpVersion = templateAgpVersion, + appConfig = + SubprojectConfig( + "app", + declarativelyAppliedPlugins = listOf("com.android.application") + ), + pluginConfigs = + listOf( + SubprojectConfig( + "plugin", + declarativelyAppliedPlugins = listOf("com.android.library") + ) + ) + ) - detectApplyingKotlinGradlePlugin(appProject) + val appPluginManager = testProject.appPluginManager + val plugin1Manager = testProject.plugin1Manager - verify { rootProject.subprojects(capture(subprojectsActionSlot)) } - subprojectsActionSlot.captured.execute(appProject) - subprojectsActionSlot.captured.execute(pluginProjectOne) - subprojectsActionSlot.captured.execute(pluginProjectTwo) + every { appPluginManager.apply("kotlin-android") } throws Exception("KGP not on classpath") + every { plugin1Manager.apply("kotlin-android") } throws Exception("KGP not on classpath") - verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } - projectsEvaluatedActionSlot.captured.execute(mockGradle) + executeDetectApplyingKotlinGradlePlugin(testProject) - verify { - mockLogger.error( - """ - WARNING: Your Android app project: app located at: ${appBuildGradleFile.absolutePath} - applies the Kotlin Gradle Plugin, which will cause build failures in future versions of Flutter. - Please migrate your app to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_APPS - - """.trimIndent() - ) - } + verify(exactly = 0) { + mockLogger.error(any()) + } - verify { - mockLogger.error( - """ - WARNING: Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP): plugin1, plugin2 - Future versions of Flutter will fail to build if your app uses plugins that apply KGP. - - Please check the changelogs of these plugins and upgrade to a version that supports Built-in Kotlin. - If no such version exists, report the issue to the plugin. If necessary, here is a guide on filing - an issue against a plugin: $BUILT_IN_KOTLIN_DOCS_TO_REPORT_UNMIGRATED_PLUGINS - - If you are a plugin author, please migrate your plugin to Built-in Kotlin using this guide: $BUILT_IN_KOTLIN_DOCS_FOR_PLUGINS - """.trimIndent() - ) + verify { + mockLogger.quiet( + match { + it.contains("Applying the Kotlin Android Plugin (KGP) was unsuccessful") && + it.contains("ensure KGP is declared in the root plugins block") && + it.contains(BUILT_IN_KOTLIN_DOCS) + } + ) + } } - - verify(exactly = 0) { appProjectPluginManager.apply("kotlin-android") } - verify(exactly = 0) { pluginProjectOnePluginManager.apply("kotlin-android") } - verify(exactly = 0) { pluginProjectTwoPluginManager.apply("kotlin-android") } } - @Test - fun `does not log when migrated to Built-in Kotlin`( - @TempDir tempDir: Path - ) { - val appDir = tempDir.resolve("app").toFile().apply { mkdirs() } - val appBuildGradleFile = - File(appDir, "build.gradle").apply { - createNewFile() - writeText( - """ - apply plugin: 'com.android.application' - """.trimIndent() + @Nested + inner class TestLogWarningsWhenAGPIsLessThan9 { + @Test + fun `does not log warnings when migrated to built-in kotlin`( + @TempDir tempDir: Path + ) { + val testProject = + setupTest( + tempDir = tempDir, + agpVersion = errorAgpVersion, + appConfig = + SubprojectConfig( + "app", + declarativelyAppliedPlugins = listOf("com.android.application") + ), + pluginConfigs = + listOf( + SubprojectConfig( + "plugin", + declarativelyAppliedPlugins = listOf("com.android.library") + ) + ) ) - } - val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() } - val pluginBuildGradleFile = - File(pluginDir, "build.gradle").apply { - createNewFile() - writeText( - """ - apply plugin: 'com.android.library' - """.trimIndent() - ) - } + val appPluginManager = testProject.appPluginManager + val plugin1Manager = testProject.plugin1Manager - val rootProject = mockk() - val mockGradle = mockk() - val mockLogger = mockk(relaxed = true) + executeDetectApplyingKotlinGradlePlugin(testProject) - val appProjectPluginManager = mockk(relaxed = true) - val pluginProjectPluginManager = mockk(relaxed = true) + // No warnings should be logged because AGP version is < 9. + verify(exactly = 0) { mockLogger.error(any()) } - val appProject = - createMockSubproject( - tempDir = tempDir, - buildFile = appBuildGradleFile, - projectName = "app", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = appProjectPluginManager - ) + // KGP is still applied in case the entire project has been migrated. + verify(exactly = 1) { appPluginManager.apply("kotlin-android") } + verify(exactly = 1) { plugin1Manager.apply("kotlin-android") } + } - val pluginProject = - createMockSubproject( - tempDir = tempDir, - buildFile = pluginBuildGradleFile, - projectName = "plugin", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = pluginProjectPluginManager - ) + @Test + fun `does not log warnings when KGP is applied`( + @TempDir tempDir: Path + ) { + val testProject = + setupTest( + tempDir = tempDir, + agpVersion = errorAgpVersion, + appConfig = + SubprojectConfig( + "app", + declarativelyAppliedPlugins = listOf("com.android.application", "kotlin-android") + ), + pluginConfigs = + listOf( + SubprojectConfig( + "plugin", + declarativelyAppliedPlugins = listOf("com.android.library", "kotlin-android") + ) + ) + ) - val subprojectsActionSlot = slot>() - val projectsEvaluatedActionSlot = slot>() + val appPluginManager = testProject.appPluginManager + val plugin1Manager = testProject.plugin1Manager - every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit - every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit + executeDetectApplyingKotlinGradlePlugin(testProject) - detectApplyingKotlinGradlePlugin(appProject) + // No warnings should be logged because AGP version is < 9. + verify(exactly = 0) { mockLogger.error(any()) } - verify { rootProject.subprojects(capture(subprojectsActionSlot)) } - subprojectsActionSlot.captured.execute(appProject) - subprojectsActionSlot.captured.execute(pluginProject) + verify(exactly = 0) { appPluginManager.apply("kotlin-android") } + verify(exactly = 0) { plugin1Manager.apply("kotlin-android") } + } - verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } - projectsEvaluatedActionSlot.captured.execute(mockGradle) + @Test + fun `logs KGP warning when KGP attempts to but fails to apply`( + @TempDir tempDir: Path + ) { + val testProject = + setupTest( + tempDir = tempDir, + agpVersion = errorAgpVersion, + appConfig = + SubprojectConfig( + "app", + declarativelyAppliedPlugins = listOf("com.android.application") + ), + pluginConfigs = + listOf( + SubprojectConfig( + "plugin", + declarativelyAppliedPlugins = listOf("com.android.library") + ) + ) + ) - verify(exactly = 0) { - mockLogger.error(any()) - } + val appPluginManager = testProject.appPluginManager + val plugin1Manager = testProject.plugin1Manager - verify(exactly = 1) { appProjectPluginManager.apply("kotlin-android") } - verify(exactly = 1) { pluginProjectPluginManager.apply("kotlin-android") } - } + every { appPluginManager.apply("kotlin-android") } throws Exception("KGP not on classpath") + every { plugin1Manager.apply("kotlin-android") } throws Exception("KGP not on classpath") - @Test - fun `logs when KGP is applied but fails to apply`( - @TempDir tempDir: Path - ) { - val appDir = tempDir.resolve("app").toFile().apply { mkdirs() } - val appBuildGradleFile = - File(appDir, "build.gradle").apply { - createNewFile() - writeText( - """ - plugins { - id("com.android.application") - } - """.trimIndent() - ) + executeDetectApplyingKotlinGradlePlugin(testProject) + + verify(exactly = 0) { + mockLogger.error(any()) } - val pluginDir = tempDir.resolve("plugin").toFile().apply { mkdirs() } - val pluginBuildGradleFile = - File(pluginDir, "build.gradle").apply { - createNewFile() - writeText( - """ - plugins { - id("com.android.library") + verify { + mockLogger.quiet( + match { + it.contains("Applying the Kotlin Android Plugin (KGP) was unsuccessful") && + it.contains("ensure KGP is declared in the root plugins block") && + it.contains(BUILT_IN_KOTLIN_DOCS) } - """.trimIndent() ) } - - val rootProject = mockk() - val mockGradle = mockk() - val mockLogger = mockk(relaxed = true) - - val appProjectPluginManager = mockk(relaxed = true) - val pluginProjectPluginManager = mockk(relaxed = true) - - val appProject = - createMockSubproject( - tempDir = tempDir, - buildFile = appBuildGradleFile, - projectName = "app", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = appProjectPluginManager - ) - - val pluginProject = - createMockSubproject( - tempDir = tempDir, - buildFile = pluginBuildGradleFile, - projectName = "plugin", - mockLogger = mockLogger, - rootProjectMock = rootProject, - gradleMock = mockGradle, - pluginManager = pluginProjectPluginManager - ) - - val subprojectsActionSlot = slot>() - val projectsEvaluatedActionSlot = slot>() - - every { rootProject.subprojects(capture(subprojectsActionSlot)) } returns Unit - every { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } returns Unit - - every { appProjectPluginManager.apply("kotlin-android") } throws Exception("KGP not on classpath") - every { pluginProjectPluginManager.apply("kotlin-android") } throws Exception("KGP not on classpath") - - detectApplyingKotlinGradlePlugin(appProject) - - verify { rootProject.subprojects(capture(subprojectsActionSlot)) } - subprojectsActionSlot.captured.execute(appProject) - subprojectsActionSlot.captured.execute(pluginProject) - - verify { mockGradle.projectsEvaluated(capture(projectsEvaluatedActionSlot)) } - projectsEvaluatedActionSlot.captured.execute(mockGradle) - - verify(exactly = 0) { - mockLogger.error(any()) - } - - verify { - mockLogger.quiet( - """ - Applying the Kotlin Android Plugin (KGP) was unsuccessful. KGP was not found on the classpath. - If your project uses Kotlin, ensure KGP is declared in the root plugins block. - For more details check: $BUILT_IN_KOTLIN_DOCS - """.trimIndent() - ) } } } - - private fun createMockSubproject( - tempDir: Path, - buildFile: File, - projectName: String, - mockLogger: Logger, - rootProjectMock: Project? = null, - gradleMock: Gradle? = null, - pluginManager: PluginManager - ): Project { - val projectDir = tempDir.resolve(projectName).toFile().apply { mkdirs() } - - val project = mockk() - every { project.name } returns projectName - every { project.projectDir } returns projectDir - every { project.buildFile } returns buildFile - every { project.logger } returns mockLogger - every { project.pluginManager } returns pluginManager - - if (rootProjectMock != null) every { project.rootProject } returns rootProjectMock - if (gradleMock != null) every { project.gradle } returns gradleMock - - return project - } } // forceNdkDownload diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index 2ea734ff6d705..6535e950a6f9d 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart @@ -671,6 +671,11 @@ class AndroidDevice extends Device { 'enable-vulkan-validation', 'true', ], + if (debuggingOptions.enableHcpp) ...[ + '--ez', + 'enable-hcpp-and-surface-control', + 'true', + ], if (debuggingOptions.debuggingEnabled) ...[ if (debuggingOptions.buildInfo.isDebug) ...[ ...['--ez', 'enable-checked-mode', 'true'], @@ -687,11 +692,6 @@ class AndroidDevice extends Device { 'dart-flags', debuggingOptions.dartFlags, ], - if (debuggingOptions.enableHcpp) ...[ - '--ez', - 'enable-hcpp-and-surface-control', - 'true', - ], if (debuggingOptions.useTestFonts) ...['--ez', 'use-test-fonts', 'true'], if (debuggingOptions.verboseSystemLogs) ...['--ez', 'verbose-logging', 'true'], if (userIdentifier != null) ...['--user', userIdentifier], diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index 068732f6efa19..1c786709848be 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -29,6 +29,7 @@ import '../convert.dart'; import '../flutter_manifest.dart'; import '../globals.dart' as globals; import '../project.dart'; +import '../shorebird/android_build_trace_session.dart'; import 'android_builder.dart'; import 'android_studio.dart'; import 'gradle_errors.dart'; @@ -278,6 +279,7 @@ class AndroidGradleBuilder implements AndroidBuilder { VoidCallback? postRunTask, int? maxRetries, _OutputParser? outputParser, + void Function(Process process)? onStart, }) async { final bool usesAndroidX = isAppUsingAndroidX(project.android.hostAppGradleRoot); final String? agpVersion = gradle.getAgpVersion( @@ -354,6 +356,7 @@ class AndroidGradleBuilder implements AndroidBuilder { allowReentrantFlutter: true, environment: _java?.environment, mapFunction: consumeLog, + onStart: onStart, ); } on ProcessException catch (exception) { consumeLog(exception.toString()); @@ -482,7 +485,12 @@ class AndroidGradleBuilder implements AndroidBuilder { return; } - // Assembly work starts here. + final AndroidBuildTraceSession? traceSession = AndroidBuildTraceSession.maybeStart( + androidBuildInfo, + _fileSystem, + project.android.buildDirectory, + ); + final BuildInfo buildInfo = androidBuildInfo.buildInfo; final String assembleTask = isBuildingBundle ? getBundleTaskFor(buildInfo) @@ -559,6 +567,7 @@ class AndroidGradleBuilder implements AndroidBuilder { } } options.addAll(androidBuildInfo.buildInfo.toGradleConfig()); + options.addAll(traceSession?.extraGradleOptions(assembleTask) ?? const []); if (buildInfo.fileSystemRoots.isNotEmpty) { options.add('-Pfilesystem-roots=${buildInfo.fileSystemRoots.join('|')}'); } @@ -572,8 +581,10 @@ class AndroidGradleBuilder implements AndroidBuilder { final int exitCode = await _runGradleTask( assembleTask, preRunTask: () { + traceSession?.onGradleAboutToStart(); sw = Stopwatch()..start(); }, + onStart: (process) => traceSession?.onGradleSpawn(process), postRunTask: () { final Duration elapsedDuration = sw.elapsed; _analytics.send( @@ -591,13 +602,21 @@ class AndroidGradleBuilder implements AndroidBuilder { gradleExecutablePath: gradleExecutablePath, ); + traceSession?.onGradleFinished(assembleTask); + if (exitCode != 0) { + traceSession?.abortOnGradleFailure(); throwToolExit( 'Gradle task $assembleTask failed with exit code $exitCode', exitCode: exitCode, ); } + traceSession?.finish( + buildTarget: isBuildingBundle ? 'appbundle' : 'apk', + printStatus: _logger.printStatus, + ); + if (isBuildingBundle) { final File bundleFile = findBundleFile(project, buildInfo, _logger, _analytics); @@ -719,10 +738,26 @@ class AndroidGradleBuilder implements AndroidBuilder { } if (!(result.stdout.contains('libapp.so.sym') || result.stdout.contains('libapp.so.dbg'))) { - _logger.printTrace( - 'libapp.so.sym or libapp.so.dbg not present when checking final appbundle for debug symbols.', + // Shorebird-specific: demote upstream's fatal check to a warning. + // + // Upstream Flutter PR #181275 (3.44) inverted libapp.so strip + // responsibility โ€” Flutter stopped stripping it, AGP is expected to, + // and this check fails the build if `.sym/.dbg` is absent. Shorebird + // flows that either keep libapp.so unstripped (legacy fixtures with + // `keepDebugSymbols.add("**/libapp.so")`) or pre-strip via + // `--extra-gen-snapshot-options=--strip` (obfuscated builds) both + // produce AABs without `libapp.so.sym`. Returning `true` here lets the + // build succeed; the trade-off is that Play Console won't get Dart + // crash symbols for these AABs. New apps should remove the legacy + // `keepDebugSymbols` line to re-enable AGP-side stripping. + _logger.printWarning( + 'libapp.so.sym or libapp.so.dbg not present when checking final ' + 'appbundle for debug symbols. Play Console will not receive Dart ' + 'crash symbols for this build. To enable, remove ' + '`packaging.jniLibs.keepDebugSymbols.add("**/libapp.so")` from ' + 'android/app/build.gradle.kts.', ); - return false; + return true; } return true; diff --git a/packages/flutter_tools/lib/src/android/gradle_errors.dart b/packages/flutter_tools/lib/src/android/gradle_errors.dart index 6ee6620d5acd8..594e09261c448 100644 --- a/packages/flutter_tools/lib/src/android/gradle_errors.dart +++ b/packages/flutter_tools/lib/src/android/gradle_errors.dart @@ -661,12 +661,13 @@ final missingNdkSourcePropertiesFile = GradleHandledError( eventLabel: 'ndk-missing-source-properties-file', ); -// TODO(jesswon): Remove this constant and its usages once AGP 9 is supported in the ecosystem: https://github.com/flutter/flutter/issues/181383 -const String _kAgp9WarningAndMigrationPrompt = ''' -Please do not upgrade your Flutter app on Android to AGP 9 as migrating plugins to AGP 9 -and Flutter apps on AGP 9 using plugins is not yet supported. For more details, see https://github.com/flutter/flutter/issues/181383. -\nTo proceed with the AGP 9 migration despite this warning: - '''; +/// The URL for documentation on migrating to built-in Kotlin. +const String kMigrateToBuiltInKotlinDocsUrl = + 'https://docs.flutter.dev/release/breaking-changes/migrate-to-built-in-kotlin'; + +/// The URL for documentation on opting out of the new AGP DSL. +const String kOptOutOfNewDslDocsUrl = + 'https://developer.android.com/build/releases/agp-9-0-0-release-notes'; /// Handler when applying the kotlin-android plugin results in a build failure. This failure occurs when /// using AGP 9+ because built-in Kotlin has become the default behavior. @@ -679,14 +680,11 @@ final applyingKotlinAndroidPluginErrorHandler = GradleHandledError( }, handler: ({required String line, required FlutterProject project, required bool usesAndroidX}) async { - final File appGradleFile = project.android.appGradleFile; - globals.printBox( - ''' -${globals.logger.terminal.warningMark} $_kAgp9WarningAndMigrationPrompt -Starting AGP 9+, the default has become built-in Kotlin. This results in a build failure when applying the kotlin-android plugin at ${appGradleFile.path}. -\nTo resolve this, migrate to built-in Kotlin. For instructions on how to migrate, see: https://docs.flutter.dev/release/breaking-changes/migrate-to-agp-9''', - title: _boxTitle, - ); + globals.printBox(''' +${globals.logger.terminal.warningMark} Starting AGP 9+, the default has become built-in Kotlin. +This results in a build failure when applying the kotlin-android plugin. +To resolve this, migrate to built-in Kotlin. +\nFor instructions on how to migrate, see: $kMigrateToBuiltInKotlinDocsUrl''', title: _boxTitle); return GradleBuildStatus.exit; }, @@ -707,9 +705,10 @@ final useNewAgpDslErrorHandler = GradleHandledError( final File appGradleFile = project.android.appGradleFile; globals.printBox( ''' -${globals.logger.terminal.warningMark} $_kAgp9WarningAndMigrationPrompt -Starting AGP 9+, only the new DSL interface will be read. This results in a build failure when applying the Flutter Gradle plugin at ${appGradleFile.path}. -\nTo resolve this update flutter or opt out of `android.newDsl`. For instructions on how to opt out, see: https://docs.flutter.dev/release/breaking-changes/migrate-to-agp-9 +${globals.logger.terminal.warningMark} Starting AGP 9+, only the new DSL interface will be read. +This results in a build failure when applying the Flutter Gradle plugin at ${appGradleFile.path}. +\nTo resolve this update flutter or opt out of `android.newDsl`. +For instructions on how to opt out, see: $kOptOutOfNewDslDocsUrl \nIf you are not upgrading to AGP 9+, run `flutter analyze --suggestions` to check for incompatible dependencies.''', title: _boxTitle, ); diff --git a/packages/flutter_tools/lib/src/android/gradle_utils.dart b/packages/flutter_tools/lib/src/android/gradle_utils.dart index 928a45c4ba878..bd271d58b0254 100644 --- a/packages/flutter_tools/lib/src/android/gradle_utils.dart +++ b/packages/flutter_tools/lib/src/android/gradle_utils.dart @@ -33,22 +33,23 @@ import 'android_sdk.dart'; // * Gradle warn version in packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt // * Gradle test constants in packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt // See https://gradle.org/releases -const templateDefaultGradleVersion = '8.14'; +const templateDefaultGradleVersion = '9.1.0'; // When bumping, also update: // * AGP version constants in packages/flutter_tools/gradle/build.gradle.kts // * AGP warn version in packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt // * AGP test constants in packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt +// * AGP test constants in packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt // See https://mvnrepository.com/artifact/com.android.tools.build/gradle -const templateAndroidGradlePluginVersion = '8.11.1'; -const templateAndroidGradlePluginVersionForModule = '8.11.1'; +const templateAndroidGradlePluginVersion = '9.0.1'; +const templateAndroidGradlePluginVersionForModule = '9.0.1'; // When bumping, also update: // * KGP version constants in packages/flutter_tools/gradle/build.gradle.kts // * KGP warn version in packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt // * KGP jvm constant in packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt // See https://kotlinlang.org/docs/releases.html#release-details -const templateKotlinGradlePluginVersion = '2.2.20'; +const templateKotlinGradlePluginVersion = '2.3.20'; // The Flutter Gradle Plugin is only applied to app projects, and modules that // are built from source using (`include_flutter.groovy`). The remaining @@ -81,29 +82,29 @@ const oneMajorVersionHigherJavaVersion = '26'; // // Supported here means supported by the tooling for // flutter analyze --suggestions and does not imply broader flutter support. -const maxKnownAndSupportedGradleVersion = '9.1.0'; +const maxKnownAndSupportedGradleVersion = '9.3.1'; // Update this with new KGP versions come out including minor versions. // // Supported here means supported by the tooling for // flutter analyze --suggestions and does not imply broader flutter support. -const maxKnownAndSupportedKgpVersion = '2.3.10'; +const maxKnownAndSupportedKgpVersion = '2.3.20'; // Update this when new versions of AGP come out. // // Supported here means tooling is aware of this version's Java <-> AGP // compatibility. @visibleForTesting -const maxKnownAndSupportedAgpVersion = '9.0'; +const maxKnownAndSupportedAgpVersion = '9.1'; // Update this when new versions of AGP with Kotlin support come out. // // Supported here means supported by the tooling for // flutter analyze --suggestions and does not imply broader flutter support. -const maxKnownAgpVersionWithFullKotlinSupport = '8.13.0'; +const maxKnownAgpVersionWithFullKotlinSupport = '9.0.1'; // Update this when new versions of AGP come out. -const maxKnownAgpVersion = '9.0'; +const maxKnownAgpVersion = '9.1'; // Supported here means tooling is aware of this versions // Java <-> AGP compatibility and does not imply broader flutter support. @@ -1113,7 +1114,7 @@ String getGradleVersionFor(String agpV) { GradleForAgp(agpMin: '8.11.0', agpMax: '8.11.99', minRequiredGradle: '8.13'), GradleForAgp(agpMin: '8.12.0', agpMax: '8.12.99', minRequiredGradle: '8.13'), GradleForAgp(agpMin: '8.13.0', agpMax: '8.13.99', minRequiredGradle: '8.13'), - GradleForAgp(agpMin: '9.0', agpMax: '9.0.99', minRequiredGradle: '9.0.0'), + GradleForAgp(agpMin: '9.0', agpMax: '9.0.99', minRequiredGradle: '9.1.0'), // Assume if AGP is newer than this code knows about return the highest gradle // version we know about. GradleForAgp( diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart index 2fba7b3226fd5..4f81c1e06a41c 100644 --- a/packages/flutter_tools/lib/src/asset.dart +++ b/packages/flutter_tools/lib/src/asset.dart @@ -1104,7 +1104,7 @@ class ManifestAssetBundle implements AssetBundle { result.removeWhere((_Asset asset, List<_Asset> variants) { if (!asset.matchesFlavor(flavor)) { - _logger.printWarning( + _logger.printTrace( 'Skipping assets entry "${asset.entryUri.path}" since ' 'its configured flavor(s) did not match the provided flavor (if any).\n' 'Configured flavors: ${asset.flavors.join(', ')}\n', @@ -1112,7 +1112,7 @@ class ManifestAssetBundle implements AssetBundle { return true; } if (!asset.matchesPlatform(targetPlatform)) { - _logger.printWarning( + _logger.printTrace( 'Skipping assets entry "${asset.entryUri.path}" since ' 'its configured platform(s) did not match the target platform.\n' 'Configured platforms: ${asset.platforms.join(', ')}\n' diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart index fa7ffb45912ee..ec06f9ee3efe0 100644 --- a/packages/flutter_tools/lib/src/base/build.dart +++ b/packages/flutter_tools/lib/src/base/build.dart @@ -2,13 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io' show Platform; + import 'package:process/process.dart'; import '../artifacts.dart'; import '../build_info.dart'; import '../darwin/darwin.dart'; import '../macos/xcode.dart'; - import 'file_system.dart'; import 'logger.dart'; import 'process.dart'; @@ -30,7 +31,9 @@ class GenSnapshot { required Artifacts artifacts, required ProcessManager processManager, required Logger logger, + required FileSystem fileSystem, }) : _artifacts = artifacts, + _fileSystem = fileSystem, _processUtils = ProcessUtils(logger: logger, processManager: processManager); final Artifacts _artifacts; @@ -55,6 +58,75 @@ class GenSnapshot { ' See dartbug.com/30524 for more information.', }; + /// Returns the path to an analyze_snapshot binary that matches gen_snapshot's + /// SDK version, or null if no matching binary can be found. + /// + /// gen_snapshot and analyze_snapshot share a snapshot-format hash baked into + /// each binary; if the hashes disagree, analyze_snapshot rejects gen_snapshot's + /// output with "Wrong full snapshot version" at parse time. Pre-existing binary + /// layouts (e.g. an older `universal/analyze_snapshot_` left behind from + /// a previous BUILD.gn revision) can shadow the freshly built one. Resolve by + /// probing every candidate location and returning only the one whose + /// `--sdk_version` matches gen_snapshot's `--version` output. + String? getAnalyzeSnapshotPath(SnapshotType snapshotType, DarwinArch? darwinArch) { + final Artifact genSnapshotArtifact; + final String analyzeName; + if (snapshotType.platform == TargetPlatform.ios || + snapshotType.platform == TargetPlatform.darwin) { + if (darwinArch == DarwinArch.arm64) { + genSnapshotArtifact = Artifact.genSnapshotArm64; + analyzeName = 'analyze_snapshot_arm64'; + } else { + genSnapshotArtifact = Artifact.genSnapshotX64; + analyzeName = 'analyze_snapshot_x64'; + } + } else { + genSnapshotArtifact = Artifact.genSnapshot; + analyzeName = 'analyze_snapshot'; + } + final String genSnapshotPath = getSnapshotterPath(snapshotType, genSnapshotArtifact); + final String? genVersion = _probeSdkVersion(genSnapshotPath, '--version'); + final String dir = _fileSystem.path.dirname(genSnapshotPath); + // Cached SDK layout puts analyze_snapshot alongside gen_snapshot. Local + // engine layout resolves gen_snapshot via .../universal/gen_snapshot_arm64 + // while analyze_snapshot lives one level up at the build-dir root. Probe + // both. If we successfully read gen_snapshot's version, accept only a + // candidate whose --sdk_version matches; otherwise fall back to the first + // existing candidate (better than failing closed when the version probe + // itself misbehaves). + for (final candidate in [ + _fileSystem.path.join(dir, analyzeName), + _fileSystem.path.join(dir, '..', analyzeName), + ]) { + if (!_fileSystem.file(candidate).existsSync()) { + continue; + } + if (genVersion == null) { + return candidate; + } + final String? candidateVersion = _probeSdkVersion(candidate, '--sdk_version'); + if (candidateVersion == genVersion) { + return candidate; + } + } + return null; + } + + /// Runs [binary] with [versionFlag] and returns the trimmed stderr output, + /// or null if the binary couldn't be invoked or printed nothing. + /// gen_snapshot and analyze_snapshot both write their version line to stderr. + String? _probeSdkVersion(String binary, String versionFlag) { + try { + final RunResult result = _processUtils.runSync([binary, versionFlag]); + final String stderr = result.stderr.trim(); + return stderr.isEmpty ? null : stderr; + } on Exception { + return null; + } + } + + final FileSystem _fileSystem; + Future run({ required SnapshotType snapshotType, DarwinArch? darwinArch, @@ -96,15 +168,18 @@ class AOTSnapshotter { }) : _logger = logger, _fileSystem = fileSystem, _xcode = xcode, + _processUtils = ProcessUtils(logger: logger, processManager: processManager), _genSnapshot = GenSnapshot( artifacts: artifacts, processManager: processManager, logger: logger, + fileSystem: fileSystem, ); final Logger _logger; final FileSystem _fileSystem; final Xcode _xcode; + final ProcessUtils _processUtils; final GenSnapshot _genSnapshot; /// Builds an architecture-specific ahead-of-time compiled snapshot of the specified script. @@ -130,7 +205,27 @@ class AOTSnapshotter { final Directory outputDir = _fileSystem.directory(outputPath); outputDir.createSync(recursive: true); - final genSnapshotArgs = ['--deterministic']; + // Currently we only use the linker on iOS, but we will eventually split out + // the concept of "optimizes patch snapshot" from "uses linker" and probably + // only uses the linker on iOS, but optimize patch snapshots everywhere. + // TODO(eseidel): TargetPlatform.darwin doesn't use the linker. + bool usesLinker = (platform == TargetPlatform.ios || platform == TargetPlatform.darwin); + final dumpLinkInfoArgs = [ + // Shorebird dumps the class table information during snapshot compilation which is later used during linking. + '--print_class_table_link_debug_info_to=${_fileSystem.path.join(outputDir.parent.path, 'App.class_table.json')}', + '--print_class_table_link_info_to=${_fileSystem.path.join(outputDir.parent.path, 'App.ct.link')}', + '--print_field_table_link_debug_info_to=${_fileSystem.path.join(outputDir.parent.path, 'App.field_table.json')}', + '--print_field_table_link_info_to=${_fileSystem.path.join(outputDir.parent.path, 'App.ft.link')}', + '--print_dispatch_table_link_debug_info_to=${_fileSystem.path.join(outputDir.parent.path, 'App.dispatch_table.json')}', + '--print_dispatch_table_link_info_to=${_fileSystem.path.join(outputDir.parent.path, 'App.dt.link')}', + ]; + + final genSnapshotArgs = [ + // Shorebird uses --deterministic to improve snapshot stability and increase linking. + '--deterministic', + // Only save LinkInfo if we're using the linker. + if (usesLinker) ...dumpLinkInfoArgs, + ]; final bool targetingApplePlatform = platform == TargetPlatform.ios || platform == TargetPlatform.darwin; @@ -140,13 +235,6 @@ class AOTSnapshotter { buildMode == BuildMode.profile || buildMode == BuildMode.release; _logger.printTrace('extractAppleDebugSymbols = $extractAppleDebugSymbols'); - final bool targetingAndroidPlatform = - platform == TargetPlatform.android || - platform == TargetPlatform.android_arm || - platform == TargetPlatform.android_arm64 || - platform == TargetPlatform.android_x64; - _logger.printTrace('targetingAndroidPlatform = $targetingAndroidPlatform'); - // We strip snapshot by default, but allow to suppress this behavior // by supplying --no-strip in extraGenSnapshotOptions. var shouldStrip = true; @@ -161,37 +249,11 @@ class AOTSnapshotter { } } - String aotSharedLibrary = _fileSystem.path.join(outputDir.path, 'app.so'); - String? frameworkPath; + final String assembly = _fileSystem.path.join(outputDir.path, 'snapshot_assembly.S'); if (targetingApplePlatform) { - // On iOS and macOS, we use Xcode to compile the snapshot into a dynamic - // library that the end-developer can link into their app. - const frameworkName = 'App.framework'; - if (!quiet) { - final String targetArch = darwinArch!.name; - _logger.printStatus('Building $frameworkName for $targetArch...'); - } - frameworkPath = _fileSystem.path.join(outputPath, frameworkName); - _fileSystem.directory(frameworkPath).createSync(recursive: true); - - const frameworkSnapshotName = 'App'; - aotSharedLibrary = _fileSystem.path.join(frameworkPath, frameworkSnapshotName); - final String relocatableObject = _fileSystem.path.join(outputPath, 'app.o'); - // When the minimum version is updated, remember to update - // template MinimumOSVersion. - // https://github.com/flutter/flutter/pull/62902 - final minOSVersion = platform == TargetPlatform.ios - ? FlutterDarwinPlatform.ios.deploymentTarget().toString() - : FlutterDarwinPlatform.macos.deploymentTarget().toString(); - genSnapshotArgs.addAll([ - '--snapshot_kind=app-aot-macho-dylib', - '--macho=$aotSharedLibrary', - '--macho-object=$relocatableObject', - '--macho-min-os-version=$minOSVersion', - '--macho-rpath=@executable_path/Frameworks,@loader_path/Frameworks', - '--macho-install-name=@rpath/$frameworkName/$frameworkSnapshotName', - ]); + genSnapshotArgs.addAll(['--snapshot_kind=app-aot-assembly', '--assembly=$assembly']); } else { + final String aotSharedLibrary = _fileSystem.path.join(outputDir.path, 'app.so'); genSnapshotArgs.addAll(['--snapshot_kind=app-aot-elf', '--elf=$aotSharedLibrary']); } @@ -203,9 +265,6 @@ class AOTSnapshotter { if (stripAfterBuild) { _logger.printTrace('Will strip AOT snapshot manually after build and dSYM generation.'); } - } else if (targetingAndroidPlatform) { - stripAfterBuild = false; - // When building for Android, we let AGP handle stripping of debug symbols. } else { stripAfterBuild = false; if (shouldStrip) { @@ -246,6 +305,39 @@ class AOTSnapshotter { genSnapshotArgs.add(mainPath); final snapshotType = SnapshotType(platform, buildMode); + + final int ddMaxBytes = _readDdMaxBytes(); + // DD pass requires: + // 1. an analyze_snapshot binary whose --sdk_version matches gen_snapshot's + // --version, and + // 2. a single non-racing ELF output path per build invocation. + // + // macOS builds fail both. (1) The shipped `analyze_snapshot` is a single + // host-arch binary; when targeting x64 from an arm64 host, gen_snapshot_x64 + // reports `macos_simx64` while analyze_snapshot reports `macos_arm64` and + // the version-equality check rejects it. (2) Flutter spawns + // gen_snapshot_arm64 and gen_snapshot_x64 in parallel and both DD passes + // race on the same `App_dd_analysis.so` path under + // `.dart_tool/flutter_build//`. + // + // Skip DD on macOS until that work is done. Patches built without DD + // activation will compute DD on the fly when applied, which is the + // pre-1.6.99 behavior โ€” slightly worse link percentage but functional. + final bool ddPassSupported = platform != TargetPlatform.darwin; + if (ddMaxBytes > 0 && usesLinker && ddPassSupported) { + final int pass1Exit = await _runDdAnalysisPass( + snapshotType: snapshotType, + darwinArch: darwinArch, + outputDir: outputDir, + baseGenSnapshotArgs: genSnapshotArgs, + mainPath: mainPath, + ddMaxBytes: ddMaxBytes, + ); + if (pass1Exit != 0) { + return pass1Exit; + } + } + final int genSnapshotExitCode = await _genSnapshot.run( snapshotType: snapshotType, additionalArgs: genSnapshotArgs, @@ -256,38 +348,256 @@ class AOTSnapshotter { return genSnapshotExitCode; } + // On iOS and macOS, we use Xcode to compile the snapshot into a dynamic library that the + // end-developer can link into their app. if (targetingApplePlatform) { - if (extractAppleDebugSymbols) { - final RunResult dsymResult = await _xcode.dsymutil([ - '-o', - '$frameworkPath.dSYM', - aotSharedLibrary, - ]); - if (dsymResult.exitCode != 0) { + return _buildFramework( + appleArch: darwinArch!, + isIOS: platform == TargetPlatform.ios, + sdkRoot: sdkRoot, + assemblyPath: assembly, + outputPath: outputDir.path, + quiet: quiet, + stripAfterBuild: stripAfterBuild, + extractAppleDebugSymbols: extractAppleDebugSymbols, + ); + } else { + return 0; + } + } + + /// Reads the SHOREBIRD_DD_MAX_BYTES env var (preferring `dart-define` over + /// the process environment). Returns 0 if unset, malformed, or non-positive, + /// which signals "no DD pass." + int _readDdMaxBytes() { + const fromDefine = String.fromEnvironment('SHOREBIRD_DD_MAX_BYTES'); + final String? raw = fromDefine.isNotEmpty + ? fromDefine + : Platform.environment['SHOREBIRD_DD_MAX_BYTES']; + return int.tryParse(raw ?? '') ?? 0; + } + + /// Runs the DD analysis pass: gen_snapshot โ†’ ELF + DD identity, then + /// analyze_snapshot to compute the DD table, caller links, and slot mapping. + /// On success, mutates [baseGenSnapshotArgs] to add `--dd_slot_mapping=...` + /// before [mainPath] so the subsequent gen_snapshot run picks it up. + /// Returns the gen_snapshot pass-1 exit code (0 on success). + Future _runDdAnalysisPass({ + required SnapshotType snapshotType, + required DarwinArch? darwinArch, + required Directory outputDir, + required List baseGenSnapshotArgs, + required String mainPath, + required int ddMaxBytes, + }) async { + _logger.printTrace('DD 2-pass build: dd_max_bytes=$ddMaxBytes'); + + String linkPath(String name) => _fileSystem.path.join(outputDir.parent.path, name); + final String elfForAnalysis = linkPath('App_dd_analysis.so'); + final String ddIdentityPath = linkPath('App.dd_identity.link'); + final String ddTablePath = linkPath('App.dd.link'); + final String ddCallerLinksPath = linkPath('App.dd_callers.link'); + final String ddSlotMappingPath = linkPath('App.dd_slots.link'); + final String ddResolutionPath = linkPath('App.dd_resolution.tsv'); + + // Pass 1: build ELF for analysis + DD identity. Strip the existing snapshot + // kind/output args from the base set; mainPath must remain at the end. + final elfArgs = [ + ...baseGenSnapshotArgs.where( + (String a) => + a != mainPath && + !a.startsWith('--snapshot_kind=') && + !a.startsWith('--assembly=') && + !a.startsWith('--elf='), + ), + '--snapshot_kind=app-aot-elf', + '--elf=$elfForAnalysis', + '--print_dd_function_identity_to=$ddIdentityPath', + mainPath, + ]; + final int pass1Exit = await _genSnapshot.run( + snapshotType: snapshotType, + additionalArgs: elfArgs, + darwinArch: darwinArch, + ); + if (pass1Exit != 0) { + _logger.printError('DD pass 1 (ELF for analysis) failed with exit code $pass1Exit'); + return pass1Exit; + } + + final String? analyzeSnapshotPath = _genSnapshot.getAnalyzeSnapshotPath( + snapshotType, + darwinArch, + ); + if (analyzeSnapshotPath == null) { + _logger.printError( + 'DD pass: could not find an analyze_snapshot binary whose --sdk_version ' + 'matches gen_snapshot. The release will ship without DD activation; ' + 'patches against it will fall back to on-the-fly DD computation and ' + 'produce a structurally divergent snapshot (devastating link percentage). ' + 'Aborting the build instead.', + ); + _fileSystem.file(elfForAnalysis).deleteSync(); + return 1; + } + + final int ddTableExit = await _processUtils.stream([ + analyzeSnapshotPath, + '--compute_dd_table=$ddTablePath', + '--dd_caller_links=$ddCallerLinksPath', + '--dd_max_bytes=$ddMaxBytes', + elfForAnalysis, + ]); + if (ddTableExit != 0) { + _logger.printError( + 'DD pass: analyze_snapshot --compute_dd_table failed with exit code ' + '$ddTableExit. App.dd.link will not be produced and the release would ' + 'ship without DD activation.', + ); + _fileSystem.file(elfForAnalysis).deleteSync(); + return ddTableExit; + } + + final int ddSlotMappingExit = await _processUtils.stream([ + analyzeSnapshotPath, + '--compute_dd_slot_mapping=$ddSlotMappingPath', + '--dd_table_data=$ddTablePath', + '--dd_caller_links=$ddCallerLinksPath', + '--dd_function_identity=$ddIdentityPath', + elfForAnalysis, + ]); + if (ddSlotMappingExit != 0) { + _logger.printError( + 'DD pass: analyze_snapshot --compute_dd_slot_mapping failed with exit ' + 'code $ddSlotMappingExit. The DD slot mapping will not be available and ' + 'gen_snapshot pass 2 would emit a no-DD snapshot.', + ); + _fileSystem.file(elfForAnalysis).deleteSync(); + return ddSlotMappingExit; + } + + if (!_fileSystem.file(ddSlotMappingPath).existsSync()) { + _logger.printError( + 'DD pass: analyze_snapshot --compute_dd_slot_mapping reported success ' + 'but $ddSlotMappingPath was not produced.', + ); + _fileSystem.file(elfForAnalysis).deleteSync(); + return 1; + } + final int mainPathIndex = baseGenSnapshotArgs.indexOf(mainPath); + baseGenSnapshotArgs.insertAll(mainPathIndex, [ + '--dd_slot_mapping=$ddSlotMappingPath', + // Per-slot resolution outcome dump (TSV: slot, outcome, rewritten, + // name). Diagnostic for analyzing which slots got dropped during + // the resolver's run; ends up alongside the other supplement files + // and gets carried into the patch debug bundle. + '--print_dd_resolution_to=$ddResolutionPath', + ]); + _logger.printTrace('DD 2-pass build: added --dd_slot_mapping and --print_dd_resolution_to'); + + _fileSystem.file(elfForAnalysis).deleteSync(); + return 0; + } + + /// Builds an iOS or macOS framework at [outputPath]/App.framework from the assembly + /// source at [assemblyPath]. + Future _buildFramework({ + required DarwinArch appleArch, + required bool isIOS, + String? sdkRoot, + required String assemblyPath, + required String outputPath, + required bool quiet, + required bool stripAfterBuild, + required bool extractAppleDebugSymbols, + }) async { + final String targetArch = appleArch.name; + if (!quiet) { + _logger.printStatus('Building App.framework for $targetArch...'); + } + + final commonBuildOptions = [ + '-arch', + targetArch, + if (isIOS) + // When the minimum version is updated, remember to update + // template MinimumOSVersion. + // https://github.com/flutter/flutter/pull/62902 + '-miphoneos-version-min=${FlutterDarwinPlatform.ios.deploymentTarget()}', + if (sdkRoot != null) ...['-isysroot', sdkRoot], + ]; + + final String assemblyO = _fileSystem.path.join(outputPath, 'snapshot_assembly.o'); + + final RunResult compileResult = await _xcode.cc([ + ...commonBuildOptions, + '-c', + assemblyPath, + '-o', + assemblyO, + ]); + if (compileResult.exitCode != 0) { + _logger.printError( + 'Failed to compile AOT snapshot. Compiler terminated with exit code ${compileResult.exitCode}', + ); + return compileResult.exitCode; + } + + final String frameworkDir = _fileSystem.path.join(outputPath, 'App.framework'); + _fileSystem.directory(frameworkDir).createSync(recursive: true); + final String appLib = _fileSystem.path.join(frameworkDir, 'App'); + final linkArgs = [ + ...commonBuildOptions, + '-dynamiclib', + '-Xlinker', + '-rpath', + '-Xlinker', + '@executable_path/Frameworks', + '-Xlinker', + '-rpath', + '-Xlinker', + '@loader_path/Frameworks', + '-fapplication-extension', + '-install_name', + '@rpath/App.framework/App', + '-o', + appLib, + assemblyO, + ]; + + final RunResult linkResult = await _xcode.clang(linkArgs); + if (linkResult.exitCode != 0) { + _logger.printError( + 'Failed to link AOT snapshot. Linker terminated with exit code ${linkResult.exitCode}', + ); + return linkResult.exitCode; + } + + if (extractAppleDebugSymbols) { + final RunResult dsymResult = await _xcode.dsymutil([ + '-o', + '$frameworkDir.dSYM', + appLib, + ]); + if (dsymResult.exitCode != 0) { + _logger.printError( + 'Failed to generate dSYM - dsymutil terminated with exit code ${dsymResult.exitCode}', + ); + return dsymResult.exitCode; + } + + if (stripAfterBuild) { + // See https://www.unix.com/man-page/osx/1/strip/ for arguments + final RunResult stripResult = await _xcode.strip(['-x', appLib, '-o', appLib]); + if (stripResult.exitCode != 0) { _logger.printError( - 'Failed to generate dSYM - dsymutil terminated with exit code ${dsymResult.exitCode}', + 'Failed to strip debugging symbols from the generated AOT snapshot - strip terminated with exit code ${stripResult.exitCode}', ); - return dsymResult.exitCode; - } - - if (stripAfterBuild) { - // See https://www.unix.com/man-page/osx/1/strip/ for arguments - final RunResult stripResult = await _xcode.strip([ - '-x', - aotSharedLibrary, - '-o', - aotSharedLibrary, - ]); - if (stripResult.exitCode != 0) { - _logger.printError( - 'Failed to strip debugging symbols from the generated AOT snapshot - strip terminated with exit code ${stripResult.exitCode}', - ); - return stripResult.exitCode; - } + return stripResult.exitCode; } - } else { - assert(!stripAfterBuild); } + } else { + assert(!stripAfterBuild); } return 0; @@ -305,7 +615,6 @@ class AOTSnapshotter { TargetPlatform.darwin, TargetPlatform.linux_x64, TargetPlatform.linux_arm64, - TargetPlatform.linux_riscv64, TargetPlatform.windows_x64, TargetPlatform.windows_arm64, ].contains(platform); diff --git a/packages/flutter_tools/lib/src/base/error_handling_io.dart b/packages/flutter_tools/lib/src/base/error_handling_io.dart index 4a048813f4b18..8bbd81f372588 100644 --- a/packages/flutter_tools/lib/src/base/error_handling_io.dart +++ b/packages/flutter_tools/lib/src/base/error_handling_io.dart @@ -8,12 +8,14 @@ import 'dart:io' show Directory, File, + FileSystemEntity, Link, Process, ProcessException, ProcessResult, ProcessSignal, ProcessStartMode, + sleep, systemEncoding; import 'dart:typed_data'; @@ -31,13 +33,39 @@ import 'platform.dart'; // ToolExit and a message that is more clear than the FileSystemException by // itself. +/// On Windows this is error code 0: ERROR_SUCCESS. +const int kSystemCodeSuccess = 0; + +/// On Windows this is error code 1: ERROR_INVALID_FUNCTION. +const int kSystemCodeInvalidFunction = 1; + /// On Windows this is error code 2: ERROR_FILE_NOT_FOUND, and on /// macOS/Linux it is error code 2/ENOENT: No such file or directory. -const kSystemCodeCannotFindFile = 2; +const int kSystemCodeCannotFindFile = 2; /// On Windows this error is 3: ERROR_PATH_NOT_FOUND, and on /// macOS/Linux, it is error code 3/ESRCH: No such process. -const kSystemCodePathNotFound = 3; +const int kSystemCodePathNotFound = 3; + +/// On Windows this error is 5: ERROR_ACCESS_DENIED, and on +/// macOS/Linux, it is error code 13/EACCES or 1/EPERM: Permission denied. +const int kSystemCodeAccessDenied = 5; + +/// On Windows this is error code 32: ERROR_SHARING_VIOLATION. +const int kSystemCodeSharingViolation = 32; + +/// On Windows this is error code 33: ERROR_LOCK_VIOLATION. +const int kSystemCodeLockViolation = 33; + +/// On Windows this is error code 112: ERROR_DISK_FULL, and on +/// macOS/Linux, it is error code 28/ENOSPC: No space left on device. +const int kSystemCodeDeviceFull = 112; + +/// On Windows this is error code 1224: ERROR_USER_MAPPED_FILE. +const int kSystemCodeUserMappedSectionOpened = 1224; + +/// On Windows this is error code 1314: ERROR_PRIVILEGE_NOT_HELD. +const int kSystemCodePrivilegeNotHeld = 1314; /// A [FileSystem] that throws a [ToolExit] on certain errors. /// @@ -82,12 +110,24 @@ class ErrorHandlingFileSystem extends ForwardingFileSystem { /// This method should be preferred to checking if it exists and /// then deleting, because it handles the edge case where the file or directory /// is deleted by a different program between the two calls. + /// + /// Note: Disk presence is checked type-agnostically (followLinks: false) + /// to safely resolve and delete broken symlinks, sockets, or type-mismatched + /// folders from disk, preventing subsequent recreation failures. static bool deleteIfExists(FileSystemEntity entity, {bool recursive = false}) { - if (!entity.existsSync()) { + final FileSystemEntityType type = entity.fileSystem.typeSync(entity.path, followLinks: false); + if (type == .notFound) { return false; } + + final FileSystemEntity actualEntity = switch (type) { + .file => entity is File ? entity : entity.fileSystem.file(entity.path), + .directory => entity is Directory ? entity : entity.fileSystem.directory(entity.path), + .link => entity is Link ? entity : entity.fileSystem.link(entity.path), + _ => entity, + }; try { - entity.deleteSync(recursive: recursive); + actualEntity.deleteSync(recursive: recursive); } on FileSystemException catch (err) { // Certain error codes indicate the file could not be found. It could have // been deleted by a different program while the tool was running. @@ -100,9 +140,10 @@ class ErrorHandlingFileSystem extends ForwardingFileSystem { if (!codeCorrespondsToPathOrFileNotFound || _noExitOnFailure) { rethrow; } - if (entity.existsSync()) { + if (actualEntity.fileSystem.typeSync(actualEntity.path, followLinks: false) != + FileSystemEntityType.notFound) { throwToolExit( - 'Unable to delete file or directory at "${entity.path}". ' + 'Unable to delete file or directory at "${actualEntity.path}". ' 'This may be due to the project being in a read-only ' 'volume. Consider relocating the project and trying again.', ); @@ -117,9 +158,13 @@ class ErrorHandlingFileSystem extends ForwardingFileSystem { Directory get currentDirectory { try { return _runSync(() => directory(delegate.currentDirectory), platform: _platform); - } on FileSystemException catch (err) { + } on Exception catch (err) { // Special handling for OS error 2 for current directory only. - if (err.osError?.errorCode == kSystemCodeCannotFindFile) { + final bool isCannotFindFile = + (err is ToolExit && + (err.message?.contains('The file or directory could not be found') ?? false)) || + (err is FileSystemException && err.osError?.errorCode == kSystemCodeCannotFindFile); + if (isCannotFindFile) { throwToolExit( 'Unable to read current working directory. This can happen if the directory the ' 'Flutter tool was run from was moved or deleted.', @@ -342,6 +387,224 @@ class ErrorHandlingFile extends ForwardingFileSystemEntity with F return wrapFile(resultFile); } + @override + Future exists() async { + // ignore: avoid_slow_async_io + return _run(() => delegate.exists(), platform: _platform); + } + + @override + bool existsSync() { + return _runSync(() => delegate.existsSync(), platform: _platform); + } + + @override + Future create({bool recursive = false, bool exclusive = false}) async { + return _run( + () async => wrapFile(await delegate.create(recursive: recursive, exclusive: exclusive)), + platform: _platform, + failureMessage: 'Flutter failed to create file at "${delegate.path}"', + posixPermissionSuggestion: recursive + ? null + : _posixPermissionSuggestion([delegate.parent.path]), + ); + } + + @override + Future rename(String newPath) async { + return _run( + () async => wrapFile(await delegate.rename(newPath)), + platform: _platform, + failureMessage: 'Flutter failed to rename file at "${delegate.path}" to "$newPath"', + posixPermissionSuggestion: _posixPermissionSuggestion([ + delegate.path, + delegate.parent.path, + ]), + ); + } + + @override + File renameSync(String newPath) { + return _runSync( + () => wrapFile(delegate.renameSync(newPath)), + platform: _platform, + failureMessage: 'Flutter failed to rename file at "${delegate.path}" to "$newPath"', + posixPermissionSuggestion: _posixPermissionSuggestion([ + delegate.path, + delegate.parent.path, + ]), + ); + } + + @override + Future delete({bool recursive = false}) async { + return _run( + () async => wrapFile((await delegate.delete(recursive: recursive)) as io.File), + platform: _platform, + failureMessage: 'Flutter failed to delete file at "${delegate.path}"', + posixPermissionSuggestion: _posixPermissionSuggestion([delegate.path]), + ignoreErrorCodes: const [ + kSystemCodeCannotFindFile, + kSystemCodePathNotFound, + ], // enoent, kFileNotFound, kPathNotFound + ); + } + + @override + void deleteSync({bool recursive = false}) { + _runSync( + () => delegate.deleteSync(recursive: recursive), + platform: _platform, + failureMessage: 'Flutter failed to delete file at "${delegate.path}"', + posixPermissionSuggestion: _posixPermissionSuggestion([delegate.path]), + ignoreErrorCodes: const [kSystemCodeCannotFindFile, kSystemCodePathNotFound], + ); + } + + @override + Future stat() async { + return _run( + // ignore: avoid_slow_async_io + () => delegate.stat(), + platform: _platform, + failureMessage: 'Flutter failed to retrieve statistics of file at "${delegate.path}"', + ); + } + + @override + FileStat statSync() { + return _runSync( + () => delegate.statSync(), + platform: _platform, + failureMessage: 'Flutter failed to retrieve statistics of file at "${delegate.path}"', + ); + } + + @override + Future length() async { + return _run( + () => delegate.length(), + platform: _platform, + failureMessage: 'Flutter failed to retrieve length of file at "${delegate.path}"', + ); + } + + @override + int lengthSync() { + return _runSync( + () => delegate.lengthSync(), + platform: _platform, + failureMessage: 'Flutter failed to retrieve length of file at "${delegate.path}"', + ); + } + + @override + Future lastModified() async { + return _run( + // ignore: avoid_slow_async_io + () => delegate.lastModified(), + platform: _platform, + failureMessage: 'Flutter failed to retrieve last modified time of file at "${delegate.path}"', + ); + } + + @override + DateTime lastModifiedSync() { + return _runSync( + () => delegate.lastModifiedSync(), + platform: _platform, + failureMessage: 'Flutter failed to retrieve last modified time of file at "${delegate.path}"', + ); + } + + @override + Future setLastModified(DateTime time) async { + return _run( + () => delegate.setLastModified(time), + platform: _platform, + failureMessage: 'Flutter failed to set last modified time of file at "${delegate.path}"', + ); + } + + @override + void setLastModifiedSync(DateTime time) { + _runSync( + () => delegate.setLastModifiedSync(time), + platform: _platform, + failureMessage: 'Flutter failed to set last modified time of file at "${delegate.path}"', + ); + } + + @override + Future open({FileMode mode = FileMode.read}) async { + return _run( + () => delegate.open(mode: mode), + platform: _platform, + failureMessage: 'Flutter failed to open file at "${delegate.path}"', + posixPermissionSuggestion: _posixPermissionSuggestion([delegate.path]), + ); + } + + @override + Future readAsBytes() async { + return _run( + () => delegate.readAsBytes(), + platform: _platform, + failureMessage: 'Flutter failed to read file at "${delegate.path}"', + posixPermissionSuggestion: _posixPermissionSuggestion([delegate.path]), + ); + } + + @override + Uint8List readAsBytesSync() { + return _runSync( + () => delegate.readAsBytesSync(), + platform: _platform, + failureMessage: 'Flutter failed to read file at "${delegate.path}"', + posixPermissionSuggestion: _posixPermissionSuggestion([delegate.path]), + ); + } + + @override + Future readAsString({Encoding encoding = utf8}) async { + return _run( + () => delegate.readAsString(encoding: encoding), + platform: _platform, + failureMessage: 'Flutter failed to read file at "${delegate.path}"', + posixPermissionSuggestion: _posixPermissionSuggestion([delegate.path]), + ); + } + + @override + Future> readAsLines({Encoding encoding = utf8}) async { + return _run>( + () => delegate.readAsLines(encoding: encoding), + platform: _platform, + failureMessage: 'Flutter failed to read file at "${delegate.path}"', + posixPermissionSuggestion: _posixPermissionSuggestion([delegate.path]), + ); + } + + @override + List readAsLinesSync({Encoding encoding = utf8}) { + return _runSync>( + () => delegate.readAsLinesSync(encoding: encoding), + platform: _platform, + failureMessage: 'Flutter failed to read file at "${delegate.path}"', + posixPermissionSuggestion: _posixPermissionSuggestion([delegate.path]), + ); + } + + @override + Future copy(String newPath) async { + return _run( + () async => wrapFile(await delegate.copy(newPath)), + platform: _platform, + failureMessage: 'Flutter failed to copy file from "${delegate.path}" to "$newPath"', + posixPermissionSuggestion: _posixPermissionSuggestion([delegate.path]), + ); + } + String _posixPermissionSuggestion(List paths) => 'Try running:\n' ' sudo chown -R \$(whoami) ${paths.map(fileSystem.path.absolute).join(' ')}'; @@ -442,6 +705,7 @@ class ErrorHandlingDirectory extends ForwardingFileSystemEntity[kSystemCodeCannotFindFile, kSystemCodePathNotFound], ); } @@ -452,6 +716,7 @@ class ErrorHandlingDirectory extends ForwardingFileSystemEntity[kSystemCodeCannotFindFile, kSystemCodePathNotFound], ); } @@ -465,6 +730,102 @@ class ErrorHandlingDirectory extends ForwardingFileSystemEntity exists() async { + // ignore: avoid_slow_async_io + return _run(() => delegate.exists(), platform: _platform); + } + + @override + Future rename(String newPath) async { + return _run( + () async => wrapDirectory(await delegate.rename(newPath)), + platform: _platform, + failureMessage: 'Flutter failed to rename directory at "${delegate.path}" to "$newPath"', + posixPermissionSuggestion: _posixPermissionSuggestion(delegate.path), + ); + } + + @override + Directory renameSync(String newPath) { + return _runSync( + () => wrapDirectory(delegate.renameSync(newPath)), + platform: _platform, + failureMessage: 'Flutter failed to rename directory at "${delegate.path}" to "$newPath"', + posixPermissionSuggestion: _posixPermissionSuggestion(delegate.path), + ); + } + + @override + Future stat() async { + return _run( + // ignore: avoid_slow_async_io + () => delegate.stat(), + platform: _platform, + failureMessage: 'Flutter failed to retrieve statistics of directory at "${delegate.path}"', + ); + } + + @override + FileStat statSync() { + return _runSync( + () => delegate.statSync(), + platform: _platform, + failureMessage: 'Flutter failed to retrieve statistics of directory at "${delegate.path}"', + ); + } + + @override + Stream list({bool recursive = false, bool followLinks = true}) { + return delegate + .list(recursive: recursive, followLinks: followLinks) + .map((io.FileSystemEntity entity) { + if (entity is io.File) { + return wrapFile(entity); + } else if (entity is io.Directory) { + return wrapDirectory(entity); + } else if (entity is io.Link) { + return wrapLink(entity); + } + throw AssertionError('Unsupported type: $entity'); + }) + .handleError((Object error) { + if (error is FileSystemException) { + _onFileSystemException( + exception: error, + platform: _platform, + failureMessage: 'Flutter failed to list directory at "${delegate.path}"', + posixPermissionSuggestion: _posixPermissionSuggestion(delegate.path), + ); + } + // ignore: only_throw_errors + throw error; + }); + } + + @override + List listSync({bool recursive = false, bool followLinks = true}) { + return _runSync>( + () { + return delegate.listSync(recursive: recursive, followLinks: followLinks).map(( + io.FileSystemEntity entity, + ) { + if (entity is io.File) { + return wrapFile(entity); + } else if (entity is io.Directory) { + return wrapDirectory(entity); + } else if (entity is io.Link) { + return wrapLink(entity); + } + throw AssertionError('Unsupported type: $entity'); + }).toList(); + }, + platform: _platform, + failureMessage: 'Flutter failed to list directory at "${delegate.path}"', + posixPermissionSuggestion: _posixPermissionSuggestion(delegate.path), + ); + } + String _posixPermissionSuggestion(String path) => 'Try running:\n' ' sudo chown -R \$(whoami) ${fileSystem.path.absolute(path)}'; @@ -497,6 +858,142 @@ class ErrorHandlingLink extends ForwardingFileSystemEntity with F Link wrapLink(io.Link delegate) => ErrorHandlingLink(platform: _platform, fileSystem: fileSystem, delegate: delegate); + @override + Future exists() async { + // ignore: avoid_slow_async_io + return _run(() => delegate.exists(), platform: _platform); + } + + @override + bool existsSync() { + return _runSync(() => delegate.existsSync(), platform: _platform); + } + + @override + Future create(String target, {bool recursive = false}) async { + return _run( + () async => wrapLink(await delegate.create(target, recursive: recursive)), + platform: _platform, + failureMessage: 'Flutter failed to create a link at "${delegate.path}" to "$target"', + ignoreErrorCodes: _platform.isWindows + ? const [ + kSystemCodeInvalidFunction, + kSystemCodeAccessDenied, + kSystemCodePrivilegeNotHeld, + ] + : const [], + ); + } + + @override + void createSync(String target, {bool recursive = false}) { + _runSync( + () => delegate.createSync(target, recursive: recursive), + platform: _platform, + failureMessage: 'Flutter failed to create a link at "${delegate.path}" to "$target"', + ignoreErrorCodes: _platform.isWindows + ? const [ + kSystemCodeInvalidFunction, + kSystemCodeAccessDenied, + kSystemCodePrivilegeNotHeld, + ] + : const [], + ); + } + + @override + Future update(String target) async { + return _run( + () async => wrapLink(await delegate.update(target)), + platform: _platform, + failureMessage: 'Flutter failed to update a link at "${delegate.path}" to "$target"', + ); + } + + @override + void updateSync(String target) { + _runSync( + () => delegate.updateSync(target), + platform: _platform, + failureMessage: 'Flutter failed to update a link at "${delegate.path}" to "$target"', + ); + } + + @override + Future target() async { + return _run( + () => delegate.target(), + platform: _platform, + failureMessage: 'Flutter failed to resolve target of a link at "${delegate.path}"', + ); + } + + @override + String targetSync() { + return _runSync( + () => delegate.targetSync(), + platform: _platform, + failureMessage: 'Flutter failed to resolve target of a link at "${delegate.path}"', + ); + } + + @override + Future rename(String newPath) async { + return _run( + () async => wrapLink(await delegate.rename(newPath)), + platform: _platform, + failureMessage: 'Flutter failed to rename a link at "${delegate.path}" to "$newPath"', + ); + } + + @override + Link renameSync(String newPath) { + return _runSync( + () => wrapLink(delegate.renameSync(newPath)), + platform: _platform, + failureMessage: 'Flutter failed to rename a link at "${delegate.path}" to "$newPath"', + ); + } + + @override + Future delete({bool recursive = false}) async { + return _run( + () async => wrapLink((await delegate.delete(recursive: recursive)) as io.Link), + platform: _platform, + failureMessage: 'Flutter failed to delete a link at "${delegate.path}"', + ignoreErrorCodes: const [kSystemCodeCannotFindFile, kSystemCodePathNotFound], + ); + } + + @override + void deleteSync({bool recursive = false}) { + _runSync( + () => delegate.deleteSync(recursive: recursive), + platform: _platform, + failureMessage: 'Flutter failed to delete a link at "${delegate.path}"', + ignoreErrorCodes: const [kSystemCodeCannotFindFile, kSystemCodePathNotFound], + ); + } + + @override + Future stat() async { + return _run( + // ignore: avoid_slow_async_io + () => delegate.stat(), + platform: _platform, + failureMessage: 'Flutter failed to retrieve statistics of a link at "${delegate.path}"', + ); + } + + @override + FileStat statSync() { + return _runSync( + () => delegate.statSync(), + platform: _platform, + failureMessage: 'Flutter failed to retrieve statistics of a link at "${delegate.path}"', + ); + } + @override String toString() => delegate.toString(); } @@ -504,41 +1001,105 @@ class ErrorHandlingLink extends ForwardingFileSystemEntity with F const _kNoExecutableFound = 'The Flutter tool could not locate an executable with suitable permissions'; +List? overrideWindowsRetryBackoffs; + +Duration? _getWindowsRetryDelay({ + required Platform platform, + required int errorCode, + required int attempt, + required List ignoreErrorCodes, +}) { + if (!platform.isWindows) { + return null; + } + if (ignoreErrorCodes.contains(errorCode)) { + return null; + } + if (overrideWindowsRetryBackoffs != null) { + if (_isWindowsTransientLock(errorCode) && attempt < overrideWindowsRetryBackoffs!.length) { + return overrideWindowsRetryBackoffs![attempt]; + } + return null; + } + const maxAttempts = 5; + const baseDelayMs = 50; + if (_isWindowsTransientLock(errorCode) && attempt < maxAttempts) { + final int delayMs = baseDelayMs * (1 << attempt); // 50ms, 100ms, 200ms, 400ms, 800ms + return Duration(milliseconds: delayMs); + } + return null; +} + +bool _isWindowsTransientLock(int errorCode) { + return errorCode == kSystemCodeAccessDenied || + errorCode == kSystemCodeSharingViolation || + errorCode == kSystemCodeLockViolation || + errorCode == kSystemCodeUserMappedSectionOpened; +} + Future _run( Future Function() op, { required Platform platform, String? failureMessage, String? posixPermissionSuggestion, + List ignoreErrorCodes = const [], }) async { - try { - return await op(); - } on ProcessPackageExecutableNotFoundException catch (e) { - if (e.candidates.isNotEmpty) { - throwToolExit('$_kNoExecutableFound: $e'); - } - rethrow; - } on FileSystemException catch (e) { - if (platform.isWindows) { - _handleWindowsException(e, failureMessage, e.osError?.errorCode ?? 0); - } else if (platform.isLinux || platform.isMacOS) { - _handlePosixException( - e, - failureMessage, - e.osError?.errorCode ?? 0, - posixPermissionSuggestion, + var attempt = 0; + while (true) { + try { + return await op(); + } on ProcessPackageExecutableNotFoundException catch (e) { + if (e.candidates.isNotEmpty) { + throwToolExit('$_kNoExecutableFound: $e'); + } + rethrow; + } on FileSystemException catch (e) { + final int errorCode = e.osError?.errorCode ?? 0; + if (ignoreErrorCodes.contains(errorCode)) { + rethrow; + } + final Duration? delay = _getWindowsRetryDelay( + platform: platform, + errorCode: errorCode, + attempt: attempt, + ignoreErrorCodes: ignoreErrorCodes, ); + if (delay != null) { + attempt++; + await Future.delayed(delay); + continue; + } + _onFileSystemException( + exception: e, + platform: platform, + failureMessage: failureMessage, + posixPermissionSuggestion: posixPermissionSuggestion, + ); + rethrow; + } on io.ProcessException catch (e) { + final int errorCode = e.errorCode; + if (ignoreErrorCodes.contains(errorCode)) { + rethrow; + } + final Duration? delay = _getWindowsRetryDelay( + platform: platform, + errorCode: errorCode, + attempt: attempt, + ignoreErrorCodes: ignoreErrorCodes, + ); + if (delay != null) { + attempt++; + await Future.delayed(delay); + continue; + } + _onProcessException( + exception: e, + platform: platform, + failureMessage: failureMessage, + posixPermissionSuggestion: posixPermissionSuggestion, + ); + rethrow; } - rethrow; - } on io.ProcessException catch (e) { - if (platform.isWindows) { - _handleWindowsException(e, failureMessage, e.errorCode); - } else if (platform.isLinux) { - _handlePosixException(e, failureMessage, e.errorCode, posixPermissionSuggestion); - } - if (platform.isMacOS) { - _handleMacOSException(e, failureMessage, e.errorCode, posixPermissionSuggestion); - } - rethrow; } } @@ -547,36 +1108,64 @@ T _runSync( required Platform platform, String? failureMessage, String? posixPermissionSuggestion, + List ignoreErrorCodes = const [], }) { - try { - return op(); - } on ProcessPackageExecutableNotFoundException catch (e) { - if (e.candidates.isNotEmpty) { - throwToolExit('$_kNoExecutableFound: $e'); - } - rethrow; - } on FileSystemException catch (e) { - if (platform.isWindows) { - _handleWindowsException(e, failureMessage, e.osError?.errorCode ?? 0); - } else if (platform.isLinux || platform.isMacOS) { - _handlePosixException( - e, - failureMessage, - e.osError?.errorCode ?? 0, - posixPermissionSuggestion, + var attempt = 0; + while (true) { + try { + return op(); + } on ProcessPackageExecutableNotFoundException catch (e) { + if (e.candidates.isNotEmpty) { + throwToolExit('$_kNoExecutableFound: $e'); + } + rethrow; + } on FileSystemException catch (e) { + final int errorCode = e.osError?.errorCode ?? 0; + if (ignoreErrorCodes.contains(errorCode)) { + rethrow; + } + final Duration? delay = _getWindowsRetryDelay( + platform: platform, + errorCode: errorCode, + attempt: attempt, + ignoreErrorCodes: ignoreErrorCodes, ); + if (delay != null) { + attempt++; + io.sleep(delay); + continue; + } + _onFileSystemException( + exception: e, + platform: platform, + failureMessage: failureMessage, + posixPermissionSuggestion: posixPermissionSuggestion, + ); + rethrow; + } on io.ProcessException catch (e) { + final int errorCode = e.errorCode; + if (ignoreErrorCodes.contains(errorCode)) { + rethrow; + } + final Duration? delay = _getWindowsRetryDelay( + platform: platform, + errorCode: errorCode, + attempt: attempt, + ignoreErrorCodes: ignoreErrorCodes, + ); + if (delay != null) { + attempt++; + io.sleep(delay); + continue; + } + _onProcessException( + exception: e, + platform: platform, + failureMessage: failureMessage, + posixPermissionSuggestion: posixPermissionSuggestion, + ); + rethrow; } - rethrow; - } on io.ProcessException catch (e) { - if (platform.isWindows) { - _handleWindowsException(e, failureMessage, e.errorCode); - } else if (platform.isLinux) { - _handlePosixException(e, failureMessage, e.errorCode, posixPermissionSuggestion); - } - if (platform.isMacOS) { - _handleMacOSException(e, failureMessage, e.errorCode, posixPermissionSuggestion); - } - rethrow; } } @@ -693,6 +1282,37 @@ class ErrorHandlingProcessManager extends ProcessManager { } } +void _onFileSystemException({ + required FileSystemException exception, + required Platform platform, + String? failureMessage, + String? posixPermissionSuggestion, +}) { + final int errorCode = exception.osError?.errorCode ?? 0; + if (platform.isWindows) { + _handleWindowsException(exception, failureMessage, errorCode); + } else if (platform.isLinux || platform.isMacOS) { + _handlePosixException(exception, failureMessage, errorCode, posixPermissionSuggestion); + } +} + +void _onProcessException({ + required io.ProcessException exception, + required Platform platform, + String? failureMessage, + String? posixPermissionSuggestion, +}) { + final int errorCode = exception.errorCode; + if (platform.isWindows) { + _handleWindowsException(exception, failureMessage, errorCode); + } else if (platform.isLinux) { + _handlePosixException(exception, failureMessage, errorCode, posixPermissionSuggestion); + } + if (platform.isMacOS) { + _handleMacOSException(exception, failureMessage, errorCode, posixPermissionSuggestion); + } +} + void _handlePosixException( Exception e, String? message, @@ -704,18 +1324,21 @@ void _handlePosixException( // https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno-base.h // https://github.com/apple/darwin-xnu/blob/main/bsd/dev/dtrace/scripts/errno.d const eperm = 1; + const enoent = 2; const enospc = 28; const eacces = 13; // Catch errors and bail when: - String? errorMessage; - switch (errorCode) { - case enospc: - errorMessage = - '$message. The target device is full.' + final String? errorMessage = switch (errorCode) { + enoent => + '${message != null ? "$message. " : ""}The file or directory could not be found.' '\n$e\n' - 'Free up space and try again.'; - case eperm: - case eacces: + 'This can sometimes happen if the file was deleted or moved while the tool was running.' + ' Try running "flutter clean" and try again.', + enospc => + '$message. The target device is full.' + '\n$e\n' + 'Free up space and try again.', + eperm || eacces => () { final errorBuffer = StringBuffer(); if (message != null && message.isNotEmpty) { errorBuffer.writeln('$message.'); @@ -729,11 +1352,10 @@ void _handlePosixException( if (posixPermissionSuggestion != null && posixPermissionSuggestion.isNotEmpty) { errorBuffer.writeln(posixPermissionSuggestion); } - errorMessage = errorBuffer.toString(); - default: - // Caller must rethrow the exception. - break; - } + return errorBuffer.toString(); + }(), + _ => null, + }; _throwFileSystemException(errorMessage); } @@ -777,44 +1399,45 @@ void _handleMacOSException( void _handleWindowsException(Exception e, String? message, int errorCode) { // From: // https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes + const kFileNotFound = 2; + const kPathNotFound = 3; const kDeviceFull = 112; + const kSharingViolation = 32; + const kLockViolation = 33; const kUserMappedSectionOpened = 1224; const kAccessDenied = 5; const kFatalDeviceHardwareError = 483; const kDeviceDoesNotExist = 433; // Catch errors and bail when: - String? errorMessage; - switch (errorCode) { - case kAccessDenied: - errorMessage = - '$message. The flutter tool cannot access the file or directory.\n' + final String? errorMessage = switch (errorCode) { + kFileNotFound || kPathNotFound => + '${message != null ? "$message. " : ""}The file or directory could not be found.' + '\n$e\n' + 'This can sometimes happen if the file was deleted or moved while the tool was running.' + ' Try running "flutter clean" and try again.', + kAccessDenied => + '$message. The flutter tool cannot access the file or directory.\n' 'Please ensure that the SDK and/or project is installed in a location ' - 'that has read/write permissions for the current user.'; - case kDeviceFull: - errorMessage = - '$message. The target device is full.' + 'that has read/write permissions for the current user.', + kDeviceFull => + '$message. The target device is full.' '\n$e\n' - 'Free up space and try again.'; - case kUserMappedSectionOpened: - errorMessage = - '$message. The file is being used by another program.' + 'Free up space and try again.', + kSharingViolation || kLockViolation || kUserMappedSectionOpened => + '$message. The file is being used by another program.' '\n$e\n' 'Do you have an antivirus program running? ' - 'Try disabling your antivirus program and try again.'; - case kFatalDeviceHardwareError: - errorMessage = - '$message. There is a problem with the device driver ' - 'that this file or directory is stored on.'; - case kDeviceDoesNotExist: - errorMessage = - '$message. The device was not found.' + 'Try disabling your antivirus program and try again.', + kFatalDeviceHardwareError => + '$message. There is a problem with the device driver ' + 'that this file or directory is stored on.', + kDeviceDoesNotExist => + '$message. The device was not found.' '\n$e\n' - 'Verify the device is mounted and try again.'; - default: - // Caller must rethrow the exception. - break; - } + 'Verify the device is mounted and try again.', + _ => null, + }; _throwFileSystemException(errorMessage); } diff --git a/packages/flutter_tools/lib/src/base/net.dart b/packages/flutter_tools/lib/src/base/net.dart index 248824909cf5e..93f0584cdf071 100644 --- a/packages/flutter_tools/lib/src/base/net.dart +++ b/packages/flutter_tools/lib/src/base/net.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; import '../convert.dart'; +import '../shorebird/network_trace_span.dart'; import 'common.dart'; import 'file_system.dart'; import 'io.dart'; @@ -87,6 +88,8 @@ class Net { Future _attempt(Uri url, {IOSink? destSink, bool onlyHeaders = false}) async { assert(onlyHeaders || destSink != null); _logger.printTrace('Downloading: $url'); + final traceSpan = NetworkTraceSpan.start(url: url, onlyHeaders: onlyHeaders); + final HttpClient httpClient = _httpClientFactory(); HttpClientRequest request; HttpClientResponse? response; @@ -98,6 +101,7 @@ class Net { } response = await request.close(); } on ArgumentError catch (error) { + traceSpan.record(errorKind: 'ArgumentError'); final String? overrideUrl = _platform.environment[kFlutterStorageBaseUrl]; if (overrideUrl != null && url.toString().contains(overrideUrl)) { _logger.printError(error.toString()); @@ -112,6 +116,7 @@ class Net { _logger.printError(error.toString()); rethrow; } on HandshakeException catch (error) { + traceSpan.record(errorKind: 'HandshakeException'); _logger.printTrace(error.toString()); throwToolExit( 'Could not authenticate download server. You may be experiencing a man-in-the-middle attack,\n' @@ -120,29 +125,35 @@ class Net { exitCode: kNetworkProblemExitCode, ); } on SocketException catch (error) { + traceSpan.record(errorKind: 'SocketException'); _logger.printTrace('Download error: $error'); return false; } on HttpException catch (error) { + traceSpan.record(errorKind: 'HttpException'); _logger.printTrace('Download error: $error'); return false; } + final int statusCode = response.statusCode; + // If we're making a HEAD request, we're only checking to see if the URL is // valid. if (onlyHeaders) { - return response.statusCode == HttpStatus.ok; + traceSpan.record(statusCode: statusCode); + return statusCode == HttpStatus.ok; } - if (response.statusCode != HttpStatus.ok) { - if (response.statusCode > 0 && response.statusCode < 500) { + if (statusCode != HttpStatus.ok) { + traceSpan.record(statusCode: statusCode); + if (statusCode > 0 && statusCode < 500) { throwToolExit( 'Download failed.\n' 'URL: $url\n' - 'Error: ${response.statusCode} ${response.reasonPhrase}', + 'Error: $statusCode ${response.reasonPhrase}', exitCode: kNetworkProblemExitCode, ); } // 5xx errors are server errors and we can try again - _logger.printTrace('Download error: ${response.statusCode} ${response.reasonPhrase}'); + _logger.printTrace('Download error: $statusCode ${response.reasonPhrase}'); return false; } _logger.printTrace('Received response from server, collecting bytes...'); @@ -156,6 +167,9 @@ class Net { } finally { await destSink?.flush(); await destSink?.close(); + // Record after the body has been fully drained so `dur` reflects + // end-to-end download time, not just time-to-first-byte. + traceSpan.record(statusCode: statusCode); } } } diff --git a/packages/flutter_tools/lib/src/base/process.dart b/packages/flutter_tools/lib/src/base/process.dart index 187f5bfa8b728..35813f51983c3 100644 --- a/packages/flutter_tools/lib/src/base/process.dart +++ b/packages/flutter_tools/lib/src/base/process.dart @@ -238,6 +238,7 @@ abstract class ProcessUtils { RegExp? stdoutErrorMatcher, StringConverter? mapFunction, Map? environment, + void Function(Process process)? onStart, }); bool exitsHappySync(List cli, {Map? environment}); @@ -554,6 +555,7 @@ class _DefaultProcessUtils implements ProcessUtils { RegExp? stdoutErrorMatcher, StringConverter? mapFunction, Map? environment, + void Function(Process process)? onStart, }) async { final Process process = await start( cmd, @@ -561,6 +563,7 @@ class _DefaultProcessUtils implements ProcessUtils { allowReentrantFlutter: allowReentrantFlutter, environment: environment, ); + onStart?.call(process); final StreamSubscription stdoutSubscription = process.stdout .transform(utf8LineDecoder) .where((String line) => filter == null || filter.hasMatch(line)) diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index 29ef12f7bb891..cdb4290180e9c 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -44,6 +44,7 @@ class BuildInfo { List? dartExperiments, required this.treeShakeIcons, this.performanceMeasurementFile, + this.shorebirdTraceFilePath, required this.packageConfigPath, this.codeSizeDirectory, this.androidGradleDaemon = true, @@ -179,6 +180,12 @@ class BuildInfo { /// rerun tasks. final String? performanceMeasurementFile; + /// Path to the Shorebird build-trace output file. When non-null, the + /// Shorebird-specific build-trace plumbing emits a Chrome Trace Event + /// Format JSON file here. Named `shorebird` to keep the fork's + /// surface area visibly distinct from upstream Flutter. + final String? shorebirdTraceFilePath; + /// If provided, an output directory where one or more v8-style heap snapshots /// will be written for code size profiling. final String? codeSizeDirectory; @@ -375,6 +382,7 @@ class BuildInfo { kFileSystemScheme: ?fileSystemScheme, kBuildName: ?buildName, kBuildNumber: ?buildNumber, + kFlavor: ?flavor, if (useLocalCanvasKit) kUseLocalCanvasKitFlag: useLocalCanvasKit.toString(), }; } diff --git a/packages/flutter_tools/lib/src/build_system/build_system.dart b/packages/flutter_tools/lib/src/build_system/build_system.dart index 9abcda6d39c55..d6bc3becca308 100644 --- a/packages/flutter_tools/lib/src/build_system/build_system.dart +++ b/packages/flutter_tools/lib/src/build_system/build_system.dart @@ -882,6 +882,7 @@ class _BuildInstance { Future _invokeInternal(Node node) async { final PoolResource resource = await resourcePool.request(); + final int startTimeMicroseconds = DateTime.now().microsecondsSinceEpoch; final stopwatch = Stopwatch()..start(); var succeeded = true; var skipped = false; @@ -982,6 +983,7 @@ class _BuildInstance { skipped: skipped, succeeded: succeeded, analyticsName: node.target.analyticsName, + startTimeMicroseconds: startTimeMicroseconds, ); } return succeeded; @@ -1011,6 +1013,7 @@ class PerformanceMeasurement { required this.skipped, required this.succeeded, required this.analyticsName, + required this.startTimeMicroseconds, }); final int elapsedMilliseconds; @@ -1018,6 +1021,9 @@ class PerformanceMeasurement { final bool skipped; final bool succeeded; final String analyticsName; + + /// Wall-clock start time in microseconds since epoch. + final int startTimeMicroseconds; } /// Check if there are any dependency cycles in the target. 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 89b0be24ed3fc..89bd961508363 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/assets.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/assets.dart @@ -13,6 +13,7 @@ import '../../dart/package_map.dart'; import '../../devfs.dart'; import '../../flutter_manifest.dart'; import '../../isolated/native_assets/dart_hook_result.dart'; +import '../../shorebird/shorebird_yaml.dart'; import '../build_system.dart'; import '../depfile.dart'; import '../exceptions.dart'; @@ -186,6 +187,17 @@ Future copyAssets( } if (doCopy) { await (content.file as File).copy(file.path); + if (file.basename == 'shorebird.yaml') { + try { + updateShorebirdYaml( + environment.defines[kFlavor], + file.path, + environment: environment.platform.environment, + ); + } on Exception catch (error) { + throw Exception('Failed to generate shorebird configuration. Error: $error'); + } + } } } else { await file.writeAsBytes(await entry.value.content.contentsAsBytes()); diff --git a/packages/flutter_tools/lib/src/build_system/targets/common.dart b/packages/flutter_tools/lib/src/build_system/targets/common.dart index 6e54a2ddecb7f..8f34ee062cead 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/common.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/common.dart @@ -509,3 +509,48 @@ abstract final class Lipo { } } } + +/// For managing the supplementary linking files for Shorebird. +abstract final class LinkSupplement { + static Future create( + Environment environment, { + required String inputBuildDir, + required String outputBuildDir, + }) async { + // If the shorebird directory exists, delete it first. + final Directory shorebirdDir = environment.fileSystem.directory( + environment.fileSystem.path.join(outputBuildDir, 'shorebird'), + ); + if (shorebirdDir.existsSync()) { + shorebirdDir.deleteSync(recursive: true); + } + + void maybeCopy(String name) { + final path = environment.fileSystem.path.join(inputBuildDir, name); + final File file = environment.fileSystem.file(path); + if (file.existsSync()) { + file.copySync(environment.fileSystem.path.join(shorebirdDir.path, name)); + environment.logger.printTrace('LinkSupplement: copied $name'); + } else { + environment.logger.printTrace('LinkSupplement: missing $name at $path'); + } + } + + // Copy the link information (generated by gen_snapshot) + // into the shorebird directory. + maybeCopy('App.ct.link'); + maybeCopy('App.class_table.json'); + maybeCopy('App.dt.link'); + maybeCopy('App.dispatch_table.json'); + maybeCopy('App.ft.link'); + maybeCopy('App.field_table.json'); + // DD table files for cascade limiter (produced by 2-pass release build + // when SHOREBIRD_DD_MAX_BYTES is set). + maybeCopy('App.dd.link'); + maybeCopy('App.dd_callers.link'); + maybeCopy('App.dd_identity.link'); + maybeCopy('App.dd_slots.link'); + // Per-slot DD resolution outcome diagnostic (TSV). + maybeCopy('App.dd_resolution.tsv'); + } +} 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 f3f9e0dee1de4..9f861d037adc6 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -143,6 +143,12 @@ abstract class AotAssemblyBase extends Target { // Don't fail if the dSYM wasn't created (i.e. during a debug build). skipMissingInputs: true, ); + + await LinkSupplement.create( + environment, + inputBuildDir: buildOutputPath, + outputBuildDir: getIosBuildDirectory(), + ); } } 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 f3fb4a878cf5f..52129e44511c5 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -353,6 +353,12 @@ class CompileMacOSFramework extends Target { // Don't fail if the dSYM wasn't created (i.e. during a debug build). skipMissingInputs: true, ); + + await LinkSupplement.create( + environment, + inputBuildDir: buildOutputPath, + outputBuildDir: getMacOSBuildDirectory(), + ); } @override diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart index 49e35e71706fb..6b3cdb62a7dfc 100644 --- a/packages/flutter_tools/lib/src/cache.dart +++ b/packages/flutter_tools/lib/src/cache.dart @@ -37,6 +37,7 @@ import 'base/utils.dart' show getElapsedAsSeconds, getSizeAsPlatformMB; import 'convert.dart'; import 'features.dart'; +const kShorebirdStorageUrl = 'https://download.shorebird.dev'; const kFlutterRootEnvironmentVariableName = 'FLUTTER_ROOT'; // should point to //flutter/ (root of flutter/flutter repo) const kFlutterEngineEnvironmentVariableName = @@ -526,7 +527,7 @@ class Cache { /// The base for URLs that store Flutter engine artifacts that are fetched /// during the installation of the Flutter SDK. /// - /// By default the base URL is https://storage.googleapis.com. However, if + /// By default the base URL is https://download.shorebird.dev. However, if /// `FLUTTER_STORAGE_BASE_URL` environment variable ([kFlutterStorageBaseUrl]) /// is provided, the environment variable value is returned instead. /// @@ -538,9 +539,13 @@ class Cache { String? overrideUrl = _platform.environment[kFlutterStorageBaseUrl]; if (overrideUrl == null) { return storageRealm.isEmpty - ? 'https://storage.googleapis.com' + ? 'https://download.shorebird.dev' : 'https://storage.googleapis.com/$storageRealm'; } + // Shorebird's artifact proxy is a trusted source. + if (overrideUrl == kShorebirdStorageUrl) { + return overrideUrl; + } // verify that this is a valid URI. overrideUrl = storageRealm.isEmpty ? overrideUrl : '$overrideUrl/$storageRealm'; try { diff --git a/packages/flutter_tools/lib/src/commands/analyze_once.dart b/packages/flutter_tools/lib/src/commands/analyze_once.dart index 0646482b7976e..940804d64ce15 100644 --- a/packages/flutter_tools/lib/src/commands/analyze_once.dart +++ b/packages/flutter_tools/lib/src/commands/analyze_once.dart @@ -95,8 +95,9 @@ class AnalyzeOnce extends AnalyzeBase { if (!analysisCompleter.isCompleted) { analysisCompleter.completeError( // Include the last 20 lines of server output in exception message - Exception( + _AnalysisServerExitException( 'analysis server exited with code $exitCode and output:\n${server.getLogs(20)}', + exitCode, ), ); } @@ -112,7 +113,11 @@ class AnalyzeOnce extends AnalyzeBase { ? logger.startProgress('Analyzing $message...') : null; - await analysisCompleter.future; + try { + await analysisCompleter.future; + } on _AnalysisServerExitException catch (error) { + throwToolExit(error.message, exitCode: error.exitCode); + } } finally { await server.dispose(); progress?.cancel(); @@ -173,3 +178,9 @@ class AnalyzeOnce extends AnalyzeBase { return false; } } + +class _AnalysisServerExitException implements Exception { + _AnalysisServerExitException(this.message, this.exitCode); + final String message; + final int? exitCode; +} diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart index 27c0c18461ecf..9064e92b929a2 100644 --- a/packages/flutter_tools/lib/src/commands/assemble.dart +++ b/packages/flutter_tools/lib/src/commands/assemble.dart @@ -4,6 +4,7 @@ import 'package:args/args.dart'; import 'package:meta/meta.dart'; +import 'package:shorebird_build_trace/shorebird_build_trace.dart'; import 'package:unified_analytics/unified_analytics.dart'; import '../artifacts.dart'; @@ -116,6 +117,13 @@ class AssembleCommand extends FlutterCommand { 'performance-measurement-file', help: 'Output individual target performance to a JSON file.', ); + argParser.addOption( + 'shorebird-trace-file', + help: + 'Output a Shorebird build trace in Chrome Trace Event Format ' + 'JSON. Shorebird-specific; used by the build-trace plumbing in ' + 'gradle.dart / mac.dart to collect flutter-assemble timings.', + ); argParser.addMultiOption( 'input', abbr: 'i', @@ -398,6 +406,10 @@ class AssembleCommand extends FlutterCommand { final File outFile = globals.fs.file(argumentResults['performance-measurement-file']); writePerformanceData(result.performance.values, outFile); } + if (argumentResults.wasParsed('shorebird-trace-file')) { + final File outFile = globals.fs.file(argumentResults['shorebird-trace-file']); + writeTraceData(result.performance.values, outFile); + } if (argumentResults.wasParsed('depfile')) { final File depfileFile = globals.fs.file(stringArg('depfile')); final depfile = Depfile(result.inputFiles, result.outputFiles); @@ -444,3 +456,35 @@ void writePerformanceData(Iterable measurements, File ou } outFile.writeAsStringSync(json.encode(jsonData)); } + +/// Output build trace data in Chrome Trace Event Format in [outFile]. +/// `flutter assemble` is re-entered as its own process by Gradle / +/// xcode_backend, so its events get their own Perfetto row named +/// `flutter assemble`. Spans for each [PerformanceMeasurement] are +/// emitted using its wall-clock [PerformanceMeasurement.startTimeMicroseconds] +/// (converted to a DateTime; not the stopwatch duration alone) so the +/// timeline aligns with the parent flutter tool's trace when merged. +@visibleForTesting +void writeTraceData(Iterable measurements, File outFile) { + final int pid = currentProcessId(); + final tracer = BuildTracer() + ..addProcessNameMetadata(pid: pid, name: 'flutter assemble') + ..addThreadNameMetadata(pid: pid, tid: 1, name: 'flutter assemble'); + for (final measurement in measurements) { + final start = DateTime.fromMicrosecondsSinceEpoch(measurement.startTimeMicroseconds); + tracer.addCompleteEvent( + name: measurement.analyticsName, + cat: TraceCategory.assemble.wireName, + pid: pid, + tid: 1, + start: start, + end: start.add(Duration(milliseconds: measurement.elapsedMilliseconds)), + args: { + 'target': measurement.target, + 'skipped': measurement.skipped, + 'succeeded': measurement.succeeded, + }, + ); + } + tracer.writeToFile(outFile); +} diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart index 184a68be58d90..231b6650e18ac 100644 --- a/packages/flutter_tools/lib/src/commands/build_apk.dart +++ b/packages/flutter_tools/lib/src/commands/build_apk.dart @@ -31,6 +31,7 @@ class BuildApkCommand extends BuildSubCommand { usesExtraDartFlagOptions(verboseHelp: verboseHelp); addEnableExperimentation(hide: !verboseHelp); addBuildPerformanceFile(hide: !verboseHelp); + usesShorebirdTraceOption(hide: !verboseHelp); usesAnalyzeSizeFlag(); addAndroidSpecificBuildOptions(hide: !verboseHelp); addIgnoreDeprecationOption(); diff --git a/packages/flutter_tools/lib/src/commands/build_appbundle.dart b/packages/flutter_tools/lib/src/commands/build_appbundle.dart index af58cbe68fa11..32ab81b8bab68 100644 --- a/packages/flutter_tools/lib/src/commands/build_appbundle.dart +++ b/packages/flutter_tools/lib/src/commands/build_appbundle.dart @@ -33,6 +33,7 @@ class BuildAppBundleCommand extends BuildSubCommand { usesDartDefineOption(); usesExtraDartFlagOptions(verboseHelp: verboseHelp); addBuildPerformanceFile(hide: !verboseHelp); + usesShorebirdTraceOption(hide: !verboseHelp); usesTrackWidgetCreation(verboseHelp: verboseHelp); addEnableExperimentation(hide: !verboseHelp); usesAnalyzeSizeFlag(); diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart index e783f793b9e7a..6cfb5b75dbc8a 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios.dart @@ -907,6 +907,7 @@ abstract class _BuildIOSSubCommand extends BuildSubCommand { usesExtraDartFlagOptions(verboseHelp: verboseHelp); addEnableExperimentation(hide: !verboseHelp); addBuildPerformanceFile(hide: !verboseHelp); + usesShorebirdTraceOption(hide: !verboseHelp); usesAnalyzeSizeFlag(); argParser.addFlag( 'codesign', diff --git a/packages/flutter_tools/lib/src/commands/clean.dart b/packages/flutter_tools/lib/src/commands/clean.dart index 96aa6b1613355..54c5dd40b8b04 100644 --- a/packages/flutter_tools/lib/src/commands/clean.dart +++ b/packages/flutter_tools/lib/src/commands/clean.dart @@ -81,7 +81,7 @@ class CleanCommand extends FlutterCommand { try { final XcodeProjectInterpreter xcodeProjectInterpreter = globals.xcodeProjectInterpreter!; final XcodeProjectInfo projectInfo = (await xcodeProjectInterpreter.getInfo( - xcodeWorkspace.parent.path, + xcodeProject, buildDirectory: globals.fs.directory(xcodeProject.darwinPlatform.buildDirectory()), ))!; if (argResults?.wasParsed('scheme') ?? false) { @@ -93,6 +93,7 @@ class CleanCommand extends FlutterCommand { throwToolExit('Scheme "$scheme" not found in ${projectInfo.schemes}'); } await xcodeProjectInterpreter.cleanWorkspace( + xcodeProject, xcodeWorkspace.path, scheme, verbose: _verbose, @@ -101,6 +102,7 @@ class CleanCommand extends FlutterCommand { } else { for (final String scheme in projectInfo.schemes) { await xcodeProjectInterpreter.cleanWorkspace( + xcodeProject, xcodeWorkspace.path, scheme, verbose: _verbose, diff --git a/packages/flutter_tools/lib/src/commands/widget_preview.dart b/packages/flutter_tools/lib/src/commands/widget_preview.dart index ce9ce3d1f57ff..c8219ceb17a33 100644 --- a/packages/flutter_tools/lib/src/commands/widget_preview.dart +++ b/packages/flutter_tools/lib/src/commands/widget_preview.dart @@ -273,6 +273,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C fs: fs, onChangeDetected: onChangeDetected, onPubspecChangeDetected: _onPubspecChangeDetected, + shutdownHooks: shutdownHooks, dtd: _dtdService, processManager: processManager, terminal: terminal, @@ -415,7 +416,20 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C widgetPreviewScaffoldStreamName: _dtdService.widgetPreviewScaffoldStream, ); - final FlutterWidgetPreviews originalPreviews = await _dtdService.getFlutterWidgetPreviews(); + final FlutterWidgetPreviews originalPreviews; + try { + originalPreviews = await _dtdService.getFlutterWidgetPreviews(); + } on Exception catch (e) { + throwToolExit( + 'Failed to retrieve widget previews from the Dart Tooling Daemon (DTD). ' + 'Ensure that the analysis server is running and reachable. Details: $e', + ); + } on StateError catch (e) { + throwToolExit( + 'Failed to retrieve widget previews from the Dart Tooling Daemon (DTD). ' + 'Ensure that the analysis server is running and reachable. Details: $e', + ); + } _previewCodeGenerator.populatePreviewsInGeneratedPreviewScaffoldLsp(originalPreviews); } diff --git a/packages/flutter_tools/lib/src/compile.dart b/packages/flutter_tools/lib/src/compile.dart index 5b85884c65f18..1e8f435351ab8 100644 --- a/packages/flutter_tools/lib/src/compile.dart +++ b/packages/flutter_tools/lib/src/compile.dart @@ -991,7 +991,8 @@ class DefaultResidentCompiler implements ResidentCompiler { onDone: () { // when outputFilename future is not completed, but stdout is closed // process has died unexpectedly. - if (_stdoutHandler.compilerOutput?.isCompleted == false) { + if (_stdoutHandler.compilerOutput?.isCompleted == false && + !_shutdownHooks.isShuttingDown) { _stdoutHandler.compilerOutput?.complete(); throwToolExit('The Dart compiler exited unexpectedly.'); } diff --git a/packages/flutter_tools/lib/src/dart/language_version.dart b/packages/flutter_tools/lib/src/dart/language_version.dart index 7c9003b67577d..1a18cd76963f9 100644 --- a/packages/flutter_tools/lib/src/dart/language_version.dart +++ b/packages/flutter_tools/lib/src/dart/language_version.dart @@ -56,6 +56,13 @@ LanguageVersion determineLanguageVersion(File file, Package? package, String flu // command will likely fail later in the process with a better error // message. List lines; + // If the file is missing, check existsSync() defensively first. Calling + // readAsLinesSync on a missing file inside our wrapped ErrorHandlingFileSystem + // throws a fatal ToolExit which would escape the FileSystemException catch + // block and crash the process prematurely. + if (!file.existsSync()) { + return currentLanguageVersion(file.fileSystem, flutterRoot); + } try { lines = file.readAsLinesSync(); } on FileSystemException { diff --git a/packages/flutter_tools/lib/src/darwin/darwin.dart b/packages/flutter_tools/lib/src/darwin/darwin.dart index a2fa7bdd2e07a..a6e563c0049c0 100644 --- a/packages/flutter_tools/lib/src/darwin/darwin.dart +++ b/packages/flutter_tools/lib/src/darwin/darwin.dart @@ -9,6 +9,7 @@ import '../base/version.dart'; import '../build_info.dart'; import '../ios/xcodeproj.dart'; import '../macos/swift_packages.dart'; +import '../platform_plugins.dart'; import '../project.dart'; /// Encapsulates platform-specific values for Darwin targets ([ios] and [macos]). @@ -65,6 +66,13 @@ enum FlutterDarwinPlatform { /// A list of supported [XcodeSdk]. final List sdks; + String get pluginConfigKey { + return switch (this) { + ios => IOSPlugin.kConfigKey, + macos => MacOSPlugin.kConfigKey, + }; + } + /// Minimum supported version for the platform. Version deploymentTarget() { return switch (this) { diff --git a/packages/flutter_tools/lib/src/flutter_cache.dart b/packages/flutter_tools/lib/src/flutter_cache.dart index c2228fc79b42f..bf144dbc2a5e5 100644 --- a/packages/flutter_tools/lib/src/flutter_cache.dart +++ b/packages/flutter_tools/lib/src/flutter_cache.dart @@ -864,7 +864,7 @@ class IosUsbArtifacts extends CachedArtifact { @visibleForTesting Uri get archiveUri => Uri.parse( '${cache.realmlessStorageBaseUrl}/flutter_infra_release/' - 'ios-usb-dependencies${cache.useUnsignedMacBinaries ? '/unsigned' : ''}' + 'ios-usb-dependencies/arm64_x86_64${cache.useUnsignedMacBinaries ? '/unsigned' : ''}' '/$name/$version/$name.zip', ); } diff --git a/packages/flutter_tools/lib/src/flutter_plugins.dart b/packages/flutter_tools/lib/src/flutter_plugins.dart index d1e919cc76350..691f3b24e388a 100644 --- a/packages/flutter_tools/lib/src/flutter_plugins.dart +++ b/packages/flutter_tools/lib/src/flutter_plugins.dart @@ -1358,8 +1358,7 @@ Future injectPlugins( darwinDependencyManagement ?? DarwinDependencyManagement( project: project, - plugins: plugins, - cocoapods: globals.cocoaPods!, + cocoapods: globals.cocoaPods, swiftPackageManager: SwiftPackageManager( fileSystem: globals.fs, templateRenderer: globals.templateRenderer, @@ -1368,17 +1367,19 @@ Future injectPlugins( ), fileSystem: globals.fs, featureFlags: featureFlags, - logger: globals.logger, analytics: globals.analytics, - platform: globals.platform, - xcodeProjectInterpreter: globals.xcodeProjectInterpreter, - config: globals.config, ); if (iosPlatform) { - await darwinDependencyManagerSetup.setUp(platform: FlutterDarwinPlatform.ios); + await darwinDependencyManagerSetup.setUp( + platform: FlutterDarwinPlatform.ios, + plugins: pluginsByPlatform[IOSPlugin.kConfigKey]!, + ); } if (macOSPlatform) { - await darwinDependencyManagerSetup.setUp(platform: FlutterDarwinPlatform.macos); + await darwinDependencyManagerSetup.setUp( + platform: FlutterDarwinPlatform.macos, + plugins: pluginsByPlatform[MacOSPlugin.kConfigKey]!, + ); } } } @@ -1390,6 +1391,26 @@ bool hasPlugins(FlutterProject project) { return _readFileContent(project.flutterPluginsDependenciesFile) != null; } +/// Resolves the plugin implementations for the platform specified by [platformKey]. +/// +/// Uses [_resolvePluginImplementations] with [_PluginResolutionType.nativeOrDart] +/// to find which plugins are resolved for the given platform. +/// +/// If [quiet] is true, validation and resolution errors or warnings will not +/// be printed. +List resolvePluginImplementationsForPlatform( + List plugins, + String platformKey, { + bool quiet = false, +}) { + final Map> pluginsByPlatform = _resolvePluginImplementations( + plugins, + pluginResolutionType: _PluginResolutionType.nativeOrDart, + quiet: quiet, + ); + return pluginsByPlatform[platformKey] ?? []; +} + /// Resolves the plugin implementations for all platforms. /// /// * If there is only one dependency on a package that implements the @@ -1426,9 +1447,13 @@ List resolvePlatformImplementation( /// see [resolvePlatformImplementation]. /// /// Only plugins which provide the according platform implementation are returned. +/// +/// If [quiet] is true, validation and resolution errors or warnings will not +/// be printed. Map> _resolvePluginImplementations( List plugins, { required _PluginResolutionType pluginResolutionType, + bool quiet = false, }) { final pluginsByPlatform = >{ AndroidPlugin.kConfigKey: [], @@ -1451,6 +1476,7 @@ Map> _resolvePluginImplementations( plugins, platformKey, pluginResolutionType: pluginResolutionType, + quiet: quiet, ); if (hasPlatformPluginPubspecError) { @@ -1472,11 +1498,15 @@ Map> _resolvePluginImplementations( /// Resolves the plugins for the given [platformKey] (Dart-only or native /// implementations). +/// +/// If [quiet] is true, validation and resolution errors or warnings will not +/// be printed. (List pluginImplementations, bool hasPluginPubspecError, bool hasResolutionError) _resolvePluginImplementationsByPlatform( Iterable plugins, String platformKey, { _PluginResolutionType pluginResolutionType = _PluginResolutionType.nativeOrDart, + bool quiet = false, }) { var hasPluginPubspecError = false; var hasResolutionError = false; @@ -1494,7 +1524,9 @@ _resolvePluginImplementationsByPlatform( pluginResolutionType: pluginResolutionType, ); if (error != null) { - globals.printError(error); + if (!quiet) { + globals.printError(error); + } hasPluginPubspecError = true; continue; } @@ -1533,18 +1565,22 @@ _resolvePluginImplementationsByPlatform( } } else { // Only warn, if neither an implementation for native nor for Dart is given. + if (!quiet) { + globals.printWarning( + 'Package ${plugin.name}:$platformKey references $defaultImplPluginName:$platformKey as the default plugin, but it does not provide an inline implementation.\n' + 'Ask the maintainers of ${plugin.name} to either avoid referencing a default implementation via `platforms: $platformKey: default_package: $defaultImplPluginName` ' + 'or add an inline implementation to $defaultImplPluginName via `platforms: $platformKey:` `pluginClass` or `dartPluginClass`.\n', + ); + } + } + } else { + if (!quiet) { globals.printWarning( - 'Package ${plugin.name}:$platformKey references $defaultImplPluginName:$platformKey as the default plugin, but it does not provide an inline implementation.\n' + 'Package ${plugin.name}:$platformKey references $defaultImplPluginName:$platformKey as the default plugin, but the package does not exist, or is not a plugin package.\n' 'Ask the maintainers of ${plugin.name} to either avoid referencing a default implementation via `platforms: $platformKey: default_package: $defaultImplPluginName` ' - 'or add an inline implementation to $defaultImplPluginName via `platforms: $platformKey:` `pluginClass` or `dartPluginClass`.\n', + 'or create a plugin named $defaultImplPluginName.\n', ); } - } else { - globals.printWarning( - 'Package ${plugin.name}:$platformKey references $defaultImplPluginName:$platformKey as the default plugin, but the package does not exist, or is not a plugin package.\n' - 'Ask the maintainers of ${plugin.name} to either avoid referencing a default implementation via `platforms: $platformKey: default_package: $defaultImplPluginName` ' - 'or create a plugin named $defaultImplPluginName.\n', - ); } } if (implementsPluginName != null) { @@ -1566,7 +1602,9 @@ _resolvePluginImplementationsByPlatform( defaultPackage: defaultImplementations[implCandidatesEntry.key], ); if (error != null) { - globals.printError(error); + if (!quiet) { + globals.printError(error); + } hasResolutionError = true; } else if (resolution != null) { pluginResolution[implCandidatesEntry.key] = resolution; diff --git a/packages/flutter_tools/lib/src/http_host_validator.dart b/packages/flutter_tools/lib/src/http_host_validator.dart index 9702f83bffd08..8363221166398 100644 --- a/packages/flutter_tools/lib/src/http_host_validator.dart +++ b/packages/flutter_tools/lib/src/http_host_validator.dart @@ -11,7 +11,7 @@ import 'doctor_validator.dart'; import 'features.dart'; /// Common Flutter HTTP hosts. -const kCloudHost = 'https://storage.googleapis.com/'; +const kCloudHost = 'https://download.shorebird.dev/'; const kCocoaPods = 'https://cocoapods.org/'; const kGitHub = 'https://github.com/'; const kMaven = 'https://maven.google.com/'; diff --git a/packages/flutter_tools/lib/src/ios/application_package.dart b/packages/flutter_tools/lib/src/ios/application_package.dart index f1c8f44849548..0bc7bb5a45851 100644 --- a/packages/flutter_tools/lib/src/ios/application_package.dart +++ b/packages/flutter_tools/lib/src/ios/application_package.dart @@ -130,6 +130,17 @@ class BuildableIOSApp extends IOSApp { @override String? get name => _appProductName; + String get shorebirdYamlPath => globals.fs.path.join( + archiveBundleOutputPath, + 'Products', + 'Applications', + _appProductName != null ? '$_appProductName.app' : 'Runner.app', + 'Frameworks', + 'App.framework', + 'flutter_assets', + 'shorebird.yaml', + ); + @override String get simulatorBundlePath => _buildAppPath(XcodeSdk.IPhoneSimulator.platformName); diff --git a/packages/flutter_tools/lib/src/ios/core_devices.dart b/packages/flutter_tools/lib/src/ios/core_devices.dart index 184376d02790c..153eac106cd8f 100644 --- a/packages/flutter_tools/lib/src/ios/core_devices.dart +++ b/packages/flutter_tools/lib/src/ios/core_devices.dart @@ -14,6 +14,7 @@ import '../base/logger.dart'; import '../base/process.dart'; import '../base/template.dart'; import '../base/utils.dart'; +import '../build_info.dart'; import '../convert.dart'; import '../device.dart'; import '../macos/xcode.dart'; @@ -38,13 +39,12 @@ class IOSCoreDeviceLauncher { required XcodeDebug xcodeDebug, required FileSystem fileSystem, required ProcessUtils processUtils, - required Xcode xcode, @visibleForTesting LLDB? lldb, }) : _coreDeviceControl = coreDeviceControl, _logger = logger, _xcodeDebug = xcodeDebug, _fileSystem = fileSystem, - _lldb = lldb ?? LLDB(logger: logger, processUtils: processUtils, xcode: xcode); + _lldb = lldb ?? LLDB(logger: logger, processUtils: processUtils); final IOSCoreDeviceControl _coreDeviceControl; final Logger _logger; @@ -97,6 +97,7 @@ class IOSCoreDeviceLauncher { required String bundleId, required List launchArguments, required ShutdownHooks shutdownHooks, + required BuildMode mode, }) async { // Install app to device final (bool installStatus, IOSCoreDeviceInstallResult? installResult) = await _coreDeviceControl @@ -146,6 +147,7 @@ class IOSCoreDeviceLauncher { deviceId: deviceId, appProcessId: processId, lldbLogForwarder: lldbLogForwarder, + mode: mode, ); // If it fails to attach with lldb, kill the launched process so it doesn't stay hanging. diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 390b53d39fac3..cd72e5e12220f 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -1077,6 +1077,7 @@ class IOSDevice extends Device { bundleId: package.id, launchArguments: launchArguments, shutdownHooks: globals.shutdownHooks, + mode: debuggingOptions.buildInfo.mode, ); // If it succeeds to launch with LLDB, return, otherwise continue on to diff --git a/packages/flutter_tools/lib/src/ios/lldb.dart b/packages/flutter_tools/lib/src/ios/lldb.dart index b1f2e63a65c9a..5812c0366afe1 100644 --- a/packages/flutter_tools/lib/src/ios/lldb.dart +++ b/packages/flutter_tools/lib/src/ios/lldb.dart @@ -11,22 +11,19 @@ import '../base/io.dart'; import '../base/logger.dart'; import '../base/process.dart'; import '../base/utils.dart'; -import '../base/version.dart'; -import '../macos/xcode.dart'; +import '../build_info.dart'; /// LLDB is the default debugger in Xcode on macOS. Once the application has /// launched on a physical iOS device, you can attach to it using LLDB. /// /// See `xcrun devicectl device process launch --help` for more information. class LLDB { - LLDB({required Logger logger, required ProcessUtils processUtils, required Xcode xcode}) - : _xcode = xcode, - _logger = logger, + LLDB({required Logger logger, required ProcessUtils processUtils}) + : _logger = logger, _processUtils = processUtils; final Logger _logger; final ProcessUtils _processUtils; - final Xcode _xcode; _LLDBProcess? _lldbProcess; @@ -51,6 +48,11 @@ class LLDB { /// Example: (lldb) Process 6152 resuming static final _lldbProcessResuming = RegExp(r'Process \d+ resuming'); + /// Pattern of lldb log when the process has started and the breakpoint is added. + /// + /// Example: (lldb) 1 location added to breakpoint 1 + static final _lldbBreakpointAdded = RegExp(r'location added to breakpoint'); + /// Pattern of lldb log when the breakpoint is added. /// /// Example: Breakpoint 1: no locations (pending). @@ -78,6 +80,9 @@ frame.GetThread().GetProcess().WriteMemory(base, data, error) if not error.Success(): print(f'Failed to write into {base}[+{page_len}]', error) return + +# If the returned value is False, that tells LLDB not to stop at the breakpoint +return False '''; /// Starts an LLDB process and inputs commands to start debugging the [appProcessId]. @@ -89,6 +94,7 @@ if not error.Success(): required String deviceId, required int appProcessId, required LLDBLogForwarder lldbLogForwarder, + required BuildMode mode, }) async { Timer? timer; try { @@ -112,9 +118,11 @@ if not error.Success(): return false; } await _selectDevice(deviceId); - await _setBreakpoint(); + if (mode == BuildMode.debug) { + await _setBreakpoint(); + } await _attachToAppProcess(appProcessId); - await _resumeProcess(); + await _resumeProcess(mode); _isAttached = true; } on _LLDBError catch (e) { _logger.printTrace('lldb failed with error: ${e.message}'); @@ -147,51 +155,19 @@ if not error.Success(): appProcessId: appProcessId, logger: _logger, ); + final StreamSubscription stdoutSubscription = _lldbProcess!.stdout + .transform(utf8LineDecoder) + .listen((String line) { + if (_isAttached && !_ignoreLog(line)) { + // Only forwards logs after LLDB is attached. All logs before then are part of the + // attach process. - void printLine(String line) { - if (_isAttached && !_ignoreLog(line)) { - // Only forwards logs after LLDB is attached. All logs before then are part of the - // attach process. - - lldbLogForwarder.addLog(line); - } else { - _logger.printTrace('[lldb]: $line'); - _logCompleter?.checkForMatch(line); - } - } - - String? processStopLine; - var suppressLogs = false; - - final StreamSubscription - stdoutSubscription = _lldbProcess!.stdout.transform(utf8LineDecoder).listen((String line) { - // Skip all logs between process stop and process resume if the stop was caused by a breakpoint - if (line.contains(_lldbProcessStopped)) { - processStopLine = line; - return; - } - // If the last log was a proces stop log, check if it was caused by a breakpoint. - if (processStopLine != null) { - if (line.contains('stop reason = breakpoint')) { - // When we stop due to a breakpoint, supress all logs until we resume. - _lldbProcess?.stdinWriteln('process continue'); - suppressLogs = true; - } else { - // Otherwise, print the process stop log. - printLine(processStopLine!); - } - processStopLine = null; - } - if (suppressLogs) { - if (line.contains(_lldbProcessResuming)) { - // After resuming, stop supressing logs. - suppressLogs = false; - } - return; - } - - printLine(line); - }); + lldbLogForwarder.addLog(line); + } else { + _logger.printTrace('[lldb]: $line'); + _logCompleter?.checkForMatch(line); + } + }); final StreamSubscription stderrSubscription = _lldbProcess!.stderr .transform(utf8LineDecoder) @@ -257,12 +233,9 @@ if not error.Success(): _breakpointPattern, ).then((value) => value, onError: _handleAsyncError); - final Version? xcodeVersion = _xcode.currentVersion; - final bool useManualContinue = xcodeVersion != null && (xcodeVersion >= Version(26, 4, 0)); - final breakpointSetCommand = useManualContinue - ? r"breakpoint set --func-regex '^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$'" - : r"breakpoint set --auto-continue true --func-regex '^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$'"; - await _lldbProcess?.stdinWriteln(breakpointSetCommand); + await _lldbProcess?.stdinWriteln( + r"breakpoint set --func-regex '^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$'", + ); final String log = await futureLog; final Match? match = _breakpointPattern.firstMatch(log); final String? breakpointId = match?.group(1); @@ -275,12 +248,20 @@ if not error.Success(): await _lldbProcess?.stdinWriteln('breakpoint command add --script-type python $breakpointId'); await _lldbProcess?.stdinWriteln(_pythonScript); await _lldbProcess?.stdinWriteln('DONE'); + + // Disable asynchronous mode to workaround issues with rearming of breakpoints. + // See https://github.com/flutter/flutter/issues/184254 and upstream issue + // https://github.com/llvm/llvm-project/issues/190956. + await _lldbProcess?.stdinWriteln('script lldb.debugger.SetAsync(False)'); } /// Resume the stopped process. - Future _resumeProcess() async { + Future _resumeProcess(BuildMode mode) async { final Future futureLog = _startWaitingForLog( - _lldbProcessResuming, + // When using debug mode, a breakpoint is added once the process resumes and no resume log + // is shown. Instead we match on the breakpoint added log. In profile mode, a resume log is + // shown once the process resumes and no breakpoint log is shown. + mode == BuildMode.debug ? _lldbBreakpointAdded : _lldbProcessResuming, ).then((value) => value, onError: _handleAsyncError); await _lldbProcess?.stdinWriteln('process continue'); diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 76cce955b1cc1..ed8a1b1743d26 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -25,6 +25,7 @@ import '../features.dart'; import '../flutter_manifest.dart'; import '../globals.dart' as globals; import '../macos/cocoapod_utils.dart'; +import '../macos/darwin_dependency_management.dart'; import '../macos/swift_package_manager.dart'; import '../macos/xcode.dart'; import '../migrations/lldb_init_migration.dart'; @@ -36,6 +37,7 @@ import '../migrations/xcode_script_build_phase_migration.dart'; import '../migrations/xcode_thin_binary_build_phase_input_paths_migration.dart'; import '../plugins.dart'; import '../project.dart'; +import '../shorebird/ios_build_trace_session.dart'; import 'application_package.dart'; import 'code_signing.dart'; import 'migrations/host_app_info_plist_migration.dart'; @@ -148,7 +150,7 @@ Future buildXcodeProject({ } final FlutterProject project = FlutterProject.current(); - + const FlutterDarwinPlatform darwinPlatform = FlutterDarwinPlatform.ios; final migrators = [ RemoveFrameworkLinkAndEmbeddingMigration(app.project, globals.logger, globals.analytics), XcodeBuildSystemMigration(app.project, globals.logger), @@ -163,7 +165,7 @@ Future buildXcodeProject({ UIApplicationMainDeprecationMigration(app.project, globals.logger), SwiftPackageManagerIntegrationMigration( app.project, - FlutterDarwinPlatform.ios, + darwinPlatform, buildInfo, xcodeProjectInterpreter: globals.xcodeProjectInterpreter!, logger: globals.logger, @@ -202,6 +204,15 @@ Future buildXcodeProject({ globals.logger, ); + await DarwinDependencyManagement.validatePluginSupport( + platform: darwinPlatform, + xcodeProject: project.ios, + plugins: await project.ios.getPlugins(), + fileSystem: globals.fs, + logger: globals.logger, + cocoapods: globals.cocoaPods, + ); + final XcodeProjectInfo? projectInfo = await app.project.projectInfo(); if (projectInfo == null) { globals.printError('Xcode project not found.'); @@ -289,15 +300,13 @@ Future buildXcodeProject({ : null; final bool incrementalBuild = targetBuildDir != null && targetBuildDir.existsSync(); - final buildCommands = [ - ...(await globals.xcode!.xcodebuildProjectCommand( - app.project.hostAppRoot.path, - globals.fs.directory(buildDirectoryPath), - skipPackageResolution: false, - )), - '-configuration', - configuration, - ]; + final List xcodebuildCommandArgs = await globals.xcode! + .fetchDependenciesAndGenerateXcodebuildArgs( + app.project, + globals.fs.directory(buildDirectoryPath), + skipPackageUpdatesAndValidation: false, + ); + final buildCommands = [...xcodebuildCommandArgs, '-configuration', configuration]; // Check the public headers before checking Xcode version so headers fingerprinter is created // regardless of Xcode version. @@ -345,17 +354,30 @@ Future buildXcodeProject({ final String? iosDeploymentTarget = buildSettings['IPHONEOS_DEPLOYMENT_TARGET']; if (iosDeploymentTarget != null) { SwiftPackageManager.updateMinimumDeployment( - platform: FlutterDarwinPlatform.ios, + platform: darwinPlatform, project: project.ios, deploymentTarget: iosDeploymentTarget, ); } } + // Created early enough to capture pod install, which can be minutes + // on plugin-heavy apps โ€” otherwise it's an invisible gap between + // "flutter build ios" starting and the xcode archive span. + final IosBuildTraceSession? traceSession = IosBuildTraceSession.maybeStart( + shorebirdTraceFilePath: buildInfo.shorebirdTraceFilePath, + fileSystem: globals.fs, + buildDirectoryPath: globals.fs.path.join(getBuildDirectory(), 'ios'), + ); + + traceSession?.onBeforePodInstall(); await processPodsIfNeeded(project.ios, buildDirectoryPath, buildInfo.mode); + traceSession?.onAfterPodInstall(); if (configOnly) { return XcodeBuildResult(success: true); } + buildCommands.addAll(traceSession?.extraBuildCommands() ?? const []); + if (globals.logger.isVerbose) { // An environment variable to be passed to xcode_backend.sh determining // whether to echo back executed commands. @@ -534,6 +556,8 @@ Future buildXcodeProject({ ]); } + traceSession?.onXcodeAboutToStart(); + final sw = Stopwatch()..start(); initialBuildStatus = globals.logger.startProgress('Running Xcode build...'); @@ -558,6 +582,13 @@ Future buildXcodeProject({ ), ); + await traceSession?.onXcodeFinished( + buildActionName: xcodeBuildActionToString(buildAction), + resultBundleDirectory: resultBundleDirectory, + runXcresultTool: (List args) => globals.processManager.run(args), + ); + traceSession?.finish(printStatus: globals.printStatus); + if (tempDir.existsSync()) { // Display additional warning and error message from xcresult bundle. final Directory resultBundle = tempDir.childDirectory(_kResultBundlePath); diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart index 527beef95fce8..9b03cffc07a14 100644 --- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart +++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart @@ -20,8 +20,8 @@ import '../base/terminal.dart'; import '../base/utils.dart'; import '../base/version.dart'; import '../build_info.dart'; -import '../convert.dart'; import '../reporting/reporting.dart'; +import '../xcode_project.dart'; final _settingExpr = RegExp(r'(\w+)\s*=\s*(.*)$'); final _varExpr = RegExp(r'\$\(([^)]*)\)'); @@ -177,7 +177,7 @@ class XcodeProjectInterpreter { return xcrunCommand; } - /// Prefetches SwiftPM dependencies needed for `xcodebuild` command and then returns a list of + /// Prefetches Swift package dependencies for the project and then returns a list of /// required arguments for the `xcodebuild` Xcode project command. /// /// This is not required when running commands that don't require a project (e.g. @@ -185,29 +185,29 @@ class XcodeProjectInterpreter { /// /// Using this method when running `xcodebuild` commands ensures that `xcrun` is used properly /// and that the Swift package cache is properly configured. - /// - /// When [skipPackageResolution] is true, it uses arguments to attempt skipping any Swift package - /// resolution or updates. This should be false when running [prefetchSwiftPackages], so packages - /// should already be resolved, downloaded, and updated on subsquent `xcodebuild` commands. - Future> xcodebuildProjectCommand( - String projectPath, + Future> fetchDependenciesAndGenerateXcodebuildArgs( + XcodeBasedProject xcodeProject, Directory buildDirectory, { - bool skipPackageResolution = true, + bool skipPackageUpdatesAndValidation = true, }) async { // All `xcodebuild` project commands will download and resolve Swift packages. // We should always prefetch Swift packages before running any `xcodebuild` project command // to control the output. - await prefetchSwiftPackages(projectPath, buildDirectory: buildDirectory, quiet: false); + await prefetchSwiftPackagesForProject(xcodeProject, buildDirectory: buildDirectory); return _xcodebuildProjectCommandArguments( buildDirectory, - skipPackageResolution: skipPackageResolution, + skipPackageUpdatesAndValidation: skipPackageUpdatesAndValidation, ); } + /// Returns a list of required arguments for the `xcodebuild` Xcode project command. + /// + /// When [skipPackageUpdatesAndValidation] is true, it uses arguments to attempt skipping any + /// Swift package updates and validation. List _xcodebuildProjectCommandArguments( Directory buildDirectory, { - bool skipPackageResolution = true, + bool skipPackageUpdatesAndValidation = true, }) { final String cachePath = buildDirectory .childDirectory(kSwiftPackageCacheDirectoryName) @@ -218,8 +218,7 @@ class XcodeProjectInterpreter { 'xcodebuild', '-clonedSourcePackagesDirPath', cachePath, - if (skipPackageResolution) ...[ - '-disableAutomaticPackageResolution', + if (skipPackageUpdatesAndValidation) ...[ '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', @@ -233,7 +232,7 @@ class XcodeProjectInterpreter { /// If [XcodeProjectBuildContext.scheme] is `null`, `xcodebuild` will /// return build settings for the first discovered target (by default this is Runner). Future> getBuildSettings( - String projectPath, { + XcodeBasedProject xcodeProject, { required XcodeProjectBuildContext buildContext, Duration timeout = const Duration(minutes: 1), }) async { @@ -247,8 +246,13 @@ class XcodeProjectInterpreter { XcodeSdk.IPhoneOS || XcodeSdk.IPhoneSimulator => getIosBuildDirectory(), XcodeSdk.WatchOS || XcodeSdk.WatchSimulator => getIosBuildDirectory(), }; + final List xcodebuildCommandArgs = await fetchDependenciesAndGenerateXcodebuildArgs( + xcodeProject, + _fileSystem.directory(buildDir), + ); + final String projectPath = xcodeProject.xcodeProject.path; final showBuildSettingsCommand = [ - ...(await xcodebuildProjectCommand(projectPath, _fileSystem.directory(buildDir))), + ...xcodebuildCommandArgs, '-project', _fileSystem.path.absolute(projectPath), if (scheme != null) ...['-scheme', scheme], @@ -357,14 +361,19 @@ class XcodeProjectInterpreter { } Future cleanWorkspace( + XcodeBasedProject xcodeProject, String workspacePath, String scheme, { required Directory buildDirectory, bool verbose = false, }) async { final String projectPath = _fileSystem.currentDirectory.path; + final List xcodebuildCommandArgs = await fetchDependenciesAndGenerateXcodebuildArgs( + xcodeProject, + buildDirectory, + ); await _processUtils.run([ - ...(await xcodebuildProjectCommand(projectPath, buildDirectory)), + ...xcodebuildCommandArgs, '-workspace', workspacePath, '-scheme', @@ -375,95 +384,25 @@ class XcodeProjectInterpreter { ], workingDirectory: projectPath); } - /// The process used to fetch Swift packages. - Process? _swiftPackageFetchProcess; - - /// The stdout subscription for the Swift package fetch process. - StreamSubscription? _swiftPackageFetchStdoutSubscription; - - /// The stderr subscription for the Swift package fetch process. - StreamSubscription? _swiftPackageFetchStderrSubscription; - /// Prefetches Swift packages for the given Xcode project. - /// - /// If a process is already running from a previous Flutter command, kill it before starting - /// the command. If the process is already running from the same Flutter command, wait for it to - /// complete if [waitForCompletion] is true. - /// - /// If [quiet] is false, it will print a spinner while the command is running and print logs of - /// what Swift packages are being fetched. - Future prefetchSwiftPackages( - String projectPath, { + Future prefetchSwiftPackagesForProject( + XcodeBasedProject xcodeProject, { required Directory buildDirectory, - bool quiet = true, - bool waitForCompletion = true, }) async { - Status? status; - try { - final command = [ - ..._xcodebuildProjectCommandArguments(buildDirectory, skipPackageResolution: false), - '-resolvePackageDependencies', - ]; - if (_swiftPackageFetchProcess == null) { - // Check if process is already running from a previous Flutter command. If it is, kill it - // so we don't have the process running twice. When this process is run twice, it'll cause - // one to error. The new process will pick up where the old one left off. - final RunResult result = await _processUtils.run(['pgrep', '-n', ...command]); - if (result.exitCode == 0) { - final int? pid = int.tryParse(result.stdout.trim()); - if (pid != null) { - _logger.printTrace( - 'Swift Package Manager dependencies are already being fetched by PID $pid', - ); - await _processUtils.run(['kill', '$pid']); - } - } - } - - final Process process = - _swiftPackageFetchProcess ?? - await _processUtils.start(command, workingDirectory: projectPath); - _swiftPackageFetchProcess ??= process; - if (!waitForCompletion) { - return; - } - if (!quiet) { - var printFetchWarnings = false; - _swiftPackageFetchStdoutSubscription ??= process.stdout - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((String line) { - if (line.startsWith('Fetching')) { - status?.cancel(); - if (!printFetchWarnings) { - _logger.printStatus( - 'Xcode is fetching Swift Package Manager dependencies. This may take several minutes...', - ); - printFetchWarnings = true; - } - status = _logger.startProgress(' $line...'); - } - }); - } - final stderrBuffer = StringBuffer(); - _swiftPackageFetchStderrSubscription ??= process.stderr - .transform(const Utf8Decoder(reportErrors: false)) - .listen(stderrBuffer.write); - - final int exitCode = await process.exitCode.whenComplete(() async { - await _swiftPackageFetchStdoutSubscription?.cancel(); - await _swiftPackageFetchStderrSubscription?.cancel(); - }); - if (exitCode != 0) { - throwToolExit('Xcode failed to resolve Swift Package Manager dependencies:\n$stderrBuffer'); - } - } finally { - status?.cancel(); - } + await xcodeProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: _xcodebuildProjectCommandArguments( + buildDirectory, + // skipPackageUpdatesAndValidation should be false so that when subsequent xcodebuild + // commands run, packages should already be resolved, downloaded, updated, and validated. + skipPackageUpdatesAndValidation: false, + ), + processUtils: _processUtils, + logger: _logger, + ); } Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { @@ -474,15 +413,19 @@ class XcodeProjectInterpreter { // The exit code returned by 'xcodebuild -list' when the project is corrupted. const corruptedProjectExitCode = 74; bool allowedFailures(int c) => c == missingProjectExitCode || c == corruptedProjectExitCode; + final List xcodebuildCommandArgs = await fetchDependenciesAndGenerateXcodebuildArgs( + xcodeProject, + buildDirectory, + ); final RunResult result = await _processUtils.run( [ - ...(await xcodebuildProjectCommand(projectPath, buildDirectory)), + ...xcodebuildCommandArgs, '-list', if (projectFilename != null) ...['-project', projectFilename], ], throwOnError: true, allowedFailures: allowedFailures, - workingDirectory: projectPath, + workingDirectory: xcodeProject.hostAppRoot.path, ); if (allowedFailures(result.exitCode)) { // User configuration error, tool exit instead of crashing. diff --git a/packages/flutter_tools/lib/src/isolated/native_assets/linux/native_assets.dart b/packages/flutter_tools/lib/src/isolated/native_assets/linux/native_assets.dart index 756f2ed088b16..546889ec41fca 100644 --- a/packages/flutter_tools/lib/src/isolated/native_assets/linux/native_assets.dart +++ b/packages/flutter_tools/lib/src/isolated/native_assets/linux/native_assets.dart @@ -17,7 +17,10 @@ import '../../../globals.dart' as globals; /// /// Flutter also builds code assets for widget tests. Since there is no app build in that context, /// [cmakeDirectory] should be set to null for those builds. -Future cCompilerConfigLinux({Directory? cmakeDirectory}) async { +Future cCompilerConfigLinux({ + Directory? cmakeDirectory, + bool throwIfNotFound = true, +}) async { if (cmakeDirectory == null) { // No CMake reference (e.g. for a widget test). Hooks can resolve to any // compiler. @@ -27,9 +30,12 @@ Future cCompilerConfigLinux({Directory? cmakeDirectory}) async // For app builds, use the same compiler as the native/GTK parts of the app. final File cmakeCacheTxt = cmakeDirectory.childFile('CMakeCache.txt'); if (!cmakeCacheTxt.existsSync()) { - throwToolExit( - 'Could not read compiler configurations for build hooks, expected ${cmakeCacheTxt.path} to exist.', - ); + if (throwIfNotFound) { + throwToolExit( + 'Could not read compiler configurations for build hooks, expected ${cmakeCacheTxt.path} to exist.', + ); + } + return null; } const archiverVariable = 'CMAKE_AR'; @@ -76,19 +82,31 @@ Future cCompilerConfigLinux({Directory? cmakeDirectory}) async return file.uri; } - // Find clang next to the clang++ we use in CMake - File clangPpFile = globals.fs.file(requireTool(cxxCompiler, compilerVariable)); - clangPpFile = globals.fs.file(await clangPpFile.resolveSymbolicLinks()); - final File clangFile = clangPpFile.parent.childFile('clang'); - if (!clangFile.existsSync()) { - throwToolExit('Expected to find clang next to ${clangPpFile.path}'); - } + try { + // Find clang next to the clang++ we use in CMake + File clangPpFile = globals.fs.file(requireTool(cxxCompiler, compilerVariable)); + clangPpFile = globals.fs.file(await clangPpFile.resolveSymbolicLinks()); + final File clangFile = clangPpFile.parent.childFile('clang'); + if (!clangFile.existsSync()) { + throwToolExit('Expected to find clang next to ${clangPpFile.path}'); + } - return CCompilerConfig( - compiler: clangFile.uri, - linker: requireTool(linker, linkerVariable), - archiver: requireTool(archiver, archiverVariable), - ); + return CCompilerConfig( + compiler: clangFile.uri, + linker: requireTool(linker, linkerVariable), + archiver: requireTool(archiver, archiverVariable), + ); + } on ToolExit { + if (throwIfNotFound) { + rethrow; + } + return null; + } on FileSystemException { + if (throwIfNotFound) { + rethrow; + } + return null; + } } // Format: `VARIABLE_NAME:TYPE=value`; diff --git a/packages/flutter_tools/lib/src/isolated/native_assets/targets.dart b/packages/flutter_tools/lib/src/isolated/native_assets/targets.dart index b2f0c4719894a..e4726d0a48fb0 100644 --- a/packages/flutter_tools/lib/src/isolated/native_assets/targets.dart +++ b/packages/flutter_tools/lib/src/isolated/native_assets/targets.dart @@ -276,12 +276,11 @@ final class LinuxAssetTarget extends CodeAssetTarget { @override Future setCCompilerConfig({bool mustMatchAppBuild = true}) async { - if (cmakeBuildDirectory == null && mustMatchAppBuild) { - throw StateError('Missing CMake build directory on LinuxAssetTarget'); - } + final bool isNativeAppBuild = cmakeBuildDirectory != null && cmakeBuildDirectory!.existsSync(); cCompilerConfigSync = await cCompilerConfigLinux( - cmakeDirectory: mustMatchAppBuild ? cmakeBuildDirectory! : null, + cmakeDirectory: isNativeAppBuild ? cmakeBuildDirectory : null, + throwIfNotFound: mustMatchAppBuild, ); } 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 ea4d4dbe5e4c4..6de180f85e17e 100644 --- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart @@ -392,6 +392,13 @@ class ResidentWebRunner extends ResidentRunner { appFailedToStart(); _logger.printError(error.toString(), stackTrace: stackTrace); throwToolExit(kExitMessage); + } on TimeoutException catch (error, stackTrace) { + appFailedToStart(); + _logger.printError( + 'Failed to establish connection with the web debug service: $error', + stackTrace: stackTrace, + ); + throwToolExit('Failed to connect to the web debug service.'); } on DartDevelopmentServiceException catch (error) { // The application may have started shutting down before DDS was able to finish establishing // its connection to DWDS. Don't treat this as an unhandled exception. diff --git a/packages/flutter_tools/lib/src/isolated/web_asset_server.dart b/packages/flutter_tools/lib/src/isolated/web_asset_server.dart index 7fdca6ee8818c..3768c2b94f07f 100644 --- a/packages/flutter_tools/lib/src/isolated/web_asset_server.dart +++ b/packages/flutter_tools/lib/src/isolated/web_asset_server.dart @@ -116,15 +116,17 @@ class WebAssetServer implements AssetReader { } } - static const _reloadedSourcesFileName = 'reloaded_sources.json'; + // Use relative path for the URI so the app can still find it even if it's in + // a different domain than the server. + @visibleForTesting + static Uri reloadedSourcesUri = Uri.parse('reloaded_sources.json'); - /// Given a list of [modules] that need to be reloaded during a hot restart or - /// hot reload, writes a file that contains a list of objects each with three - /// fields: + /// Given a list of [modulePaths] that need to be reloaded during a hot + /// restart or hot reload, writes a file that contains a list of objects each + /// with three fields: /// - /// `src`: A string that corresponds to the file path containing a DDC library - /// bundle. To support embedded libraries, the path should include the - /// `baseUri` of the web server. + /// `src`: A string that corresponds to the file path relative to the app base + /// URL root that contains the DDC library bundle. /// `module`: The name of the library bundle in `src`. /// `libraries`: An array of strings containing the libraries that were /// compiled in `src`. @@ -133,7 +135,7 @@ class WebAssetServer implements AssetReader { /// ```json /// [ /// { - /// "src": "/", + /// "src": "/", /// "module": "", /// "libraries": ["", ""], /// }, @@ -142,22 +144,28 @@ class WebAssetServer implements AssetReader { /// /// The path of the output file should stay consistent across the lifetime of /// the app. - void writeReloadedSources(List modules) { + void writeReloadedSources(List modulePaths) { final moduleToLibrary = >[]; - for (final module in modules) { + for (final relativeModulePath in modulePaths) { final metadata = ModuleMetadata.fromJson( - json.decode(utf8.decode(_webMemoryFS.metadataFiles['$module.metadata']!.toList())) + json.decode( + utf8.decode(_webMemoryFS.metadataFiles['$relativeModulePath.metadata']!.toList()), + ) as Map, ); final List libraries = metadata.libraries.keys.toList(); - final moduleUri = '$baseUri/$module'; moduleToLibrary.add({ - 'src': moduleUri, + // Use only the path for the module so the app can still find it even if + // it's in a different domain than the server. + // TODO(srujzs): We use a `/` prefix to match the path that DWDS gets + // when parsing the parsed URL. It may be cleaner to just remove the `/` + // in DWDS rather than add it here. + 'src': '/$relativeModulePath', 'module': metadata.name, 'libraries': libraries, }); } - writeFile(_reloadedSourcesFileName, json.encode(moduleToLibrary)); + writeFile(reloadedSourcesUri.path, json.encode(moduleToLibrary)); } @visibleForTesting @@ -358,10 +366,7 @@ class WebAssetServer implements AssetReader { canaryFeatures: canaryFeatures, ), packageConfigPath: buildInfo.packageConfigPath, - reloadedSourcesUri: server._baseUri.replace( - pathSegments: List.from(server._baseUri.pathSegments) - ..add(_reloadedSourcesFileName), - ), + reloadedSourcesUri: reloadedSourcesUri, ).strategy : FrontendServerRequireStrategyProvider( ReloadConfiguration.none, diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart index ae96de94cc9e6..baf6e71f15da2 100644 --- a/packages/flutter_tools/lib/src/macos/build_macos.dart +++ b/packages/flutter_tools/lib/src/macos/build_macos.dart @@ -27,6 +27,7 @@ import '../migrations/xcode_thin_binary_build_phase_input_paths_migration.dart'; import '../project.dart'; import 'application_package.dart'; import 'cocoapod_utils.dart'; +import 'darwin_dependency_management.dart'; import 'migrations/flutter_application_migration.dart'; import 'migrations/macos_deployment_target_migration.dart'; import 'migrations/nsapplicationmain_deprecation_migration.dart'; @@ -81,7 +82,7 @@ Future buildMacOS({ 'to learn about adding macOS support to a project.', ); } - + const FlutterDarwinPlatform darwinPlatform = FlutterDarwinPlatform.macos; final migrators = [ RemoveMacOSFrameworkLinkAndEmbeddingMigration( flutterProject.macos, @@ -97,7 +98,7 @@ Future buildMacOS({ SecureRestorableStateMigration(flutterProject.macos, globals.logger), SwiftPackageManagerIntegrationMigration( flutterProject.macos, - FlutterDarwinPlatform.macos, + darwinPlatform, buildInfo, xcodeProjectInterpreter: globals.xcodeProjectInterpreter!, logger: globals.logger, @@ -112,6 +113,15 @@ Future buildMacOS({ final migration = ProjectMigration(migrators); await migration.run(); + await DarwinDependencyManagement.validatePluginSupport( + platform: darwinPlatform, + xcodeProject: flutterProject.macos, + plugins: await flutterProject.macos.getPlugins(), + fileSystem: globals.fs, + logger: globals.logger, + cocoapods: globals.cocoaPods, + ); + final String buildDirectoryPath = getMacOSBuildDirectory(); final Directory flutterBuildDir = flutterProject.directory.childDirectory(buildDirectoryPath); if (!flutterBuildDir.existsSync()) { @@ -125,7 +135,7 @@ Future buildMacOS({ // regardless of the project name so long as there is exactly one project. final String? xcodeProjectName = xcodeProject.existsSync() ? xcodeProject.basename : null; final XcodeProjectInfo? projectInfo = await globals.xcodeProjectInterpreter?.getInfo( - xcodeProject.parent.path, + flutterProject.macos, projectFilename: xcodeProjectName, buildDirectory: flutterBuildDir, ); @@ -158,7 +168,7 @@ Future buildMacOS({ final String? macOSDeploymentTarget = buildSettings['MACOSX_DEPLOYMENT_TARGET']; if (macOSDeploymentTarget != null) { SwiftPackageManager.updateMinimumDeployment( - platform: FlutterDarwinPlatform.macos, + platform: darwinPlatform, project: flutterProject.macos, deploymentTarget: macOSDeploymentTarget, ); @@ -215,14 +225,16 @@ Future buildMacOS({ final String? excludedArches = buildSettings['EXCLUDED_ARCHS']; try { + final List xcodebuildCommandArgs = await globals.xcode! + .fetchDependenciesAndGenerateXcodebuildArgs( + flutterProject.macos, + globals.fs.directory(buildDirectoryPath), + skipPackageUpdatesAndValidation: false, + ); result = await globals.processUtils.stream( [ '/usr/bin/env', - ...(await globals.xcode!.xcodebuildProjectCommand( - flutterProject.macos.hostAppRoot.path, - globals.fs.directory(buildDirectoryPath), - skipPackageResolution: false, - )), + ...xcodebuildCommandArgs, '-workspace', xcodeWorkspace.path, '-configuration', diff --git a/packages/flutter_tools/lib/src/macos/cocoapods.dart b/packages/flutter_tools/lib/src/macos/cocoapods.dart index 7245c7636aae0..5ab6f80a81916 100644 --- a/packages/flutter_tools/lib/src/macos/cocoapods.dart +++ b/packages/flutter_tools/lib/src/macos/cocoapods.dart @@ -2,8 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; + import 'package:file/file.dart'; import 'package:process/process.dart'; +import 'package:shorebird_build_trace/shorebird_build_trace.dart'; import 'package:unified_analytics/unified_analytics.dart'; import '../base/common.dart'; @@ -348,16 +351,28 @@ class CocoaPods { Future _runPodInstall(XcodeBasedProject xcodeProject, BuildMode buildMode) async { final Status status = _logger.startProgress('Running pod install...'); - final ProcessResult result = await _processManager.run( - ['pod', 'install', '--verbose'], - workingDirectory: _fileSystem.path.dirname(xcodeProject.podfile.path), - environment: { - // See https://github.com/flutter/flutter/issues/10873. - // CocoaPods analytics adds a lot of latency. - 'COCOAPODS_DISABLE_STATS': 'true', - 'LANG': 'en_US.UTF-8', - }, - ); + + // Shorebird-specific: when a build trace is active, stream pod install + // output so we can timestamp phase transitions. On plugin-heavy apps + // pod install can be minutes, and a single opaque span hides where the + // time goes (dependency analysis vs downloads vs project generation vs + // integration). + final BuildTracer? tracer = BuildTracer.current; + final ProcessResult result; + if (tracer == null) { + result = await _processManager.run( + ['pod', 'install', '--verbose'], + workingDirectory: _fileSystem.path.dirname(xcodeProject.podfile.path), + environment: { + // See https://github.com/flutter/flutter/issues/10873. + // CocoaPods analytics adds a lot of latency. + 'COCOAPODS_DISABLE_STATS': 'true', + 'LANG': 'en_US.UTF-8', + }, + ); + } else { + result = await _runPodInstallStreamed(tracer, xcodeProject); + } status.stop(); if (_logger.isVerbose || result.exitCode != 0) { final stdout = result.stdout as String; @@ -386,6 +401,60 @@ class CocoaPods { } } + /// Runs `pod install --verbose` with live stdout streaming so phase + /// transitions can be timestamped into [tracer]. Returns a [ProcessResult] + /// shaped the same as [ProcessManager.run] would have returned, so + /// downstream error handling keeps working unchanged. + Future _runPodInstallStreamed( + BuildTracer tracer, + XcodeBasedProject xcodeProject, + ) async { + final Process process = await _processManager.start( + ['pod', 'install', '--verbose'], + workingDirectory: _fileSystem.path.dirname(xcodeProject.podfile.path), + environment: {'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'}, + ); + // Real pid of the `pod` process. Phase spans land on this pid + + // tid=1, with a `process_name` metadata event so Perfetto groups + // pod install work into its own row. + final int podPid = process.pid; + tracer + ..addProcessNameMetadata(pid: podPid, name: TraceNames.podInstallSpanName) + ..addThreadNameMetadata(pid: podPid, tid: 1, name: TraceNames.podInstallSpanName); + final phases = PhaseTracker( + tracer: tracer, + pid: podPid, + tid: 1, + namePrefix: TraceNames.podInstallNamePrefix, + ); + + final stdoutBuf = StringBuffer(); + final stderrBuf = StringBuffer(); + + final Future stdoutDone = process.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + stdoutBuf + ..write(line) + ..write('\n'); + final PodInstallPhase? phase = _podPhaseForLogLine(line); + if (phase != null) phases.transitionTo(phase.wireName); + }) + .asFuture(); + + final Future stderrDone = process.stderr + .transform(utf8.decoder) + .listen(stderrBuf.write) + .asFuture(); + + final int exitCode = await process.exitCode; + await Future.wait(>[stdoutDone, stderrDone]); + phases.end(); + + return ProcessResult(process.pid, exitCode, stdoutBuf.toString(), stderrBuf.toString()); + } + Future _diagnosePodInstallFailure( ProcessResult result, XcodeBasedProject xcodeProject, @@ -641,3 +710,27 @@ class CocoaPods { } } } + +/// CocoaPods verbose-mode log markers, in the order they print during a +/// normal `pod install`. A match means "from now on, the process is in +/// this phase" โ€” the `PhaseTracker` closes the prior phase and opens a +/// new one. +/// +/// These substrings have been stable across recent CocoaPods releases; +/// if CocoaPods renames one, the worst case is we miss a phase in the +/// trace โ€” pod install still succeeds. +const _podPhaseMarkers = { + 'Analyzing dependencies': PodInstallPhase.analyzing, + 'Downloading dependencies': PodInstallPhase.downloading, + 'Generating Pods project': PodInstallPhase.generating, + 'Integrating client project': PodInstallPhase.integrating, +}; + +/// Returns the phase a `pod install --verbose` log [line] transitions +/// into, or null if the line doesn't contain any of the known markers. +PodInstallPhase? _podPhaseForLogLine(String line) { + for (final MapEntry entry in _podPhaseMarkers.entries) { + if (line.contains(entry.key)) return entry.value; + } + return null; +} diff --git a/packages/flutter_tools/lib/src/macos/darwin_dependency_management.dart b/packages/flutter_tools/lib/src/macos/darwin_dependency_management.dart index d44babc451ead..d333091f99ff9 100644 --- a/packages/flutter_tools/lib/src/macos/darwin_dependency_management.dart +++ b/packages/flutter_tools/lib/src/macos/darwin_dependency_management.dart @@ -5,14 +5,12 @@ import 'package:unified_analytics/unified_analytics.dart'; import '../base/common.dart'; -import '../base/config.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; -import '../base/platform.dart'; import '../darwin/darwin.dart'; import '../features.dart'; import '../flutter_manifest.dart'; -import '../ios/xcodeproj.dart'; +import '../flutter_plugins.dart'; import '../plugins.dart'; import '../project.dart'; import 'cocoapods.dart'; @@ -25,39 +23,24 @@ import 'swift_package_manager.dart'; class DarwinDependencyManagement { DarwinDependencyManagement({ required FlutterProject project, - required List plugins, - required CocoaPods cocoapods, + required CocoaPods? cocoapods, required SwiftPackageManager swiftPackageManager, required FileSystem fileSystem, required FeatureFlags featureFlags, - required Logger logger, required Analytics analytics, - required Platform platform, - required XcodeProjectInterpreter? xcodeProjectInterpreter, - required Config? config, - }) : _config = config, - _xcodeProjectInterpreter = xcodeProjectInterpreter, - _project = project, - _plugins = plugins, + }) : _project = project, _cocoapods = cocoapods, _swiftPackageManager = swiftPackageManager, _fileSystem = fileSystem, _featureFlags = featureFlags, - _logger = logger, - _analytics = analytics, - _hostPlatform = platform; + _analytics = analytics; final FlutterProject _project; - final List _plugins; - final CocoaPods _cocoapods; + final CocoaPods? _cocoapods; final SwiftPackageManager _swiftPackageManager; final FileSystem _fileSystem; final FeatureFlags _featureFlags; - final Logger _logger; final Analytics _analytics; - final Platform _hostPlatform; - final XcodeProjectInterpreter? _xcodeProjectInterpreter; - final Config? _config; /// Generates/updates required files and project settings for Darwin /// Dependency Managers (CocoaPods and Swift Package Manager). Projects may @@ -71,19 +54,13 @@ class DarwinDependencyManagement { /// Swift Package Manager requires a generated Package.swift and certain /// settings in the Xcode project's project.pbxproj and xcscheme (done later /// before build). - Future setUp({required FlutterDarwinPlatform platform}) async { + Future setUp({ + required FlutterDarwinPlatform platform, + required List plugins, + }) async { final XcodeBasedProject xcodeProject = platform.xcodeProject(_project); if (xcodeProject.usesSwiftPackageManager) { - await _swiftPackageManager.generatePluginsSwiftPackage(_plugins, platform, xcodeProject); - - // Start the SwiftPM dependency resolution in the background. - await _xcodeProjectInterpreter?.prefetchSwiftPackages( - xcodeProject.hostAppRoot.path, - waitForCompletion: false, - buildDirectory: _fileSystem.directory( - platform.buildDirectory(config: _config, fileSystem: _fileSystem), - ), - ); + await _swiftPackageManager.generatePluginsSwiftPackage(plugins, platform, xcodeProject); } else if (xcodeProject.flutterPluginSwiftPackageInProjectSettings) { // If Swift Package Manager is not enabled but the project is already // integrated for Swift Package Manager, pass no plugins to the generator. @@ -102,14 +79,10 @@ class DarwinDependencyManagement { if (_project.isModule) { return; } - final ( - :int totalCount, - :int swiftPackageCount, - :int podCount, - ) = await _evaluatePluginsAndPrintWarnings( + final (:int totalCount, :int swiftPackageCount, :int podCount) = await _countPluginsPerManager( platform: platform, xcodeProject: xcodeProject, - hostPlatformIsMacOS: _hostPlatform.isMacOS, + plugins: plugins, ); final bool useCocoapods; @@ -120,16 +93,16 @@ class DarwinDependencyManagement { // is not empty, regardless of if plugins are CocoaPod compatible. This // is done because `processPodsIfNeeded` uses `hasPlugins` to determine // whether to run. - useCocoapods = _plugins.isNotEmpty; + useCocoapods = plugins.isNotEmpty; } if (useCocoapods) { - await _cocoapods.setupPodfile(xcodeProject); + await _cocoapods?.setupPodfile(xcodeProject); } /// The user may have a custom maintained Podfile that they're running `pod install` /// on themselves. else if (xcodeProject.podfile.existsSync() && xcodeProject.podfileLock.existsSync()) { - _cocoapods.addPodsDependencyToFlutterXcconfig(xcodeProject); + _cocoapods?.addPodsDependencyToFlutterXcconfig(xcodeProject); } final event = Event.flutterInjectDarwinPlugins( @@ -147,158 +120,252 @@ class DarwinDependencyManagement { ); _analytics.send(event); - - // Validate SwiftPM support when building a plugin's example app. - _validateExampleAppPluginSupportsSwiftPackageManager(platform: platform); } /// Returns count of total number of plugins, number of Swift Package Manager compatible plugins, /// and number of CocoaPods compatible plugins. A plugin can be both Swift Package Manager and /// CocoaPods compatible. - /// - /// If [hostPlatformIsMacOS], prints warnings when using a plugin incompatible with the available - /// Darwin Dependency Manager (Swift Package Manager or CocoaPods). - /// - /// If [hostPlatformIsMacOS], prints message prompting the user to deintegrate CocoaPods if - /// using all Swift Package plugins. - Future<({int totalCount, int swiftPackageCount, int podCount})> _evaluatePluginsAndPrintWarnings({ + Future<({int totalCount, int swiftPackageCount, int podCount})> _countPluginsPerManager({ required FlutterDarwinPlatform platform, required XcodeBasedProject xcodeProject, - required bool hostPlatformIsMacOS, + required List plugins, }) async { var pluginCount = 0; var swiftPackageCount = 0; var cocoapodCount = 0; - final Set cocoapodOnlyPlugins = {}; - for (final Plugin plugin in _plugins) { - if (plugin.platforms[platform.name] == null) { - continue; - } - final String? swiftPackagePath = plugin.pluginSwiftPackageManifestPath( + for (final plugin in plugins) { + final bool pluginSupportsSwiftPM = plugin.supportSwiftPackageManagerForPlatform( + _fileSystem, + platform.name, + ); + final bool pluginSupportCocoapods = plugin.supportCocoapodsForPlatform( _fileSystem, platform.name, ); - final bool swiftPackageManagerCompatible = - swiftPackagePath != null && _fileSystem.file(swiftPackagePath).existsSync(); - - final String? podspecPath = plugin.pluginPodspecPath(_fileSystem, platform.name); - final bool cocoaPodsCompatible = - podspecPath != null && _fileSystem.file(podspecPath).existsSync(); // If a plugin is missing both a Package.swift and Podspec, it won't be // included by either Swift Package Manager or Cocoapods. This can happen // when a plugin doesn't have native platform code. // For example, image_picker_macos only uses dart code. - if (!swiftPackageManagerCompatible && !cocoaPodsCompatible) { + if (!pluginSupportsSwiftPM && !pluginSupportCocoapods) { continue; } - pluginCount += 1; - if (swiftPackageManagerCompatible) { + if (pluginSupportsSwiftPM) { swiftPackageCount += 1; } - if (cocoaPodsCompatible) { + if (pluginSupportCocoapods) { cocoapodCount += 1; } + } + return (totalCount: pluginCount, swiftPackageCount: swiftPackageCount, podCount: cocoapodCount); + } - if (cocoaPodsCompatible && !swiftPackageManagerCompatible) { - cocoapodOnlyPlugins.add(plugin.name); - } + /// Validate that plugins are compatible with the project dependency management set up. + /// + /// This includes the following checks: + /// - Throws when using a SwiftPM-only plugin but SwiftPM is not enabled + /// - Warns when using CocoaPod-only plugins + /// - Warns when CocoaPods integration is removable + /// - Warns when building a plugin example app that doesn't support SwiftPM + static Future validatePluginSupport({ + required FlutterDarwinPlatform platform, + required XcodeBasedProject xcodeProject, + required List plugins, + required FileSystem fileSystem, + required Logger logger, + required CocoaPods? cocoapods, + }) async { + final bool projectUsesSwiftPM = + xcodeProject.usesSwiftPackageManager && + xcodeProject.flutterPluginSwiftPackageInProjectSettings; + + final swiftPackageOnlyPlugins = []; + final cocoapodOnlyPlugins = []; + final List filteredPlugins = resolvePluginImplementationsForPlatform( + plugins, + platform.pluginConfigKey, + quiet: true, + ); + for (final plugin in filteredPlugins) { + final bool pluginSupportsSwiftPM = plugin.supportSwiftPackageManagerForPlatform( + fileSystem, + platform.name, + ); + final bool pluginSupportCocoapods = plugin.supportCocoapodsForPlatform( + fileSystem, + platform.name, + ); - // If not using Swift Package Manager and plugin does not have podspec - // but does have a Package.swift, throw an error. Otherwise, it'll error - // when it builds. - if (hostPlatformIsMacOS && - !xcodeProject.usesSwiftPackageManager && - !cocoaPodsCompatible && - swiftPackageManagerCompatible) { - if (xcodeProject.compatibleWithSwiftPackageManager) { - throwToolExit( - 'Plugin ${plugin.name} is only compatible with Swift Package Manager. Try ' - 'enabling Swift Package Manager by running ' - '"flutter config --enable-swift-package-manager" or remove the ' - 'plugin as a dependency.', - ); - } else { - throwToolExit( - 'Plugin ${plugin.name} is only compatible with Swift Package Manager, but your ' - 'project does not currently support it. To support Swift Package Manager:\n' - ' - Ensure Xcode 15+ is being used ' - ' - Enable Swift Package Manager feature by running ' - '"flutter config --enable-swift-package-manager"', - ); - } + // If a plugin is missing both a Package.swift and Podspec, it won't be + // included by either Swift Package Manager or Cocoapods. This can happen + // when a plugin doesn't have native platform code. + // For example, image_picker_macos only uses dart code. + if (!pluginSupportsSwiftPM && !pluginSupportCocoapods) { + continue; + } + if (pluginSupportsSwiftPM && !pluginSupportCocoapods) { + swiftPackageOnlyPlugins.add(plugin.name); + } + if (!pluginSupportsSwiftPM && pluginSupportCocoapods) { + cocoapodOnlyPlugins.add(plugin.name); } } - if (hostPlatformIsMacOS && cocoapodOnlyPlugins.isNotEmpty) { - _logger.printWarning( - 'The following plugins do not support Swift Package Manager for ${platform.name}:\n' - ' - ${cocoapodOnlyPlugins.join('\n - ')}\n' - 'This will become an error in a future version of Flutter. Please contact the plugin ' - 'maintainers to request Swift Package Manager adoption.', + _throwToolExitIfSwiftPMOnlyPluginsExist( + swiftPackageOnlyPlugins: swiftPackageOnlyPlugins, + xcodeProject: xcodeProject, + projectUsesSwiftPM: projectUsesSwiftPM, + ); + + _printCocoapodOnlyPluginsWarning( + cocoapodOnlyPlugins: cocoapodOnlyPlugins, + logger: logger, + platform: platform, + ); + + await _printRemoveCocoapodIntegrationMessage( + cocoapods: cocoapods, + cocoapodOnlyPlugins: cocoapodOnlyPlugins, + logger: logger, + platform: platform, + xcodeProject: xcodeProject, + projectUsesSwiftPM: projectUsesSwiftPM, + ); + + // Prompt plugin author when local plugin does not support SwiftPM + _validateExampleAppPluginSupportsSwiftPackageManager( + fileSystem: fileSystem, + logger: logger, + platform: platform, + project: xcodeProject.parent, + plugins: plugins, + ); + } + + /// Throw a [ToolExit] if there are plugins that only support SwiftPM but the project does not + /// use SwiftPM. Otherwise, it'll error later when it builds. + static void _throwToolExitIfSwiftPMOnlyPluginsExist({ + required List swiftPackageOnlyPlugins, + required XcodeBasedProject xcodeProject, + required bool projectUsesSwiftPM, + }) { + if (projectUsesSwiftPM || swiftPackageOnlyPlugins.isEmpty) { + return; + } + if (xcodeProject.compatibleWithSwiftPackageManager) { + throwToolExit( + 'The following plugin(s) are only compatible with Swift Package Manager:\n' + ' - ${swiftPackageOnlyPlugins.join('\n - ')}\n\n' + 'Try enabling Swift Package Manager by running ' + '"flutter config --enable-swift-package-manager" or remove the ' + 'plugin as a dependency.', + ); + } else { + throwToolExit( + 'The following plugin(s) are only compatible with Swift Package Manager:\n' + ' - ${swiftPackageOnlyPlugins.join('\n - ')}\n\n' + 'Your project does not currently support Swift Package Manager. To support Swift Package Manager:\n' + ' - Ensure Xcode 15+ is being used ' + ' - Enable Swift Package Manager feature by running ' + '"flutter config --enable-swift-package-manager"', ); } + } - // Only show warnings to remove CocoaPods if the project is using Swift - // Package Manager, has already been migrated to have SPM integration, and - // all plugins are Swift Packages. - if (xcodeProject.usesSwiftPackageManager && - xcodeProject.flutterPluginSwiftPackageInProjectSettings && - pluginCount == swiftPackageCount && - swiftPackageCount != 0) { - final bool podfileExists = xcodeProject.podfile.existsSync(); - if (podfileExists) { - // If all plugins are Swift Packages and the Podfile matches the - // default template, recommend pod deintegration. - final File podfileTemplate = await _cocoapods.getPodfileTemplate( - xcodeProject, - xcodeProject.xcodeProject, - ); - - final configWarning = - '${_podIncludeInConfigWarning(xcodeProject, 'Debug')}' - '${_podIncludeInConfigWarning(xcodeProject, 'Release')}'; - - if (hostPlatformIsMacOS && - xcodeProject.podfile.readAsStringSync() == podfileTemplate.readAsStringSync()) { - _logger.printWarning( - 'All plugins found for ${platform.name} are Swift Packages, but your ' - 'project still has CocoaPods integration. To remove CocoaPods ' - 'integration, complete the following steps:\n' - ' * In the ${platform.name}/ directory run "pod deintegrate"\n' - ' * Also in the ${platform.name}/ directory, delete the Podfile\n' - '$configWarning\n' - "Removing CocoaPods integration will improve the project's build time.", - ); - } else if (hostPlatformIsMacOS) { - // If all plugins are Swift Packages, but the Podfile has custom logic, - // recommend migrating manually. - _logger.printWarning( - 'All plugins found for ${platform.name} are Swift Packages, but your ' - 'project still has CocoaPods integration. Your project uses a ' - 'non-standard Podfile and will need to be migrated to Swift Package ' - 'Manager manually. Some steps you may need to complete include:\n' - ' * In the ${platform.name}/ directory run "pod deintegrate"\n' - ' * Transition any Pod dependencies to Swift Package equivalents. ' - 'See https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app\n' - ' * Transition any custom logic\n' - '$configWarning\n' - "Removing CocoaPods integration will improve the project's build time.", - ); - } - } + /// Print a warning when there are plugins that don't support SwiftPM and therefore require + /// CocoaPods. + static void _printCocoapodOnlyPluginsWarning({ + required List cocoapodOnlyPlugins, + required Logger logger, + required FlutterDarwinPlatform platform, + }) { + if (cocoapodOnlyPlugins.isEmpty) { + return; } + logger.printWarning( + 'The following plugins do not support Swift Package Manager for ${platform.name}:\n' + ' - ${cocoapodOnlyPlugins.join('\n - ')}\n' + 'This will become an error in a future version of Flutter. Please contact the plugin ' + 'maintainers to request Swift Package Manager adoption.', + ); + } - return (totalCount: pluginCount, swiftPackageCount: swiftPackageCount, podCount: cocoapodCount); + /// Print a message recommending removing CocoaPod integration when all plugins support SwiftPM. + static Future _printRemoveCocoapodIntegrationMessage({ + required CocoaPods? cocoapods, + required XcodeBasedProject xcodeProject, + required Logger logger, + required FlutterDarwinPlatform platform, + required List cocoapodOnlyPlugins, + required bool projectUsesSwiftPM, + }) async { + if (!projectUsesSwiftPM || cocoapodOnlyPlugins.isNotEmpty || cocoapods == null) { + return; + } + final bool podfileExists = xcodeProject.podfile.existsSync(); + if (!podfileExists) { + return; + } + final configWarning = + '${_podIncludeInConfigWarning(cocoapods: cocoapods, xcodeProject: xcodeProject, mode: 'Debug')}' + '${_podIncludeInConfigWarning(cocoapods: cocoapods, xcodeProject: xcodeProject, mode: 'Release')}'; + + if (await _podfileMatchesTemplate(cocoapods: cocoapods, xcodeProject: xcodeProject)) { + logger.printWarning( + 'All plugins found for ${platform.name} are Swift Packages, but your ' + 'project still has CocoaPods integration. To remove CocoaPods ' + 'integration, complete the following steps:\n' + ' * In the ${platform.name}/ directory run "pod deintegrate"\n' + ' * Also in the ${platform.name}/ directory, delete the Podfile\n' + '$configWarning\n' + "Removing CocoaPods integration will improve the project's build time.", + ); + } else { + // If all plugins are Swift Packages, but the Podfile has custom logic, + // recommend migrating manually. + logger.printWarning( + 'All plugins found for ${platform.name} are Swift Packages, but your ' + 'project still has CocoaPods integration. Your project uses a ' + 'non-standard Podfile and will need to be migrated to Swift Package ' + 'Manager manually. Some steps you may need to complete include:\n' + ' * In the ${platform.name}/ directory run "pod deintegrate"\n' + ' * Transition any Pod dependencies to Swift Package equivalents. ' + 'See https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app\n' + ' * Transition any custom logic\n' + '$configWarning\n' + "Removing CocoaPods integration will improve the project's build time.", + ); + } + } + + /// Return `true` if the Podfile matches the create template, meaning it hasn't been modified. + static Future _podfileMatchesTemplate({ + required CocoaPods cocoapods, + required XcodeBasedProject xcodeProject, + }) async { + // If all plugins are Swift Packages and the Podfile matches the + // default template, recommend pod deintegration. + final File podfileTemplate = await cocoapods.getPodfileTemplate( + xcodeProject, + xcodeProject.xcodeProject, + ); + + return xcodeProject.podfile.readAsStringSync() == podfileTemplate.readAsStringSync(); } - String _podIncludeInConfigWarning(XcodeBasedProject xcodeProject, String mode) { + /// Returns a step to remove the CocoaPods `#include` from the xcconfig file for the given mode. + /// If the xcconfig file does not include the CocoaPods `#include`, it returns an empty string. + static String _podIncludeInConfigWarning({ + required CocoaPods cocoapods, + required XcodeBasedProject xcodeProject, + required String mode, + }) { final File xcconfigFile = xcodeProject.xcodeConfigFor(mode); - final bool configIncludesPods = _cocoapods.xcconfigIncludesPods(xcconfigFile); + final bool configIncludesPods = cocoapods.xcconfigIncludesPods(xcconfigFile); if (configIncludesPods) { return ' * Remove the include to ' - '"${_cocoapods.includePodsXcconfig(mode)}" in your ' + '"${cocoapods.includePodsXcconfig(mode)}" in your ' '${xcconfigFile.parent.parent.basename}/${xcconfigFile.parent.basename}/${xcconfigFile.basename}\n'; } @@ -314,29 +381,43 @@ class DarwinDependencyManagement { /// to add SwiftPM support. /// 2. If the plugin has a Package.swift, validates that it contains a /// dependency on FlutterFramework. - void _validateExampleAppPluginSupportsSwiftPackageManager({ + static void _validateExampleAppPluginSupportsSwiftPackageManager({ required FlutterDarwinPlatform platform, + required FileSystem fileSystem, + required Logger logger, + required FlutterProject project, + required List plugins, }) { - final Plugin? parentPlugin = _loadPluginFromExampleProject(); + final Plugin? parentPlugin = _loadPluginFromExampleProject( + fileSystem: fileSystem, + logger: logger, + project: project, + plugins: plugins, + ); if (parentPlugin == null) { return; } final String? warning = validatePluginSupportsSwiftPackageManager( parentPlugin, - fileSystem: _fileSystem, + fileSystem: fileSystem, platform: platform.name, ); if (warning != null) { - _logger.printWarning(warning); + logger.printWarning(warning); } } /// Returns the parent plugin if the current project is a plugin's example app, /// or `null` otherwise. - Plugin? _loadPluginFromExampleProject() { - final Directory projectDir = _project.directory; + static Plugin? _loadPluginFromExampleProject({ + required FileSystem fileSystem, + required Logger logger, + required FlutterProject project, + required List plugins, + }) { + final Directory projectDir = project.directory; if (!projectDir.path.endsWith('example')) { return null; } @@ -351,11 +432,11 @@ class DarwinDependencyManagement { try { parentManifest = FlutterManifest.createFromPath( parentPubspec.path, - fileSystem: _fileSystem, - logger: _logger, + fileSystem: fileSystem, + logger: logger, ); } on Exception catch (e) { - _logger.printTrace('Failed to parse parent pubspec for SwiftPM validation: $e'); + logger.printTrace('Failed to parse parent pubspec for SwiftPM validation: $e'); return null; } @@ -364,7 +445,7 @@ class DarwinDependencyManagement { } final String pluginName = parentManifest.appName; - return _plugins.where((Plugin p) => p.name == pluginName).firstOrNull; + return plugins.where((Plugin p) => p.name == pluginName).firstOrNull; } /// Validates a plugin's Swift Package Manager compatibility for a given [platform]. diff --git a/packages/flutter_tools/lib/src/macos/swift_package_manager.dart b/packages/flutter_tools/lib/src/macos/swift_package_manager.dart index f35576e07dcbd..6c2d83670af80 100644 --- a/packages/flutter_tools/lib/src/macos/swift_package_manager.dart +++ b/packages/flutter_tools/lib/src/macos/swift_package_manager.dart @@ -173,8 +173,8 @@ class SwiftPackageManager { } final Link pluginSymlink = symlinkDirectory.childLink(basename); - ErrorHandlingFileSystem.deleteIfExists(pluginSymlink); - pluginSymlink.createSync(packagePath); + _createPluginSymlink(pluginSymlink: pluginSymlink, packagePath: packagePath); + final String packageRelativePath = _fileSystem.path.relative( pluginSymlink.path, from: pathRelativeTo, @@ -198,6 +198,57 @@ class SwiftPackageManager { return (packageDependencies, targetDependencies); } + /// Safely creates a symlink at [pluginSymlink] pointing to [packagePath]. + /// + /// If a symlink already exists and points to the correct target, creation is skipped + /// to avoid potential Xcode parallel target build race conditions. + /// If creation fails due to sharing violations or locks (e.g., when Xcode is open), + /// throws a descriptive [ToolExit] advising the user to close Xcode and run "flutter clean". + void _createPluginSymlink({required Link pluginSymlink, required String packagePath}) { + final FileSystemEntityType type = _fileSystem.typeSync(pluginSymlink.path, followLinks: false); + var skipCreation = false; + if (type == FileSystemEntityType.link) { + try { + if (pluginSymlink.targetSync() == packagePath) { + skipCreation = true; + } + } on FileSystemException catch (_) { + // If targetSync fails (e.g. broken link), proceed to delete. + } + } + + if (skipCreation) { + return; + } + + ErrorHandlingFileSystem.deleteIfExists(pluginSymlink, recursive: true); + + try { + pluginSymlink.createSync(packagePath); + } on FileSystemException catch (e) { + if (e.osError?.errorCode == 17) { + // OS Error: File exists, errno = 17 + final FileSystemEntityType postCrashType = _fileSystem.typeSync( + pluginSymlink.path, + followLinks: false, + ); + if (postCrashType == FileSystemEntityType.link) { + try { + if (pluginSymlink.targetSync() == packagePath) { + // Concurrently created by another parallel target build, and points to the correct target. + return; + } + } on FileSystemException catch (_) {} + } + } + throwToolExit( + 'Failed to create Swift Package plugin symlink at "${pluginSymlink.path}" to "$packagePath":\n' + '$e\n' + 'If Xcode is currently open, please close Xcode, run "flutter clean", and try building again.', + ); + } + } + /// Checks if the plugin has a dependency on another Flutter plugin and returns a list of paths /// that should be replaced in the [manifestContent]. /// diff --git a/packages/flutter_tools/lib/src/macos/xcdevice.dart b/packages/flutter_tools/lib/src/macos/xcdevice.dart index f16947715ea26..3397a953ddc45 100644 --- a/packages/flutter_tools/lib/src/macos/xcdevice.dart +++ b/packages/flutter_tools/lib/src/macos/xcdevice.dart @@ -649,7 +649,6 @@ class XCDevice { xcodeDebug: _xcodeDebug, fileSystem: globals.fs, processUtils: _processUtils, - xcode: _xcode, ), xcodeDebug: _xcodeDebug, platform: globals.platform, diff --git a/packages/flutter_tools/lib/src/macos/xcode.dart b/packages/flutter_tools/lib/src/macos/xcode.dart index 9186ca11b3585..5485c2ef1c22e 100644 --- a/packages/flutter_tools/lib/src/macos/xcode.dart +++ b/packages/flutter_tools/lib/src/macos/xcode.dart @@ -19,6 +19,7 @@ import '../base/version.dart'; import '../build_info.dart'; import '../cache.dart'; import '../ios/xcodeproj.dart'; +import '../xcode_project.dart'; Version get xcodeRequiredVersion => Version(15, null, null); @@ -231,14 +232,14 @@ class Xcode { /// See [XcodeProjectInterpreter.xcrunCommand]. List xcrunCommand() => _xcodeProjectInterpreter.xcrunCommand(); - Future> xcodebuildProjectCommand( - String projectPath, + Future> fetchDependenciesAndGenerateXcodebuildArgs( + XcodeBasedProject xcodeProject, Directory buildDirectory, { - bool skipPackageResolution = true, - }) async => _xcodeProjectInterpreter.xcodebuildProjectCommand( - projectPath, + bool skipPackageUpdatesAndValidation = true, + }) async => _xcodeProjectInterpreter.fetchDependenciesAndGenerateXcodebuildArgs( + xcodeProject, buildDirectory, - skipPackageResolution: skipPackageResolution, + skipPackageUpdatesAndValidation: skipPackageUpdatesAndValidation, ); Future cc(List args) => _run('cc', args); diff --git a/packages/flutter_tools/lib/src/migrations/swift_package_manager_integration_migration.dart b/packages/flutter_tools/lib/src/migrations/swift_package_manager_integration_migration.dart index 33db4ae6a9c78..dc7df4db919ca 100644 --- a/packages/flutter_tools/lib/src/migrations/swift_package_manager_integration_migration.dart +++ b/packages/flutter_tools/lib/src/migrations/swift_package_manager_integration_migration.dart @@ -229,7 +229,7 @@ class SwiftPackageManagerIntegrationMigration extends ProjectMigrator { // Get the project info to make sure it compiles with xcodebuild await _xcodeProjectInterpreter.getInfo( - _xcodeProject.hostAppRoot.path, + _xcodeProject, buildDirectory: _fileSystem.directory( _platform.buildDirectory(config: _config, fileSystem: _fileSystem), ), diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart index f5276afaaa77d..0628199b54c83 100644 --- a/packages/flutter_tools/lib/src/plugins.dart +++ b/packages/flutter_tools/lib/src/plugins.dart @@ -470,7 +470,7 @@ class Plugin { return fileSystem.path.join(packagePath, 'Package.swift'); } - /// Returns true if a Package.swift is found. + /// Returns true if the plugin supports the [platform] and a Package.swift exists. bool supportSwiftPackageManagerForPlatform(FileSystem fileSystem, String platform) { final String? manifestPath = pluginSwiftPackageManifestPath(fileSystem, platform); return platforms[platform] != null && @@ -478,6 +478,14 @@ class Plugin { fileSystem.file(manifestPath).existsSync(); } + /// Returns true if the plugin supports the [platform] and a podspec exists. + bool supportCocoapodsForPlatform(FileSystem fileSystem, String platform) { + final String? podspecPath = pluginPodspecPath(fileSystem, platform); + return platforms[platform] != null && + podspecPath != null && + fileSystem.file(podspecPath).existsSync(); + } + /// Expected path to the plugin's podspec. Returns null if the plugin does /// not support the [platform] or the [platform] is not iOS or macOS. String? pluginPodspecPath(FileSystem fileSystem, String platform) { diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index f0178e95a60f9..b44f64fd91959 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -503,6 +503,10 @@ class AndroidProject extends FlutterProjectPlatform { '^\\s*apply plugin\\:\\s+[\'"]kotlin-android[\'"]\\s*\$', ); + static final _kotlinCompilerOptionsPattern = RegExp( + r'kotlin\s*\{[\s\S]*?compilerOptions\s*\{[^}]*\}', + ); + /// Examples of strings that this regex matches: /// - `id "kotlin-android"` /// - `id("kotlin-android")` @@ -609,11 +613,30 @@ class AndroidProject extends FlutterProjectPlatform { /// True, if the app project is using Kotlin. bool get isKotlin { - final imperativeMatch = firstMatchInFile(appGradleFile, _imperativeKotlinPluginPattern) != null; - final bool declarativeMatch = _declarativeKotlinPluginPatterns.any((RegExp pattern) { - return (firstMatchInFile(appGradleFile, pattern) != null); - }); - return imperativeMatch || declarativeMatch; + if (appGradleFile.existsSync()) { + if (firstMatchInFile(appGradleFile, _imperativeKotlinPluginPattern) != null) { + return true; + } + for (final RegExp pattern in _declarativeKotlinPluginPatterns) { + if (firstMatchInFile(appGradleFile, pattern) != null) { + return true; + } + } + try { + final String content = appGradleFile.readAsStringSync(); + if (_kotlinCompilerOptionsPattern.hasMatch(content)) { + return true; + } + } on FileSystemException { + // Ignore and continue with other checks. + } + } + final Directory kotlinSrc = hostAppGradleRoot + .childDirectory('app') + .childDirectory('src') + .childDirectory('main') + .childDirectory('kotlin'); + return kotlinSrc.existsSync(); } /// Gets top-level Gradle build file. diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index b2a593186aa9d..473967a49656d 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -131,6 +131,10 @@ abstract final class FlutterOptions { static const kDartDefineFromFileOption = 'dart-define-from-file'; static const kWebDefinesOption = 'web-define'; static const kPerformanceMeasurementFile = 'performance-measurement-file'; + // Shorebird-specific build-trace option. Prefixed so the flag name and + // generated env vars / Gradle properties don't squat on identifiers + // upstream Flutter might want later. + static const kShorebirdTrace = 'shorebird-trace'; static const kDeviceUser = 'device-user'; static const kDeviceTimeout = 'device-timeout'; static const kDeviceConnection = 'device-connection'; @@ -1064,6 +1068,19 @@ abstract class FlutterCommand extends Command { ); } + void usesShorebirdTraceOption({bool hide = false}) { + argParser.addOption( + FlutterOptions.kShorebirdTrace, + help: + 'Output a Chrome Trace Event Format JSON file for build ' + 'profiling. The resulting file can be viewed at ' + 'https://ui.perfetto.dev. Shorebird-specific; named to avoid ' + 'colliding with any future upstream trace option.', + hide: hide, + valueHelp: 'path/to/shorebird-trace.json', + ); + } + void addAndroidSpecificBuildOptions({bool hide = false}) { argParser.addFlag( FlutterOptions.kAndroidGradleDaemon, @@ -1466,6 +1483,11 @@ abstract class FlutterCommand extends Command { ? stringArg(FlutterOptions.kPerformanceMeasurementFile) : null; + final String? shorebirdTraceFilePath = + argParser.options.containsKey(FlutterOptions.kShorebirdTrace) + ? stringArg(FlutterOptions.kShorebirdTrace) + : null; + final Map defineConfigJsonMap = extractDartDefineConfigJsonMap(); final List dartDefines = extractDartDefines(defineConfigJsonMap: defineConfigJsonMap); @@ -1517,6 +1539,7 @@ abstract class FlutterCommand extends Command { dartDefines: dartDefines, dartExperiments: experiments, performanceMeasurementFile: performanceMeasurementFile, + shorebirdTraceFilePath: shorebirdTraceFilePath, packageConfigPath: packagesPath ?? packageConfigFile.path, codeSizeDirectory: codeSizeDirectory, androidGradleDaemon: androidGradleDaemon, diff --git a/packages/flutter_tools/lib/src/shorebird/android_build_trace_session.dart b/packages/flutter_tools/lib/src/shorebird/android_build_trace_session.dart new file mode 100644 index 0000000000000..5b01afa30fbc5 --- /dev/null +++ b/packages/flutter_tools/lib/src/shorebird/android_build_trace_session.dart @@ -0,0 +1,210 @@ +// Shorebird-specific. Keeps the build-trace plumbing for +// `flutter build apk` / `flutter build appbundle` out of gradle.dart +// so the Shorebird fork's diff against upstream stays small and the +// build flow reads the same as upstream. + +import 'dart:io' show Process; + +import 'package:shorebird_build_trace/shorebird_build_trace.dart'; + +import '../base/file_system.dart'; +import '../build_info.dart'; +import '../cache.dart'; +import 'network_trace_span.dart'; + +/// Wraps the lifecycle of a Shorebird build trace across one +/// `buildGradleApp` call. Returned by [maybeStart] only when +/// `--shorebird-trace=` was passed; the constructor installs +/// [BuildTracer.current] and [finish] / [abortOnGradleFailure] +/// clear it, so gradle.dart itself never touches the static. +/// +/// Manual start/stop (rather than a body-wrapping closure) because +/// wrapping the ~200-line `buildGradleApp` body would force a reindent +/// that balloons our diff against upstream flutter and makes every +/// subsequent merge of that method harder. +class AndroidBuildTraceSession { + AndroidBuildTraceSession._({ + required BuildTracer tracer, + required FileSystem fileSystem, + required String tracePath, + required Directory buildDirectory, + }) : _tracer = tracer, + _fs = fileSystem, + _tracePath = tracePath, + _buildDirectory = buildDirectory, + _flutterPid = currentProcessId(), + _buildStart = DateTime.now() { + BuildTracer.start(_tracer); + _tracer + ..addProcessNameMetadata(pid: _flutterPid, name: 'flutter_tool') + ..addThreadNameMetadata(pid: _flutterPid, tid: _flutterToolTid, name: 'flutter tool') + ..addThreadNameMetadata(pid: _flutterPid, tid: _gradleWaitTid, name: 'gradle (wait)') + ..addThreadNameMetadata(pid: _flutterPid, tid: networkTid, name: 'network'); + } + + /// Returns a session when a trace path is configured for this build, + /// null otherwise. + static AndroidBuildTraceSession? maybeStart( + AndroidBuildInfo androidBuildInfo, + FileSystem fileSystem, + Directory buildDirectory, + ) { + final String? path = androidBuildInfo.buildInfo.shorebirdTraceFilePath; + if (path == null) { + return null; + } + return AndroidBuildTraceSession._( + tracer: BuildTracer(), + fileSystem: fileSystem, + tracePath: path, + buildDirectory: buildDirectory, + ); + } + + final BuildTracer _tracer; + final FileSystem _fs; + final String _tracePath; + final Directory _buildDirectory; + final int _flutterPid; + final DateTime _buildStart; + + static const int _flutterToolTid = 1; + static const int _gradleWaitTid = 2; + + String? _assembleTraceFilePath; + String? _gradleTaskTraceFilePath; + DateTime? _gradleStart; + DateTime? _gradleEnd; + + /// Returns the `-P`/`-I=` flags to thread trace plumbing into Gradle. + /// Safe to splat into the options list unconditionally. + /// + /// [assembleTask] carries the variant + build type (e.g. + /// `assembleFooRelease` / `bundleRelease`); the intermediate trace + /// paths embed it so a single Gradle invocation running multiple + /// variants in parallel can't have per-variant FlutterTask instances + /// stomp on each other's trace. + List extraGradleOptions(String assembleTask) { + final String assembleTrace = _fs.path.join( + _buildDirectory.path, + 'intermediates', + 'shorebird', + 'shorebird_assemble_trace_$assembleTask.json', + ); + final String gradleTaskTrace = _fs.path.join( + _buildDirectory.path, + 'intermediates', + 'shorebird', + 'shorebird_gradle_task_trace_$assembleTask.json', + ); + final String initScript = _fs.path.join( + Cache.flutterRoot!, + 'packages', + 'flutter_tools', + 'gradle', + 'shorebird_trace_init.gradle', + ); + _assembleTraceFilePath = assembleTrace; + _gradleTaskTraceFilePath = gradleTaskTrace; + return [ + '-Pshorebird-trace-file=$assembleTrace', + '-I=$initScript', + '-Pshorebird.gradle-trace-file=$gradleTaskTrace', + ]; + } + + /// Hook for `_runGradleTask`'s `preRunTask` โ€” records when the Gradle + /// wrapper is about to run so [finish] can compute the outer build + /// span. + void onGradleAboutToStart() { + final preGradleEnd = DateTime.now(); + _tracer.addCompleteEvent( + name: 'pre-gradle setup', + cat: TraceCategory.flutter.wireName, + pid: _flutterPid, + tid: _flutterToolTid, + start: _buildStart, + end: preGradleEnd, + ); + _gradleStart = DateTime.now(); + } + + /// Hook for `_runGradleTask`'s `onStart` โ€” emits a flow-start tied to + /// Gradle's real pid so Perfetto draws an arrow from our + /// `gradle ` span into the first per-task event the init script + /// emits. + void onGradleSpawn(Process process) { + _tracer.addFlowStart( + id: process.pid, + pid: _flutterPid, + tid: _gradleWaitTid, + at: DateTime.now(), + ); + } + + /// Call after `_runGradleTask` returns, before the exit-code check. + /// Records the outer `gradle ` span and merges the intermediate + /// trace files into the main tracer. + void onGradleFinished(String assembleTask) { + final gradleEnd = DateTime.now(); + _gradleEnd = gradleEnd; + _tracer.addCompleteEvent( + name: '${TraceNames.gradleSpanPrefix}$assembleTask', + cat: TraceCategory.gradle.wireName, + pid: _flutterPid, + tid: _gradleWaitTid, + start: _gradleStart ?? _buildStart, + end: gradleEnd, + ); + final String? assembleTraceFilePath = _assembleTraceFilePath; + if (assembleTraceFilePath != null) { + _tracer.mergeEventsFromFile(_fs.file(assembleTraceFilePath)); + } + final String? gradleTaskTraceFilePath = _gradleTaskTraceFilePath; + if (gradleTaskTraceFilePath != null) { + _tracer.mergeEventsFromFile(_fs.file(gradleTaskTraceFilePath)); + } + } + + /// Call right before `throwToolExit` when Gradle returned non-zero โ€” + /// clears [BuildTracer.current] so the caught ToolExit doesn't leak + /// tracer state into any later code that reads [BuildTracer.current]. + void abortOnGradleFailure() { + BuildTracer.stop(); + } + + /// Writes the merged trace to disk and clears [BuildTracer.current]. + /// Records post-gradle + outer "flutter build" spans first. + /// + /// [buildTarget] is the target suffix (e.g. `apk`, `appbundle`); the + /// outer span name is assembled as + /// `TraceNames.flutterBuildSpanPrefix + buildTarget`. + /// [printStatus] is called once with a user-facing "trace written" + /// message so callers don't have to wire the logger through. + void finish({required String buildTarget, required void Function(String) printStatus}) { + final postGradleEnd = DateTime.now(); + _tracer + ..addCompleteEvent( + name: 'post-gradle processing', + cat: TraceCategory.flutter.wireName, + pid: _flutterPid, + tid: _flutterToolTid, + start: _gradleEnd ?? postGradleEnd, + end: postGradleEnd, + ) + ..addCompleteEvent( + name: '${TraceNames.flutterBuildSpanPrefix}$buildTarget', + cat: TraceCategory.flutter.wireName, + pid: _flutterPid, + tid: _flutterToolTid, + start: _buildStart, + end: postGradleEnd, + ) + ..writeToFile(_fs.file(_tracePath)); + printStatus( + 'Shorebird build trace written to $_tracePath. ' + 'View at https://ui.perfetto.dev', + ); + BuildTracer.stop(); + } +} diff --git a/packages/flutter_tools/lib/src/shorebird/ios_build_trace_session.dart b/packages/flutter_tools/lib/src/shorebird/ios_build_trace_session.dart new file mode 100644 index 0000000000000..d8a7ea3da8e55 --- /dev/null +++ b/packages/flutter_tools/lib/src/shorebird/ios_build_trace_session.dart @@ -0,0 +1,281 @@ +// Shorebird-specific. Keeps the build-trace plumbing for +// `flutter build ios` out of mac.dart so the Shorebird fork's diff +// against upstream stays small and the build flow reads the same as +// upstream. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' show ProcessResult; + +import 'package:shorebird_build_trace/shorebird_build_trace.dart'; + +import '../base/file_system.dart'; +import 'network_trace_span.dart'; + +/// Wraps the lifecycle of a Shorebird build trace across one iOS +/// build. Returned by [maybeStart] only when `--shorebird-trace=` +/// was passed; the constructor installs [BuildTracer.current] and +/// [finish] / [abortOnFailure] clear it, so mac.dart itself never +/// touches the static. +/// +/// Manual start/stop (rather than a body-wrapping closure) because +/// wrapping the ~300-line `buildXcodeProject` body would force a +/// reindent that balloons our diff against upstream flutter and makes +/// every subsequent merge of that method harder. +class IosBuildTraceSession { + IosBuildTraceSession._({ + required BuildTracer tracer, + required FileSystem fileSystem, + required String tracePath, + required String buildDirectoryPath, + }) : _tracer = tracer, + _fs = fileSystem, + _tracePath = tracePath, + _buildDirectoryPath = buildDirectoryPath, + _flutterPid = currentProcessId(), + _buildStart = DateTime.now() { + BuildTracer.start(_tracer); + _tracer + ..addProcessNameMetadata(pid: _flutterPid, name: 'flutter_tool') + ..addThreadNameMetadata(pid: _flutterPid, tid: _flutterToolTid, name: 'flutter tool') + ..addThreadNameMetadata(pid: _flutterPid, tid: _xcodeWaitTid, name: 'xcode (wait)') + ..addThreadNameMetadata(pid: _flutterPid, tid: networkTid, name: 'network') + ..addProcessNameMetadata(pid: _xcodeSubsectionPid, name: 'xcodebuild') + ..addThreadNameMetadata(pid: _xcodeSubsectionPid, tid: 1, name: 'xcode phases'); + } + + /// Returns a session when a trace path is configured for this build, + /// null otherwise. [buildDirectoryPath] is the iOS build directory + /// (relative to the project root) where the intermediate assemble + /// trace file is written. + static IosBuildTraceSession? maybeStart({ + required String? shorebirdTraceFilePath, + required FileSystem fileSystem, + required String buildDirectoryPath, + }) { + if (shorebirdTraceFilePath == null) { + return null; + } + return IosBuildTraceSession._( + tracer: BuildTracer(), + fileSystem: fileSystem, + tracePath: shorebirdTraceFilePath, + buildDirectoryPath: buildDirectoryPath, + ); + } + + final BuildTracer _tracer; + final FileSystem _fs; + final String _tracePath; + final String _buildDirectoryPath; + final int _flutterPid; + final DateTime _buildStart; + + static const int _flutterToolTid = 1; + static const int _xcodeWaitTid = 2; + + /// Synthetic pid for Xcode subsection events parsed from xcresult. + /// xcresult doesn't surface xcodebuild's pid, so events end up on + /// this fixed id named via a `process_name` metadata event. + static const int _xcodeSubsectionPid = 0x78636f; // ASCII 'xco' + + DateTime? _podInstallStart; + DateTime? _xcodeStart; + DateTime? _xcodeEnd; + String? _assembleTraceFilePath; + + /// Call immediately before `processPodsIfNeeded` so the resulting + /// span covers its full wall-clock. + void onBeforePodInstall() { + _podInstallStart = DateTime.now(); + } + + /// Call immediately after `processPodsIfNeeded` returns. Records the + /// `pod install` complete event using the start timestamp captured + /// by [onBeforePodInstall]. + void onAfterPodInstall() { + final DateTime? start = _podInstallStart; + if (start == null) { + return; + } + _tracer.addCompleteEvent( + name: TraceNames.podInstallSpanName, + cat: TraceCategory.subprocess.wireName, + pid: _flutterPid, + tid: _flutterToolTid, + start: start, + end: DateTime.now(), + ); + } + + /// Returns the extra xcodebuild arguments that thread trace plumbing + /// into the build phase script. Safe to splat into the command list + /// unconditionally. + List extraBuildCommands() { + final String assembleTrace = _fs.path.join( + _fs.currentDirectory.path, + _buildDirectoryPath, + 'shorebird_assemble_trace.json', + ); + _assembleTraceFilePath = assembleTrace; + // Naming the env var SHOREBIRD_TRACE_FILE avoids squatting on an + // Xcode-reserved prefix. Read by xcode_backend.dart. + return ['SHOREBIRD_TRACE_FILE=$assembleTrace']; + } + + /// Call right before `_runBuildWithRetries` so the outer build span + /// can compute the pre-xcode setup interval. + void onXcodeAboutToStart() { + final xcodeStart = DateTime.now(); + _xcodeStart = xcodeStart; + _tracer.addCompleteEvent( + name: 'pre-xcode setup', + cat: TraceCategory.flutter.wireName, + pid: _flutterPid, + tid: _flutterToolTid, + start: _buildStart, + end: xcodeStart, + ); + } + + /// Call after `_runBuildWithRetries` returns. Records the outer + /// `xcode ` span, then emits per-subsection events pulled + /// from xcresulttool, then merges the intermediate assemble trace. + /// + /// [runXcresultTool] is injected so callers can route through their + /// own `ProcessManager`; the session avoids a direct `globals.*` + /// dependency. + Future onXcodeFinished({ + required String buildActionName, + required Directory resultBundleDirectory, + required Future Function(List) runXcresultTool, + }) async { + final xcodeEnd = DateTime.now(); + _xcodeEnd = xcodeEnd; + _tracer.addCompleteEvent( + name: '${TraceNames.xcodeSpanPrefix}$buildActionName', + cat: TraceCategory.xcode.wireName, + pid: _flutterPid, + tid: _xcodeWaitTid, + start: _xcodeStart ?? _buildStart, + end: xcodeEnd, + ); + await _emitXcodeSubsectionEvents( + resultBundleDirectory: resultBundleDirectory, + runXcresultTool: runXcresultTool, + ); + final String? assembleTraceFilePath = _assembleTraceFilePath; + if (assembleTraceFilePath != null) { + _tracer.mergeEventsFromFile(_fs.file(assembleTraceFilePath)); + } + } + + /// Clears [BuildTracer.current] without writing the trace. Use on + /// error paths where [finish] won't run. + void abortOnFailure() { + BuildTracer.stop(); + } + + /// Writes the merged trace to disk and clears [BuildTracer.current]. + /// Records the post-xcode + outer `flutter build ios` spans first. + void finish({required void Function(String) printStatus}) { + final buildEnd = DateTime.now(); + _tracer + ..addCompleteEvent( + name: 'post-xcode processing', + cat: TraceCategory.flutter.wireName, + pid: _flutterPid, + tid: _flutterToolTid, + start: _xcodeEnd ?? buildEnd, + end: buildEnd, + ) + ..addCompleteEvent( + name: '${TraceNames.flutterBuildSpanPrefix}ios', + cat: TraceCategory.flutter.wireName, + pid: _flutterPid, + tid: _flutterToolTid, + start: _buildStart, + end: buildEnd, + ) + ..writeToFile(_fs.file(_tracePath)); + printStatus( + 'Shorebird build trace written to $_tracePath. ' + 'View at https://ui.perfetto.dev', + ); + BuildTracer.stop(); + } + + /// Ask xcresulttool for the structured build log of the archive + /// action and emit one Chrome Trace Event per top-level subsection + /// (each Xcode target build). Best-effort: silently returns if the + /// bundle can't be parsed (xcresulttool output format drifts across + /// Xcode versions). + Future _emitXcodeSubsectionEvents({ + required Directory resultBundleDirectory, + required Future Function(List) runXcresultTool, + }) async { + if (!resultBundleDirectory.existsSync()) { + return; + } + final ProcessResult result; + try { + result = await runXcresultTool([ + 'xcrun', + 'xcresulttool', + 'get', + 'log', + '--type', + 'build', + '--path', + resultBundleDirectory.path, + ]); + } on Exception { + return; + } + if (result.exitCode != 0) { + return; + } + + final Object? decoded; + try { + decoded = json.decode(result.stdout as String); + } on FormatException { + return; + } + if (decoded is! Map) { + return; + } + + final Object? subsections = decoded['subsections']; + if (subsections is! List) { + return; + } + for (final Object? sub in subsections) { + if (sub is! Map) { + continue; + } + final String title = (sub['title'] as String?) ?? ''; + final double? startTime = (sub['startTime'] as num?)?.toDouble(); + final double? duration = (sub['duration'] as num?)?.toDouble(); + if (startTime == null || duration == null || duration <= 0) { + continue; + } + final start = DateTime.fromMicrosecondsSinceEpoch((startTime * 1000000).round()); + final end = DateTime.fromMicrosecondsSinceEpoch(((startTime + duration) * 1000000).round()); + _tracer.addCompleteEvent( + name: title, + cat: TraceCategory.xcodeSubsection.wireName, + // Synthetic pid so subsections display on their own row in + // Perfetto, labelled 'xcodebuild' via the process_name + // metadata emitted at tracer setup. xcresult doesn't surface + // xcodebuild's real pid, and compile sub-subsections are done + // by clang/swiftc/ld children we never saw โ€” synthetic is + // honest. + pid: _xcodeSubsectionPid, + tid: 1, + start: start, + end: end, + ); + } + } +} diff --git a/packages/flutter_tools/lib/src/shorebird/network_trace_span.dart b/packages/flutter_tools/lib/src/shorebird/network_trace_span.dart new file mode 100644 index 0000000000000..2f08277ae7f81 --- /dev/null +++ b/packages/flutter_tools/lib/src/shorebird/network_trace_span.dart @@ -0,0 +1,54 @@ +// Shorebird-specific. Keeps the build-trace plumbing for HTTP fetches +// out of net.dart so the Shorebird fork's diff against upstream stays +// small. + +import 'package:shorebird_build_trace/shorebird_build_trace.dart'; + +/// Perfetto row id for Flutter-tool HTTP artifact fetches. Local to +/// the flutter tool's pid; picked to sit alongside the flutter-tool +/// and assemble rows without overlapping either. +const int networkTid = 5; + +/// One HTTP request's contribution to the Shorebird build trace. +/// +/// Construct immediately before issuing the request and call [record] +/// once, after the request has completed (success, error, or body +/// fully drained). Records a Chrome Trace Event Format `X` event on +/// the network row only when a [BuildTracer] is installed; a no-op +/// otherwise, so net.dart can drop one of these in every return path +/// without branching on tracing state. +class NetworkTraceSpan { + NetworkTraceSpan.start({required Uri url, required bool onlyHeaders}) + : _url = url, + _onlyHeaders = onlyHeaders, + _start = DateTime.now(); + + final Uri _url; + final bool _onlyHeaders; + final DateTime _start; + + /// Emits the span. Safe to call whether or not a tracer is active. + /// [statusCode] is recorded on normal completion; [errorKind] on + /// IO/SSL/argument errors. Both optional so callers can record a + /// span before either is known (e.g. connection-level failures). + void record({int? statusCode, String? errorKind}) { + final BuildTracer? tracer = BuildTracer.current; + if (tracer == null) { + return; + } + tracer.addCompleteEvent( + name: '${_onlyHeaders ? 'HEAD' : 'GET'} ${_url.host}', + cat: TraceCategory.network.wireName, + pid: currentProcessId(), + tid: networkTid, + start: _start, + end: DateTime.now(), + args: { + 'method': _onlyHeaders ? 'HEAD' : 'GET', + 'host': _url.host, + if (statusCode != null) 'status': statusCode, + if (errorKind != null) 'error': errorKind, + }, + ); + } +} diff --git a/packages/flutter_tools/lib/src/shorebird/shorebird_yaml.dart b/packages/flutter_tools/lib/src/shorebird/shorebird_yaml.dart new file mode 100644 index 0000000000000..24d76f84ed257 --- /dev/null +++ b/packages/flutter_tools/lib/src/shorebird/shorebird_yaml.dart @@ -0,0 +1,76 @@ +// Copyright 2024 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:yaml/yaml.dart'; +import 'package:yaml_edit/yaml_edit.dart'; + +import '../base/file_system.dart'; +import '../globals.dart' as globals; + +void updateShorebirdYaml( + String? flavor, + String shorebirdYamlPath, { + required Map environment, +}) { + final File shorebirdYaml = globals.fs.file(shorebirdYamlPath); + if (!shorebirdYaml.existsSync()) { + throw Exception('shorebird.yaml not found at $shorebirdYamlPath'); + } + final YamlDocument input = loadYamlDocument(shorebirdYaml.readAsStringSync()); + final YamlMap yamlMap = input.contents as YamlMap; + final Map compiled = compileShorebirdYaml( + yamlMap, + flavor: flavor, + environment: environment, + ); + // Currently we write out over the same yaml file, we should fix this to + // write to a new .json file instead and avoid naming confusion between the + // input and compiled files. + final YamlEditor yamlEditor = YamlEditor(''); + yamlEditor.update([], compiled); + shorebirdYaml.writeAsStringSync(yamlEditor.toString(), flush: true); +} + +String appIdForFlavor(YamlMap yamlMap, {required String? flavor}) { + if (flavor == null || flavor.isEmpty) { + final String? defaultAppId = yamlMap['app_id'] as String?; + if (defaultAppId == null || defaultAppId.isEmpty) { + throw Exception('Cannot find "app_id" in shorebird.yaml'); + } + return defaultAppId; + } + + final YamlMap? yamlFlavors = yamlMap['flavors'] as YamlMap?; + if (yamlFlavors == null) { + throw Exception('Cannot find "flavors" in shorebird.yaml.'); + } + final String? flavorAppId = yamlFlavors[flavor] as String?; + if (flavorAppId == null || flavorAppId.isEmpty) { + throw Exception('Cannot find "app_id" for $flavor in shorebird.yaml'); + } + return flavorAppId; +} + +Map compileShorebirdYaml( + YamlMap yamlMap, { + required String? flavor, + required Map environment, +}) { + final String appId = appIdForFlavor(yamlMap, flavor: flavor); + final Map compiled = {'app_id': appId}; + void copyIfSet(String key) { + if (yamlMap[key] != null) { + compiled[key] = yamlMap[key]; + } + } + + copyIfSet('base_url'); + copyIfSet('auto_update'); + copyIfSet('patch_verification'); + final String? shorebirdPublicKeyEnvVar = environment['SHOREBIRD_PUBLIC_KEY']; + if (shorebirdPublicKeyEnvVar != null) { + compiled['patch_public_key'] = shorebirdPublicKeyEnvVar; + } + return compiled; +} diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart index 50d1508fba0d7..f1c9b087af997 100644 --- a/packages/flutter_tools/lib/src/version.dart +++ b/packages/flutter_tools/lib/src/version.dart @@ -1059,6 +1059,28 @@ class GitTagVersion { } } + // Check if running on a Shorebird release branch. + final String shorebirdFlutterReleases = git + .runSync([ + 'for-each-ref', + '--contains', + gitRef, + '--format', + '%(refname:short)', + 'refs/remotes/origin/flutter_release/*', + ], workingDirectory: workingDirectory) + .stdout + .trim(); + final stableVersionPattern = RegExp(r'^\d+\.\d+\.\d+$'); + final String? shorebirdFlutterVersion = LineSplitter.split(shorebirdFlutterReleases) + .map((e) => e.replaceFirst('origin/flutter_release/', '')) + .where((e) => stableVersionPattern.hasMatch(e)) + .toList() + .firstOrNull; + if (shorebirdFlutterVersion != null) { + return parse(shorebirdFlutterVersion); + } + // If we don't exist in a tag, use git to find the latest tag. return _useNewestTagAndCommitsPastFallback( git: git, diff --git a/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart b/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart index 8c2f95d36f666..fc39d783b1813 100644 --- a/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart +++ b/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart @@ -12,6 +12,7 @@ import '../artifacts.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; import '../base/platform.dart'; +import '../base/process.dart'; import '../base/terminal.dart'; import '../dart/analysis.dart'; import '../project.dart'; @@ -42,11 +43,13 @@ class LspPreviewDetector { required this.suppressAnalytics, this.analysisServerFactory, required this.artifacts, + required this.shutdownHooks, @visibleForTesting this.watcherBuilder = _defaultWatcherBuilder, @visibleForTesting this.onPackageConfigChangeDetected, }) : projectRoot = project.directory; final Artifacts artifacts; + final ShutdownHooks shutdownHooks; final Platform platform; final WidgetPreviewAnalytics previewAnalytics; final FlutterProject project; @@ -175,7 +178,20 @@ class LspPreviewDetector { return; } await _analysisServer?.waitForAnalysis(); - final FlutterWidgetPreviews result = await dtd.getFlutterWidgetPreviews(); - onChangeDetected(result); + try { + final FlutterWidgetPreviews result = await dtd.getFlutterWidgetPreviews(); + onChangeDetected(result); + } catch (e) { + if (_disposed || shutdownHooks.isShuttingDown) { + logger.printTrace('Failed to get widget previews during shutdown: $e'); + } else if (e is StateError || e is Exception) { + logger.printWarning( + 'Lost connection to the Dart Tooling Daemon (DTD). ' + 'Live preview updates are paused. Details: $e', + ); + } else { + rethrow; + } + } } } diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart index 295c6e5b4dae2..ab55087b046d2 100644 --- a/packages/flutter_tools/lib/src/xcode_project.dart +++ b/packages/flutter_tools/lib/src/xcode_project.dart @@ -5,11 +5,16 @@ /// @docImport 'ios/mac.dart'; library; +import 'dart:async'; + import 'package:yaml/yaml.dart' as yaml; +import 'base/common.dart'; import 'base/error_handling_io.dart'; import 'base/file_system.dart'; +import 'base/io.dart'; import 'base/logger.dart'; +import 'base/process.dart'; import 'base/template.dart'; import 'base/utils.dart'; import 'base/version.dart'; @@ -240,7 +245,7 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform { return null; } return _projectInfo ??= await xcodeProjectInterpreter.getInfo( - hostAppRoot.path, + this, buildDirectory: globals.fs.directory(darwinPlatform.buildDirectory()), ); } @@ -339,7 +344,7 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform { } final Map buildSettings = await xcodeProjectInterpreter.getBuildSettings( - xcodeProject.path, + this, buildContext: buildContext, ); if (buildSettings.isNotEmpty) { @@ -384,6 +389,62 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform { } return flavor; } + + /// The process used to fetch Swift packages. + Process? _swiftPackageFetchProcess; + + /// The stdout subscription for the Swift package fetch process. + StreamSubscription? _swiftPackageFetchStdoutSubscription; + + /// The stderr subscription for the Swift package fetch process. + StreamSubscription? _swiftPackageFetchStderrSubscription; + + /// Prefetches Swift packages for the Xcode project if the project has migrated to SwiftPM. + Future prefetchSwiftPackages({ + required List xcodebuildProjectCommandArguments, + required ProcessUtils processUtils, + required Logger logger, + }) async { + Status? status; + try { + final command = [...xcodebuildProjectCommandArguments, '-resolvePackageDependencies']; + if (_swiftPackageFetchProcess != null) { + return; + } + final Process process = await processUtils.start(command, workingDirectory: hostAppRoot.path); + _swiftPackageFetchProcess = process; + var printFetchWarnings = false; + _swiftPackageFetchStdoutSubscription ??= process.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + if (line.startsWith('Fetching')) { + status?.cancel(); + if (!printFetchWarnings) { + logger.printStatus( + 'Xcode is fetching Swift Package Manager dependencies. This may take several minutes...', + ); + printFetchWarnings = true; + } + status = logger.startProgress(' $line...'); + } + }); + final stderrBuffer = StringBuffer(); + _swiftPackageFetchStderrSubscription ??= process.stderr + .transform(const Utf8Decoder(reportErrors: false)) + .listen(stderrBuffer.write); + + final int exitCode = await process.exitCode.whenComplete(() async { + await _swiftPackageFetchStdoutSubscription?.cancel(); + await _swiftPackageFetchStderrSubscription?.cancel(); + }); + if (exitCode != 0) { + throwToolExit('Xcode failed to resolve Swift Package Manager dependencies:\n$stderrBuffer'); + } + } finally { + status?.cancel(); + } + } } /// Represents the iOS sub-project of a Flutter project. diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 182009dc14a59..81abc7d7284c6 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -58,6 +58,16 @@ dependencies: pubspec_parse: 1.5.0 graphs: 2.3.2 + + # Shorebird-specific: shared build-trace library. Pinned to a specific + # commit of the shorebird monorepo. Roll this ref together with any + # change to the shared library. Subdir path points at the package + # within the monorepo. + shorebird_build_trace: + git: + url: https://github.com/shorebirdtech/shorebird.git + ref: cff436f66534e14618cddcaf93f0d0e9862c937a + path: packages/shorebird_build_trace hooks_runner: 1.1.1 hooks: 1.0.2 code_assets: 1.0.0 diff --git a/packages/flutter_tools/templates/app/android-java.tmpl/app/build.gradle.kts.tmpl b/packages/flutter_tools/templates/app/android-java.tmpl/app/build.gradle.kts.tmpl index 73cde770bb945..905e8e619d508 100644 --- a/packages/flutter_tools/templates/app/android-java.tmpl/app/build.gradle.kts.tmpl +++ b/packages/flutter_tools/templates/app/android-java.tmpl/app/build.gradle.kts.tmpl @@ -1,6 +1,5 @@ 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") } @@ -15,10 +14,6 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "{{androidIdentifier}}" @@ -39,6 +34,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + flutter { source = "../.." } diff --git a/packages/flutter_tools/templates/app/android-kotlin.tmpl/app/build.gradle.kts.tmpl b/packages/flutter_tools/templates/app/android-kotlin.tmpl/app/build.gradle.kts.tmpl index 73cde770bb945..905e8e619d508 100644 --- a/packages/flutter_tools/templates/app/android-kotlin.tmpl/app/build.gradle.kts.tmpl +++ b/packages/flutter_tools/templates/app/android-kotlin.tmpl/app/build.gradle.kts.tmpl @@ -1,6 +1,5 @@ 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") } @@ -15,10 +14,6 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "{{androidIdentifier}}" @@ -39,6 +34,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + flutter { source = "../.." } diff --git a/packages/flutter_tools/templates/app/android.tmpl/gradle.properties.tmpl b/packages/flutter_tools/templates/app/android.tmpl/gradle.properties.tmpl index fbee1d8cdafcd..e96108cfe4c4d 100644 --- a/packages/flutter_tools/templates/app/android.tmpl/gradle.properties.tmpl +++ b/packages/flutter_tools/templates/app/android.tmpl/gradle.properties.tmpl @@ -1,2 +1,6 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true +# This newDsl flag was added by the Flutter template +android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false diff --git a/packages/flutter_tools/templates/module/android/gradle/build.gradle.tmpl b/packages/flutter_tools/templates/module/android/gradle/build.gradle.tmpl index 66f4f1c271d24..482285e42dde8 100644 --- a/packages/flutter_tools/templates/module/android/gradle/build.gradle.tmpl +++ b/packages/flutter_tools/templates/module/android/gradle/build.gradle.tmpl @@ -8,7 +8,6 @@ allprojects { } apply plugin: "com.android.library" -apply plugin: "kotlin-android" android { namespace = "{{androidIdentifier}}" diff --git a/packages/flutter_tools/templates/module/android/gradle/gradle.properties.tmpl b/packages/flutter_tools/templates/module/android/gradle/gradle.properties.tmpl index fbee1d8cdafcd..e96108cfe4c4d 100644 --- a/packages/flutter_tools/templates/module/android/gradle/gradle.properties.tmpl +++ b/packages/flutter_tools/templates/module/android/gradle/gradle.properties.tmpl @@ -1,2 +1,6 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true +# This newDsl flag was added by the Flutter template +android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false diff --git a/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/build.gradle.tmpl index b3586db51ccb7..5be35e931f775 100644 --- a/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/build.gradle.tmpl +++ b/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/build.gradle.tmpl @@ -35,6 +35,6 @@ buildDir = new File(rootProject.projectDir, "../build/host") dependencies { implementation(project(":flutter")) implementation(fileTree(dir: "libs", include: ["*.jar"])) - implementation("androidx.appcompat:appcompat:1.0.2") - implementation("androidx.constraintlayout:constraintlayout:1.1.3") + implementation("androidx.appcompat:appcompat:1.7.0") + implementation("androidx.constraintlayout:constraintlayout:2.2.0") } diff --git a/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/build.gradle.kts.tmpl b/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/build.gradle.kts.tmpl index b030c044d4c87..a2ccf176fd147 100644 --- a/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/build.gradle.kts.tmpl +++ b/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/build.gradle.kts.tmpl @@ -23,7 +23,6 @@ allprojects { plugins { id("com.android.library") - id("kotlin-android") } android { @@ -36,10 +35,6 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - sourceSets { getByName("main") { java.srcDirs("src/main/kotlin") @@ -70,6 +65,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + dependencies { testImplementation("org.jetbrains.kotlin:kotlin-test") testImplementation("org.mockito:mockito-core:5.0.0") diff --git a/packages/flutter_tools/test/android_preview_integration.shard/flutter_build_preview_sdk_test.dart b/packages/flutter_tools/test/android_preview_integration.shard/flutter_build_preview_sdk_test.dart index 4b0b99d50d93d..126e53a26857b 100644 --- a/packages/flutter_tools/test/android_preview_integration.shard/flutter_build_preview_sdk_test.dart +++ b/packages/flutter_tools/test/android_preview_integration.shard/flutter_build_preview_sdk_test.dart @@ -42,22 +42,25 @@ void main() { tryToDelete(tempDir); }); - test('build succeeds targeting string compileSdkVersion', () async { + test('build succeeds targeting string compileSdk', () async { final File buildGradleFile = exampleAppDir .childDirectory('android') .childDirectory('app') .childFile('build.gradle.kts'); - // write a build.gradle.kts with compileSdkVersion as `android-UpsideDownCake` which is a string preview version + // write a build.gradle.kts with compileSdk as preview("Baklava") which computes the preview compile sdk version buildGradleFile.writeAsStringSync( - buildGradleFile.readAsStringSync().replaceFirst( - compileSdkVersionMatch, - 'compileSdkVersion = "android-UpsideDownCake"', - ), + buildGradleFile.readAsStringSync().replaceFirst(compileSdkVersionMatch, ''' +compileSdk { + version = preview("Baklava") +}'''), flush: true, ); expect( buildGradleFile.readAsStringSync(), - contains('compileSdkVersion = "android-UpsideDownCake"'), + contains(''' +compileSdk { + version = preview("Baklava") +}'''), ); final ProcessResult result = await processManager.run([ @@ -87,15 +90,15 @@ void main() { .childDirectory('android') .childDirectory('app') .childFile('build.gradle.kts'); - // write a build.gradle.kts with compileSdkPreview as `UpsideDownCake` which is a string preview version + // write a build.gradle.kts with compileSdkPreview as `Baklava` which is a string preview version buildGradleFile.writeAsStringSync( buildGradleFile.readAsStringSync().replaceFirst( compileSdkVersionMatch, - 'compileSdkPreview = "UpsideDownCake"', + 'compileSdkPreview = "Baklava"', ), flush: true, ); - expect(buildGradleFile.readAsStringSync(), contains('compileSdkPreview = "UpsideDownCake"')); + expect(buildGradleFile.readAsStringSync(), contains('compileSdkPreview = "Baklava"')); final ProcessResult result = await processManager.run([ flutterBin, @@ -124,15 +127,15 @@ void main() { .childDirectory('android') .childDirectory('app') .childFile('build.gradle.kts'); - // write a build.gradle.kts with compileSdkPreview as `UpsideDownCake` which is a string preview version + // write a build.gradle.kts with compileSdkPreview as `Baklava` which is a string preview version appBuildGradleFile.writeAsStringSync( appBuildGradleFile.readAsStringSync().replaceFirst( compileSdkVersionMatch, - 'compileSdkPreview = "UpsideDownCake"', + 'compileSdkPreview = "Baklava"', ), flush: true, ); - expect(appBuildGradleFile.readAsStringSync(), contains('compileSdkPreview = "UpsideDownCake"')); + expect(appBuildGradleFile.readAsStringSync(), contains('compileSdkPreview = "Baklava"')); final File pluginBuildGradleFile = pluginDir .childDirectory('android') @@ -141,14 +144,11 @@ void main() { pluginBuildGradleFile.writeAsStringSync( pluginBuildGradleFile.readAsStringSync().replaceFirst( compileSdkVersionMatch, - 'compileSdkPreview = "UpsideDownCake"', + 'compileSdkPreview = "Baklava"', ), flush: true, ); - expect( - pluginBuildGradleFile.readAsStringSync(), - contains('compileSdkPreview = "UpsideDownCake"'), - ); + expect(pluginBuildGradleFile.readAsStringSync(), contains('compileSdkPreview = "Baklava"')); final ProcessResult result = await processManager.run([ flutterBin, diff --git a/packages/flutter_tools/test/commands.shard/hermetic/analyze_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/analyze_test.dart index 46c5f49ffe36d..f448b6a1bd36e 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/analyze_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/analyze_test.dart @@ -9,6 +9,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/terminal.dart'; @@ -114,6 +115,44 @@ void main() { }, ); + testUsingContext( + 'Analysis server premature exit with 255 throws ToolExit', + () async { + const stderr = 'Fatal error in analyzer'; + processManager.addCommands([ + const FakeCommand( + command: [ + 'Artifact.engineDartSdkPath/bin/dart', + 'language-server', + '--dart-sdk', + 'Artifact.engineDartSdkPath', + '--disable-server-feature-completion', + '--disable-server-feature-search', + '--suppress-analytics', + ], + exitCode: 255, + stderr: stderr, + ), + ]); + await expectLater( + runner.run(['analyze']), + throwsA( + isA() + .having( + (ToolExit e) => e.message, + 'message', + contains('analysis server exited with code 255 and output:\n[stderr] $stderr'), + ) + .having((ToolExit e) => e.exitCode, 'exitCode', equals(255)), + ), + ); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }, + ); + testUsingContext( '--flutter-repo analyzes everything in the flutterRoot', () async { diff --git a/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart index f3d7dbe5038b7..43e58726c551f 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/assemble_test.dart @@ -28,13 +28,9 @@ void main() { final StackTrace stackTrace = StackTrace.current; late FakeAnalytics fakeAnalytics; - late MemoryFileSystem fileSystem; - setUp(() { - fileSystem = MemoryFileSystem.test(); - fileSystem.file('pubspec.yaml').createSync(); fakeAnalytics = getInitializedFakeAnalyticsInstance( - fs: fileSystem, + fs: MemoryFileSystem.test(), fakeFlutterVersion: FakeFlutterVersion(), ); }); @@ -55,7 +51,7 @@ void main() { }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), - FileSystem: () => fileSystem, + FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }, ); @@ -84,7 +80,7 @@ void main() { }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), - FileSystem: () => fileSystem, + FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }, ); @@ -113,7 +109,7 @@ void main() { }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), - FileSystem: () => fileSystem, + FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }, ); @@ -142,7 +138,7 @@ void main() { }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), - FileSystem: () => fileSystem, + FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }, ); @@ -164,7 +160,7 @@ void main() { }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), - FileSystem: () => fileSystem, + FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true), }, @@ -196,7 +192,7 @@ void main() { overrides: { Analytics: () => fakeAnalytics, Cache: () => Cache.test(processManager: FakeProcessManager.any()), - FileSystem: () => fileSystem, + FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }, ); @@ -228,7 +224,7 @@ void main() { }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), - FileSystem: () => fileSystem, + FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true), Analytics: () => fakeAnalytics, @@ -249,7 +245,7 @@ void main() { }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), - FileSystem: () => fileSystem, + FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }, ); @@ -273,7 +269,7 @@ void main() { }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), - FileSystem: () => fileSystem, + FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }, ); @@ -285,56 +281,24 @@ void main() { AssembleCommand(buildSystem: TestBuildSystem.all(BuildResult(success: true))), ); - const invalidDartDefines = [ - 'flutter.inspector.structuredErrors%3Dtrue', - '///', - '@@@@', - "'", - '"', - '`', - r'\', - r'$', - ';', - '/*', - '*/', - '//', - '\n', - '\r', - '<', - '>', - '{', - '}', - '[', - ']', - '(', - ')', - '%', - '=', - '&', - '?', - '#', + final command = [ + 'assemble', + '--output', + 'Output', + '-DartDefines=flutter.inspector.structuredErrors%3Dtrue', + 'debug_macos_bundle_flutter_assets', ]; - for (final invalidDartDefine in invalidDartDefines) { - final command = [ - 'assemble', - '--output', - 'Output', - '-DartDefines=$invalidDartDefine', - 'debug_macos_bundle_flutter_assets', - ]; - expect( - commandRunner.run(command), - throwsToolExit( - message: - 'Error parsing assemble command: The -Pdart-defines argument contains non-base64 encoded data. ' - 'Check your build command and try again.', - ), - ); - } + expect( + commandRunner.run(command), + throwsToolExit( + message: + 'Error parsing assemble command: your generated configuration may be out of date', + ), + ); }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), - FileSystem: () => fileSystem, + FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }, ); @@ -350,7 +314,7 @@ void main() { }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), - FileSystem: () => fileSystem, + FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }, ); @@ -380,7 +344,7 @@ void main() { }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), - FileSystem: () => fileSystem, + FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }, ); @@ -400,6 +364,7 @@ void main() { elapsedMilliseconds: 123, skipped: false, succeeded: true, + startTimeMicroseconds: 0, ), }, ), @@ -422,7 +387,7 @@ void main() { }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), - FileSystem: () => fileSystem, + FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }, ); @@ -452,7 +417,7 @@ void main() { localEngineHost: 'out/host_release', ), Cache: () => Cache.test(processManager: FakeProcessManager.any()), - FileSystem: () => fileSystem, + FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }, ); @@ -525,7 +490,7 @@ void main() { }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), - FileSystem: () => fileSystem, + FileSystem: () => MemoryFileSystem.test(), ProcessManager: () => FakeProcessManager.any(), }, ); @@ -538,6 +503,7 @@ void main() { skipped: false, succeeded: true, elapsedMilliseconds: 123, + startTimeMicroseconds: 0, ), ]; final FileSystem fileSystem = MemoryFileSystem.test(); @@ -582,23 +548,56 @@ void main() { expect(testLogger.statusText, contains('assemble')); }); - testUsingContext( - 'flutter assemble fails if pubspec.yaml is missing', - () async { - final CommandRunner commandRunner = createTestCommandRunner( - AssembleCommand(buildSystem: TestBuildSystem.error(null)), - ); + testWithoutContext('writeTraceData outputs Chrome Trace Event Format', () { + final measurements = [ + PerformanceMeasurement( + analyticsName: 'KernelSnapshot', + target: 'kernel_snapshot', + skipped: false, + succeeded: true, + elapsedMilliseconds: 500, + startTimeMicroseconds: 1000000, + ), + ]; + final FileSystem fileSystem = MemoryFileSystem.test(); + final outFile = fileSystem.currentDirectory.childDirectory('foo').childFile('trace.json'); - await expectLater( - commandRunner.run(['assemble', '-o Output', 'debug_macos_bundle_flutter_assets']), - throwsToolExit(message: 'No pubspec.yaml file found'), - ); - }, - overrides: { - FileSystem: () => MemoryFileSystem.test(), - ProcessManager: () => FakeProcessManager.any(), - }, - ); + writeTraceData(measurements, outFile); + + expect(outFile, exists); + final events = json.decode(outFile.readAsStringSync()) as List; + // 2 metadata events (process_name, thread_name) + 1 complete span. + expect(events, hasLength(3)); + + // Metadata events name the process + thread so Perfetto labels the + // row rather than showing a bare pid. + final processMeta = events[0]! as Map; + expect(processMeta['ph'], 'M'); + expect(processMeta['name'], 'process_name'); + expect((processMeta['args']! as Map)['name'], 'flutter assemble'); + + final threadMeta = events[1]! as Map; + expect(threadMeta['ph'], 'M'); + expect(threadMeta['name'], 'thread_name'); + expect((threadMeta['args']! as Map)['name'], 'flutter assemble'); + + final event = events[2]! as Map; + expect(event['ph'], 'X'); + expect(event['name'], 'KernelSnapshot'); + expect(event['cat'], 'assemble'); + expect(event['ts'], 1000000); + expect(event['dur'], 500000); + // pid is the real pid of the test process; just assert it's present + // and an int, and matches between metadata + span. + expect(event['pid'], isA()); + expect(event['pid'], processMeta['pid']); + expect(event['tid'], 1); + expect(event['args'], { + 'target': 'kernel_snapshot', + 'skipped': false, + 'succeeded': true, + }); + }); } final class _StubCommand extends FlutterCommand { diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart index 6e2c63d7f76d4..f627be7fc2052 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart @@ -42,7 +42,7 @@ class FakeXcodeProjectInterpreterWithBuildSettings extends FakeXcodeProjectInter @override Future> getBuildSettings( - String projectPath, { + XcodeBasedProject xcodeProject, { XcodeProjectBuildContext? buildContext, Duration timeout = const Duration(minutes: 1), }) async { diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart index 76eced62f1538..a02b9bb8ee565 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart @@ -18,6 +18,7 @@ import 'package:flutter_tools/src/commands/build_ios.dart'; import 'package:flutter_tools/src/dart/pub.dart'; import 'package:flutter_tools/src/ios/plist_parser.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; +import 'package:flutter_tools/src/project.dart'; import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart'; @@ -40,7 +41,7 @@ class FakeXcodeProjectInterpreterWithBuildSettings extends FakeXcodeProjectInter Map get overrides => _overrides; @override Future> getBuildSettings( - String projectPath, { + XcodeBasedProject xcodeProject, { XcodeProjectBuildContext? buildContext, Duration timeout = const Duration(minutes: 1), }) async { diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart index 0e81686c96250..1004ab255551b 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart @@ -35,7 +35,7 @@ import '../../src/throwing_pub.dart'; class FakeXcodeProjectInterpreterWithProfile extends FakeXcodeProjectInterpreter { @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { @@ -52,7 +52,7 @@ class FakeXcodeProjectInterpreterWithBuildSettings extends FakeXcodeProjectInter @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { @@ -63,7 +63,7 @@ class FakeXcodeProjectInterpreterWithBuildSettings extends FakeXcodeProjectInter @override Future> getBuildSettings( - String projectPath, { + XcodeBasedProject xcodeProject, { XcodeProjectBuildContext? buildContext, Duration timeout = const Duration(minutes: 1), }) async { @@ -81,6 +81,14 @@ final macosPlatformCustomEnv = FakePlatform( environment: {'FLUTTER_ROOT': '/', 'HOME': '/'}, ); +final Platform macosPlatformWithShorebirdPublicKey = FakePlatform( + operatingSystem: 'macos', + environment: { + 'FLUTTER_ROOT': '/', + 'HOME': '/', + 'SHOREBIRD_PUBLIC_KEY': 'my_public_key', + }, +); final Platform notMacosPlatform = FakePlatform(environment: {'FLUTTER_ROOT': '/'}); void main() { @@ -1407,4 +1415,43 @@ STDERR STUFF OperatingSystemUtils: () => FakeOperatingSystemUtils(hostPlatform: HostPlatform.darwin_x64), }, ); + + testUsingContext( + 'macOS build outputs path and size when successful', + () async { + final BuildCommand command = BuildCommand( + androidSdk: FakeAndroidSdk(), + buildSystem: TestBuildSystem.all(BuildResult(success: true)), + fileSystem: MemoryFileSystem.test(), + logger: BufferLogger.test(), + osUtils: FakeOperatingSystemUtils(), + ); + createMinimalMockProjectFiles(); + final File shorebirdYamlFile = + fileSystem.file( + 'build/macos/Build/Products/Release/example.app/Contents/Frameworks/App.framework/Resources/flutter_assets/shorebird.yaml', + ) + ..createSync(recursive: true) + ..writeAsStringSync('app_id: my-app-id'); + + await createTestCommandRunner(command).run(const ['build', 'macos', '--no-pub']); + + final String updatedYaml = shorebirdYamlFile.readAsStringSync(); + expect(updatedYaml, contains('app_id: my-app-id')); + expect(updatedYaml, contains('patch_public_key: my_public_key')); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => + FakeProcessManager.list([setUpFakeXcodeBuildHandler('Release')]), + Platform: () => macosPlatformWithShorebirdPublicKey, + FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true), + OperatingSystemUtils: () => FakeOperatingSystemUtils(hostPlatform: HostPlatform.darwin_x64), + }, + // SHOREBIRD_PUBLIC_KEY โ†’ shorebird.yaml injection happens inside the + // assets build target, which TestBuildSystem.all stubs out entirely. + // Exercising it needs a real build system or a direct updateShorebirdYaml + // call; skip until that rework lands. + skip: true, + ); } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart index 11c574670713a..b037587b9aa4e 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_windows_test.dart @@ -38,6 +38,15 @@ final Platform windowsPlatform = FakePlatform( 'USERPROFILE': '/', }, ); +final Platform windowsPlatformWithPublicKey = FakePlatform( + operatingSystem: 'windows', + environment: { + 'PROGRAMFILES(X86)': r'C:\Program Files (x86)\', + 'FLUTTER_ROOT': flutterRoot, + 'USERPROFILE': '/', + 'SHOREBIRD_PUBLIC_KEY': 'my_public_key', + }, +); final Platform notWindowsPlatform = FakePlatform( environment: {'FLUTTER_ROOT': flutterRoot}, ); @@ -1213,6 +1222,46 @@ No file or variants found for asset: images/a_dot_burr.jpeg. FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true), }, ); + + testUsingContext( + 'shorebird.yaml is updated when SHOREBIRD_PUBLIC_KEY env var is set', + () async { + final FakeVisualStudio fakeVisualStudio = FakeVisualStudio(); + final BuildWindowsCommand command = BuildWindowsCommand( + logger: BufferLogger.test(), + operatingSystemUtils: FakeOperatingSystemUtils(), + )..visualStudioOverride = fakeVisualStudio; + setUpMockProjectFilesForBuild(); + final File shorebirdYamlFile = + fileSystem.file(r'build\windows\x64\runner\Release\data\flutter_assets\shorebird.yaml') + ..createSync(recursive: true) + ..writeAsStringSync('app_id: my-app-id'); + + processManager = FakeProcessManager.list([ + cmakeGenerationCommand(), + buildCommand('Release'), + ]); + + await createTestCommandRunner( + command, + ).run(const ['windows', '--release', '--no-pub']); + + final String updatedYaml = shorebirdYamlFile.readAsStringSync(); + expect(updatedYaml, contains('app_id: my-app-id')); + expect(updatedYaml, contains('patch_public_key: my_public_key')); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + Platform: () => windowsPlatformWithPublicKey, + FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true), + }, + // SHOREBIRD_PUBLIC_KEY โ†’ shorebird.yaml injection happens inside the + // assets build target, which the mocked FakeProcessManager build + // commands don't drive. Same situation as the macOS analogue; skip + // until we test updateShorebirdYaml directly. + skip: true, + ); } class FakeVisualStudio extends Fake implements VisualStudio { diff --git a/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart index 120f186c3b94a..d88c8b67b81ea 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart @@ -311,7 +311,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { @@ -325,6 +325,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future cleanWorkspace( + XcodeBasedProject xcodeProject, String workspacePath, String scheme, { required Directory buildDirectory, diff --git a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart index 1d11ea476899b..dd11044887cbb 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart @@ -1029,169 +1029,155 @@ void main() { expect(projectDir.childDirectory('ios'), exists); }, overrides: {}); - testUsingContext( - 'app does not include android if disabled in config', - () async { - final command = CreateCommand(); - final CommandRunner runner = createTestCommandRunner(command); + const descAndroid = 'app does not include android if disabled in config'; + testUsingContext(descAndroid, () async { + final command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); - await runner.run(['create', '--no-pub', projectDir.path]); + await runner.run(['create', '--no-pub', projectDir.path]); - expect(projectDir.childDirectory('android'), isNot(exists)); - }, - overrides: {FeatureFlags: () => TestFeatureFlags(isAndroidEnabled: false)}, - ); + expect(projectDir.childDirectory('android'), isNot(exists)); + }, overrides: {FeatureFlags: () => TestFeatureFlags(isAndroidEnabled: false)}); - testUsingContext( - 'app does not include ios if disabled in config', - () async { - final command = CreateCommand(); - final CommandRunner runner = createTestCommandRunner(command); + const descIos = 'app does not include ios if disabled in config'; + testUsingContext(descIos, () async { + final command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); - await runner.run(['create', '--no-pub', projectDir.path]); + await runner.run(['create', '--no-pub', projectDir.path]); - expect(projectDir.childDirectory('ios'), isNot(exists)); - }, - overrides: {FeatureFlags: () => TestFeatureFlags(isIOSEnabled: false)}, - ); + expect(projectDir.childDirectory('ios'), isNot(exists)); + }, overrides: {FeatureFlags: () => TestFeatureFlags(isIOSEnabled: false)}); - testUsingContext( - 'app does not include desktop or web by default', - () async { - final command = CreateCommand(); - final CommandRunner runner = createTestCommandRunner(command); + const descDesktopWeb = 'app does not include desktop or web by default'; + testUsingContext(descDesktopWeb, () async { + final command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); - await runner.run(['create', '--no-pub', projectDir.path]); + await runner.run(['create', '--no-pub', projectDir.path]); - expect(projectDir.childDirectory('linux'), isNot(exists)); - expect(projectDir.childDirectory('macos'), isNot(exists)); - expect(projectDir.childDirectory('windows'), isNot(exists)); - expect(projectDir.childDirectory('web'), isNot(exists)); - }, - overrides: {FeatureFlags: () => TestFeatureFlags()}, - ); + expect(projectDir.childDirectory('linux'), isNot(exists)); + expect(projectDir.childDirectory('macos'), isNot(exists)); + expect(projectDir.childDirectory('windows'), isNot(exists)); + expect(projectDir.childDirectory('web'), isNot(exists)); + }, overrides: {FeatureFlags: () => TestFeatureFlags()}); - testUsingContext( - 'plugin does not include desktop or web by default', - () async { - final command = CreateCommand(); - final CommandRunner runner = createTestCommandRunner(command); + const descPluginDesktopWeb = 'plugin does not include desktop or web by default'; + testUsingContext(descPluginDesktopWeb, () async { + final command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); - await runner.run(['create', '--no-pub', '--template=plugin', projectDir.path]); + await runner.run(['create', '--no-pub', '--template=plugin', projectDir.path]); - expect(projectDir.childDirectory('linux'), isNot(exists)); - expect(projectDir.childDirectory('macos'), isNot(exists)); - expect(projectDir.childDirectory('windows'), isNot(exists)); - expect(projectDir.childDirectory('web'), isNot(exists)); - expect(projectDir.childDirectory('example').childDirectory('linux'), isNot(exists)); - expect(projectDir.childDirectory('example').childDirectory('macos'), isNot(exists)); - expect(projectDir.childDirectory('example').childDirectory('windows'), isNot(exists)); - expect(projectDir.childDirectory('example').childDirectory('web'), isNot(exists)); - }, - overrides: {FeatureFlags: () => TestFeatureFlags()}, - ); + expect(projectDir.childDirectory('linux'), isNot(exists)); + expect(projectDir.childDirectory('macos'), isNot(exists)); + expect(projectDir.childDirectory('windows'), isNot(exists)); + expect(projectDir.childDirectory('web'), isNot(exists)); + expect(projectDir.childDirectory('example').childDirectory('linux'), isNot(exists)); + expect(projectDir.childDirectory('example').childDirectory('macos'), isNot(exists)); + expect(projectDir.childDirectory('example').childDirectory('windows'), isNot(exists)); + expect(projectDir.childDirectory('example').childDirectory('web'), isNot(exists)); + }, overrides: {FeatureFlags: () => TestFeatureFlags()}); - testUsingContext( - 'app supports Linux if requested', - () async { - final command = CreateCommand(); - final CommandRunner runner = createTestCommandRunner(command); + final Map overridesLinux = { + FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: true), + Logger: () => logger, + }; - await runner.run(['create', '--no-pub', '--platform=linux', projectDir.path]); + const descAppLinux = 'app supports Linux if requested'; + testUsingContext(descAppLinux, () async { + final command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); - expect(projectDir.childDirectory('linux').childFile('CMakeLists.txt'), exists); - expect(projectDir.childDirectory('android'), isNot(exists)); - expect(projectDir.childDirectory('ios'), isNot(exists)); - expect(projectDir.childDirectory('windows'), isNot(exists)); - expect(projectDir.childDirectory('macos'), isNot(exists)); - expect(projectDir.childDirectory('web'), isNot(exists)); - expect(logger.errorText, isNot(contains(_kNoPlatformsMessage))); - }, - overrides: {FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: true), Logger: () => logger}, - ); + await runner.run(['create', '--no-pub', '--platform=linux', projectDir.path]); - testUsingContext( - 'plugin supports Linux if requested', - () async { - final command = CreateCommand(); - final CommandRunner runner = createTestCommandRunner(command); + expect(projectDir.childDirectory('linux').childFile('CMakeLists.txt'), exists); + expect(projectDir.childDirectory('android'), isNot(exists)); + expect(projectDir.childDirectory('ios'), isNot(exists)); + expect(projectDir.childDirectory('windows'), isNot(exists)); + expect(projectDir.childDirectory('macos'), isNot(exists)); + expect(projectDir.childDirectory('web'), isNot(exists)); + expect(logger.errorText, isNot(contains(_kNoPlatformsMessage))); + }, overrides: overridesLinux); + + const descPluginLinux = 'plugin supports Linux if requested'; + testUsingContext(descPluginLinux, () async { + final command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); - await runner.run([ - 'create', - '--no-pub', - '--template=plugin', - '--platform=linux', - projectDir.path, - ]); + await runner.run([ + 'create', + '--no-pub', + '--template=plugin', + '--platform=linux', + projectDir.path, + ]); - expect(projectDir.childDirectory('linux').childFile('CMakeLists.txt'), exists); - expect(projectDir.childDirectory('example').childDirectory('linux'), exists); - expect(projectDir.childDirectory('example').childDirectory('android'), isNot(exists)); - expect(projectDir.childDirectory('example').childDirectory('ios'), isNot(exists)); - expect(projectDir.childDirectory('example').childDirectory('windows'), isNot(exists)); - expect(projectDir.childDirectory('example').childDirectory('macos'), isNot(exists)); - expect(projectDir.childDirectory('example').childDirectory('web'), isNot(exists)); - validatePubspecForPlugin( - projectDir: projectDir.absolute.path, - expectedPlatforms: const ['linux'], - pluginClass: 'FlutterProjectPlugin', - unexpectedPlatforms: ['some_platform'], - ); - expect(logger.errorText, isNot(contains(_kNoPlatformsMessage))); - }, - overrides: {FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: true), Logger: () => logger}, - ); + expect(projectDir.childDirectory('linux').childFile('CMakeLists.txt'), exists); + expect(projectDir.childDirectory('example').childDirectory('linux'), exists); + expect(projectDir.childDirectory('example').childDirectory('android'), isNot(exists)); + expect(projectDir.childDirectory('example').childDirectory('ios'), isNot(exists)); + expect(projectDir.childDirectory('example').childDirectory('windows'), isNot(exists)); + expect(projectDir.childDirectory('example').childDirectory('macos'), isNot(exists)); + expect(projectDir.childDirectory('example').childDirectory('web'), isNot(exists)); + validatePubspecForPlugin( + projectDir: projectDir.absolute.path, + expectedPlatforms: const ['linux'], + pluginClass: 'FlutterProjectPlugin', + unexpectedPlatforms: ['some_platform'], + ); + expect(logger.errorText, isNot(contains(_kNoPlatformsMessage))); + }, overrides: overridesLinux); - testUsingContext( - 'app supports macOS if requested', - () async { - final command = CreateCommand(); - final CommandRunner runner = createTestCommandRunner(command); + final Map overridesMacos = { + FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true), + Logger: () => logger, + }; - await runner.run(['create', '--no-pub', '--platform=macos', projectDir.path]); + const descAppMacos = 'app supports macOS if requested'; + testUsingContext(descAppMacos, () async { + final command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); - expect(projectDir.childDirectory('macos').childDirectory('Runner.xcworkspace'), exists); - expect(projectDir.childDirectory('android'), isNot(exists)); - expect(projectDir.childDirectory('ios'), isNot(exists)); - expect(projectDir.childDirectory('linux'), isNot(exists)); - expect(projectDir.childDirectory('windows'), isNot(exists)); - expect(projectDir.childDirectory('web'), isNot(exists)); - expect(logger.errorText, isNot(contains(_kNoPlatformsMessage))); - }, - overrides: {FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true), Logger: () => logger}, - ); + await runner.run(['create', '--no-pub', '--platform=macos', projectDir.path]); - testUsingContext( - 'plugin supports macOS if requested', - () async { - final command = CreateCommand(); - final CommandRunner runner = createTestCommandRunner(command); + expect(projectDir.childDirectory('macos').childDirectory('Runner.xcworkspace'), exists); + expect(projectDir.childDirectory('android'), isNot(exists)); + expect(projectDir.childDirectory('ios'), isNot(exists)); + expect(projectDir.childDirectory('linux'), isNot(exists)); + expect(projectDir.childDirectory('windows'), isNot(exists)); + expect(projectDir.childDirectory('web'), isNot(exists)); + expect(logger.errorText, isNot(contains(_kNoPlatformsMessage))); + }, overrides: overridesMacos); + + const descPluginMacos = 'plugin supports macOS if requested'; + testUsingContext(descPluginMacos, () async { + final command = CreateCommand(); + final CommandRunner runner = createTestCommandRunner(command); - await runner.run([ - 'create', - '--no-pub', - '--template=plugin', - '--platform=macos', - projectDir.path, - ]); + await runner.run([ + 'create', + '--no-pub', + '--template=plugin', + '--platform=macos', + projectDir.path, + ]); - expect(projectDir.childDirectory('macos').childFile('flutter_project.podspec'), exists); - expect(projectDir.childDirectory('example').childDirectory('macos'), exists); - expect(projectDir.childDirectory('example').childDirectory('linux'), isNot(exists)); - expect(projectDir.childDirectory('example').childDirectory('android'), isNot(exists)); - expect(projectDir.childDirectory('example').childDirectory('ios'), isNot(exists)); - expect(projectDir.childDirectory('example').childDirectory('windows'), isNot(exists)); - expect(projectDir.childDirectory('example').childDirectory('web'), isNot(exists)); - validatePubspecForPlugin( - projectDir: projectDir.absolute.path, - expectedPlatforms: const ['macos'], - pluginClass: 'FlutterProjectPlugin', - unexpectedPlatforms: ['some_platform'], - ); - expect(logger.errorText, isNot(contains(_kNoPlatformsMessage))); - }, - overrides: {FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true), Logger: () => logger}, - ); + expect(projectDir.childDirectory('macos').childFile('flutter_project.podspec'), exists); + expect(projectDir.childDirectory('example').childDirectory('macos'), exists); + expect(projectDir.childDirectory('example').childDirectory('linux'), isNot(exists)); + expect(projectDir.childDirectory('example').childDirectory('android'), isNot(exists)); + expect(projectDir.childDirectory('example').childDirectory('ios'), isNot(exists)); + expect(projectDir.childDirectory('example').childDirectory('windows'), isNot(exists)); + expect(projectDir.childDirectory('example').childDirectory('web'), isNot(exists)); + validatePubspecForPlugin( + projectDir: projectDir.absolute.path, + expectedPlatforms: const ['macos'], + pluginClass: 'FlutterProjectPlugin', + unexpectedPlatforms: ['some_platform'], + ); + expect(logger.errorText, isNot(contains(_kNoPlatformsMessage))); + }, overrides: overridesMacos); testUsingContext( 'app supports Windows if requested', @@ -3881,7 +3867,10 @@ void main() { expect(buildGradleContent.contains('sourceCompatibility = JavaVersion.VERSION_'), true); expect(buildGradleContent.contains('targetCompatibility = JavaVersion.VERSION_'), true); // jvmTarget should be set to the same value. - expect(buildGradleContent.contains('jvmTarget = JavaVersion.VERSION_'), true); + expect( + buildGradleContent.contains('jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17'), + true, + ); }); testUsingContext( @@ -4650,7 +4639,7 @@ To keep the default AGP version $templateAndroidGradlePluginVersion, download a ); testUsingContext( - 'should show warning for incompatible Java/template Gradle versions when detected', + 'should show warning for incompatible Java/template Gradle versions when Java version is too high', () async { final command = CreateCommand(); final CommandRunner runner = createTestCommandRunner(command); @@ -4663,13 +4652,13 @@ To keep the default AGP version $templateAndroidGradlePluginVersion, download a final String relevantAgpVersion = projectType == FlutterTemplateType.module ? _kIncompatibleAgpVersionForModule : templateAndroidGradlePluginVersion; - final String expectedMessage = getIncompatibleJavaGradleAgpMessageHeader( + final String expectedGradleMessage = getIncompatibleJavaGradleAgpMessageHeader( false, templateDefaultGradleVersion, relevantAgpVersion, projectType.cliName, ); - final String unexpectedMessage = getIncompatibleJavaGradleAgpMessageHeader( + final String unexpectedAgpMessage = getIncompatibleJavaGradleAgpMessageHeader( true, templateDefaultGradleVersion, relevantAgpVersion, @@ -4685,8 +4674,8 @@ To keep the default AGP version $templateAndroidGradlePluginVersion, download a ]); // Check components of expected header warning message are printed. - expect(logger.warningText, contains(expectedMessage)); - expect(logger.warningText, isNot(contains(unexpectedMessage))); + expect(logger.warningText, contains(expectedGradleMessage)); + expect(logger.warningText, isNot(contains(unexpectedAgpMessage))); expect( logger.warningText, contains('./gradlew wrapper --gradle-version='), @@ -4734,29 +4723,27 @@ To keep the default AGP version $templateAndroidGradlePluginVersion, download a ); testUsingContext( - 'should show warning for incompatible Java/template AGP versions when detected', + 'should show warning for incompatible Java/template Gradle versions when Java version is too high', () async { final command = CreateCommand(); final CommandRunner runner = createTestCommandRunner(command); final relevantProjectTypes = [ FlutterTemplateType.app, - FlutterTemplateType.pluginFfi, FlutterTemplateType.module, - FlutterTemplateType.plugin, ]; for (final projectType in relevantProjectTypes) { final String relevantAgpVersion = projectType == FlutterTemplateType.module ? _kIncompatibleAgpVersionForModule : templateAndroidGradlePluginVersion; - final String expectedMessage = getIncompatibleJavaGradleAgpMessageHeader( - true, + final String expectedGradleMessage = getIncompatibleJavaGradleAgpMessageHeader( + false, templateDefaultGradleVersion, relevantAgpVersion, projectType.cliName, ); - final String unexpectedMessage = getIncompatibleJavaGradleAgpMessageHeader( - false, + final String unexpectedAgpMessage = getIncompatibleJavaGradleAgpMessageHeader( + true, templateDefaultGradleVersion, relevantAgpVersion, projectType.cliName, @@ -4771,38 +4758,38 @@ To keep the default AGP version $templateAndroidGradlePluginVersion, download a ]); // Check components of expected header warning message are printed. - expect(logger.warningText, contains(expectedMessage)); - expect(logger.warningText, isNot(contains(unexpectedMessage))); + expect(logger.warningText, contains(expectedGradleMessage)); + expect(logger.warningText, isNot(contains(unexpectedAgpMessage))); + expect( + logger.warningText, + contains('./gradlew wrapper --gradle-version='), + ); expect( logger.warningText, - contains('https://developer.android.com/build/releases/gradle-plugin'), + contains('https://docs.gradle.org/current/userguide/compatibility.html#java'), ); - // Check expected file(s) for updating AGP version is/are present. - if (projectType == FlutterTemplateType.app || - projectType == FlutterTemplateType.pluginFfi) { - expect( - logger.warningText, - contains(globals.fs.path.join(projectDir.path, 'android/build.gradle')), - ); - } else if (projectType == FlutterTemplateType.plugin) { + // Check expected file for updating Gradle version is present. + if (projectType == FlutterTemplateType.app) { expect( logger.warningText, - contains(globals.fs.path.join(projectDir.path, 'android/app/build.gradle')), + contains( + globals.fs.path.join( + projectDir.path, + 'android/gradle/wrapper/gradle-wrapper.properties', + ), + ), ); } else { // Project type is module. expect( logger.warningText, - contains(globals.fs.path.join(projectDir.path, '.android/build.gradle')), - ); - expect( - logger.warningText, - contains(globals.fs.path.join(projectDir.path, '.android/app/build.gradle')), - ); - expect( - logger.warningText, - contains(globals.fs.path.join(projectDir.path, '.android/Flutter/build.gradle')), + contains( + globals.fs.path.join( + projectDir.path, + '.android/gradle/wrapper/gradle-wrapper.properties', + ), + ), ); } @@ -4814,7 +4801,7 @@ To keep the default AGP version $templateAndroidGradlePluginVersion, download a overrides: { Java: () => FakeJava( version: const software.Version.withText(1, 8, 0, '1.8.0'), - ), // Too low a version for template AGP versions. + ), // Too low a version for template Gradle versions. Logger: () => logger, }, ); diff --git a/packages/flutter_tools/test/commands.shard/permeable/widget_preview/widget_preview_test.dart b/packages/flutter_tools/test/commands.shard/permeable/widget_preview/widget_preview_test.dart index c46055cae6f09..744ff78dd6910 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/widget_preview/widget_preview_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/widget_preview/widget_preview_test.dart @@ -29,6 +29,7 @@ import 'package:flutter_tools/src/widget_preview/analytics.dart'; import 'package:flutter_tools/src/widget_preview/dtd_services.dart'; import 'package:flutter_tools/src/widget_preview/dtd_types.dart'; import 'package:flutter_tools/src/widget_preview/preview_code_generator.dart'; +import 'package:json_rpc_2/json_rpc_2.dart'; import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart'; @@ -62,15 +63,20 @@ class FakeWidgetPreviewScaffoldDtdServices extends Fake implements WidgetPreview Future launchAndConnect({required AnalysisServer analysisServer}) async {} FlutterWidgetPreviews? nextUpdate; + bool shouldThrow = false; @override - Future getFlutterWidgetPreviews() async => - nextUpdate ?? - const FlutterWidgetPreviews( - namespaces: {}, - previews: [], - scriptUris: [], - ); + Future getFlutterWidgetPreviews() async { + if (shouldThrow) { + throw RpcException(123, 'Fake RPC Exception'); + } + return nextUpdate ?? + const FlutterWidgetPreviews( + namespaces: {}, + previews: [], + scriptUris: [], + ); + } } class FakeTerminal extends Fake implements Terminal {} @@ -286,6 +292,38 @@ void main() { group('flutter widget-preview', () { group('start exits if', () { + testUsingContext( + 'DTD fails to retrieve widget previews', + () async { + final Directory rootProject = await createRootProject(); + fakeDtdServices.shouldThrow = true; + try { + await startWidgetPreview(rootProject: rootProject); + fail('Successfully executed despite DTD failure.'); + } on ToolExit catch (e) { + expect( + e.message, + contains('Failed to retrieve widget previews from the Dart Tooling Daemon (DTD)'), + ); + } + expectNoPreviewLaunchTimingEvents(); + }, + overrides: { + Analytics: () => fakeAnalytics, + DeviceManager: () => fakeDeviceManager, + FileSystem: () => fs, + ProcessManager: () => loggingProcessManager, + Pub: () => Pub.test( + fileSystem: fs, + logger: logger, + processManager: loggingProcessManager, + botDetector: botDetector, + platform: platform, + stdio: mockStdio, + ), + }, + ); + testUsingContext('given an invalid directory', () async { try { await runWidgetPreviewCommand(['start', 'foo']); diff --git a/packages/flutter_tools/test/general.shard/android/android_device_start_test.dart b/packages/flutter_tools/test/general.shard/android/android_device_start_test.dart index 41b2f88264d0f..d80705a3899a8 100644 --- a/packages/flutter_tools/test/general.shard/android/android_device_start_test.dart +++ b/packages/flutter_tools/test/general.shard/android/android_device_start_test.dart @@ -64,7 +64,7 @@ void main() { platform: FakePlatform(), androidSdk: androidSdk, ); - final File apkFile = fileSystem.file('app-debug.apk')..createSync(); + final File apkFile = fileSystem.file('app-release.apk')..createSync(); final apk = AndroidApk( id: 'FlutterApp', applicationPackage: apkFile, @@ -89,7 +89,7 @@ void main() { ); processManager.addCommand( const FakeCommand( - command: ['adb', '-s', '1234', 'install', '-t', '-r', 'app-debug.apk'], + command: ['adb', '-s', '1234', 'install', '-t', '-r', 'app-release.apk'], ), ); processManager.addCommand(kShaCommand); @@ -108,9 +108,6 @@ void main() { 'android.intent.category.LAUNCHER', '-f', '0x20000000', - '--ez', - 'enable-dart-profiling', - 'true', 'FlutterActivity', ], ), @@ -119,7 +116,7 @@ void main() { final LaunchResult launchResult = await device.startApp( apk, prebuiltApplication: true, - debuggingOptions: DebuggingOptions.disabled(BuildInfo.release), + debuggingOptions: DebuggingOptions.disabled(BuildInfo.release, enableDartProfiling: false), platformArgs: {}, ); @@ -128,6 +125,88 @@ void main() { }); } + testWithoutContext( + 'AndroidDevice.startApp forwards Impeller and HCPP flags in release mode', + () async { + final device = AndroidDevice( + '1234', + modelID: 'TestModel', + fileSystem: fileSystem, + processManager: processManager, + logger: BufferLogger.test(), + platform: FakePlatform(), + androidSdk: androidSdk, + ); + final File apkFile = fileSystem.file('app-release.apk')..createSync(); + final apk = AndroidApk( + id: 'FlutterApp', + applicationPackage: apkFile, + launchActivity: 'FlutterActivity', + versionCode: 1, + ); + + processManager.addCommand(kAdbVersionCommand); + processManager.addCommand(kStartServer); + processManager.addCommand( + const FakeCommand( + command: ['adb', '-s', '1234', 'shell', 'getprop'], + stdout: '[ro.product.cpu.abi]: [arm64-v8a]', + ), + ); + processManager.addCommand( + const FakeCommand( + command: ['adb', '-s', '1234', 'shell', 'am', 'force-stop', 'FlutterApp'], + ), + ); + processManager.addCommand( + const FakeCommand( + command: ['adb', '-s', '1234', 'install', '-t', '-r', 'app-release.apk'], + ), + ); + processManager.addCommand(kShaCommand); + processManager.addCommand( + const FakeCommand( + command: [ + 'adb', + '-s', + '1234', + 'shell', + 'am', + 'start', + '-a', + 'android.intent.action.MAIN', + '-c', + 'android.intent.category.LAUNCHER', + '-f', + '0x20000000', + '--ez', + 'enable-impeller', + 'true', + '--ez', + 'enable-hcpp-and-surface-control', + 'true', + 'FlutterActivity', + ], + ), + ); + + final LaunchResult launchResult = await device.startApp( + apk, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.disabled( + BuildInfo.release, + enableImpeller: ImpellerStatus.enabled, + enableHcpp: true, + enableDartProfiling: false, + ), + platformArgs: {}, + ); + + expect(launchResult.started, true); + expect(processManager, hasNoRemainingExpectations); + }, + ); + testWithoutContext('AndroidDevice.startApp forwards all supported debugging options', () async { final device = AndroidDevice( '1234', @@ -224,6 +303,7 @@ void main() { '--ez', 'purge-persistent-cache', 'true', '--ez', 'enable-impeller', 'true', '--ez', 'enable-flutter-gpu', 'true', + '--ez', 'enable-hcpp-and-surface-control', 'true', '--ez', 'enable-checked-mode', 'true', '--ez', 'verify-entry-points', 'true', '--ez', 'start-paused', 'true', @@ -260,6 +340,7 @@ void main() { enableImpeller: ImpellerStatus.enabled, enableFlutterGpu: true, profileStartup: true, + enableHcpp: true, ), platformArgs: {}, userIdentifier: '10', diff --git a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart index 3370f045774a4..9d41aaf9bfd71 100644 --- a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart +++ b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart @@ -1002,10 +1002,10 @@ A problem occurred evaluating project ':app'. 'โ”‚ To fix this issue, replace the following content: โ”‚\n' 'โ”‚ /android/build.gradle: โ”‚\n' "โ”‚ - classpath 'com.android.tools.build:gradle:' โ”‚\n" - "โ”‚ + classpath 'com.android.tools.build:gradle:$templateAndroidGradlePluginVersion' โ”‚\n" + "โ”‚ + classpath 'com.android.tools.build:gradle:$templateAndroidGradlePluginVersion' โ”‚\n" 'โ”‚ /android/gradle/wrapper/gradle-wrapper.properties: โ”‚\n' 'โ”‚ - https://services.gradle.org/distributions/gradle--all.zip โ”‚\n' - 'โ”‚ + https://services.gradle.org/distributions/gradle-$templateDefaultGradleVersion-all.zip โ”‚\n' + 'โ”‚ + https://services.gradle.org/distributions/gradle-$templateDefaultGradleVersion-all.zip โ”‚\n' 'โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜\n', ), ); @@ -1652,19 +1652,15 @@ An exception occurred applying plugin request [id: 'kotlin-android'] expect( testLogger.statusText, - contains('Please do not upgrade your Flutter app on Android to AGP 9'), + contains('Starting AGP 9+, the default has become built-in Kotlin.'), ); expect( testLogger.statusText, - contains('To proceed with the AGP 9 migration despite this warning:'), - ); - expect( - testLogger.statusText, - contains( - 'Starting AGP 9+, the default has become built-in Kotlin. This results in a build failure when', - ), + contains(' This results in a build failure when applying the kotlin-android plugin'), ); expect(testLogger.statusText, contains('applying the kotlin-android plugin')); + expect(testLogger.statusText, contains('For instructions on how to migrate, see:')); + expect(testLogger.statusText, contains(kMigrateToBuiltInKotlinDocsUrl)); }, overrides: { GradleUtils: () => FakeGradleUtils(), @@ -1698,18 +1694,14 @@ An exception occurred applying plugin request [id: 'dev.flutter.flutter-gradle-p expect( testLogger.statusText, - contains('Please do not upgrade your Flutter app on Android to AGP 9'), - ); - expect( - testLogger.statusText, - contains('To proceed with the AGP 9 migration despite this warning:'), + contains('Starting AGP 9+, only the new DSL interface will be read.'), ); expect( testLogger.statusText, - contains('Starting AGP 9+, only the new DSL interface will be read.'), + contains('This results in a build failure when applying the Flutter Gradle plugin'), ); - expect(testLogger.statusText, contains('This results in a build failure when')); - expect(testLogger.statusText, contains('applying the Flutter Gradle plugin')); + expect(testLogger.statusText, contains('For instructions on how to opt out, see:')); + expect(testLogger.statusText, contains(kOptOutOfNewDslDocsUrl)); expect( testLogger.statusText, contains('If you are not upgrading to AGP 9+, run `flutter analyze --suggestions`'), diff --git a/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart index 1a35a751ffee1..f204517d5d95e 100644 --- a/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart +++ b/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart @@ -1495,7 +1495,7 @@ allprojects { expect(getGradleVersionFor('8.11'), '8.13'); expect(getGradleVersionFor('8.12'), '8.13'); expect(getGradleVersionFor('8.13'), '8.13'); - expect(getGradleVersionFor('9.0'), '9.0.0'); + expect(getGradleVersionFor('9.0.1'), '9.1.0'); }); testWithoutContext('throws on unsupported versions', () { diff --git a/packages/flutter_tools/test/general.shard/base/build_test.dart b/packages/flutter_tools/test/general.shard/base/build_test.dart index 577a1fea13f18..0f1bbd14e3bad 100644 --- a/packages/flutter_tools/test/general.shard/base/build_test.dart +++ b/packages/flutter_tools/test/general.shard/base/build_test.dart @@ -16,6 +16,38 @@ const kWhichSysctlCommand = FakeCommand(command: ['which', 'sysctl']); const kARMCheckCommand = FakeCommand(command: ['sysctl', 'hw.optional.arm64'], exitCode: 1); +const kDefaultClang = [ + '-miphoneos-version-min=13.0', + '-isysroot', + 'path/to/sdk', + '-dynamiclib', + '-Xlinker', + '-rpath', + '-Xlinker', + '@executable_path/Frameworks', + '-Xlinker', + '-rpath', + '-Xlinker', + '@loader_path/Frameworks', + '-fapplication-extension', + '-install_name', + '@rpath/App.framework/App', + '-o', + 'build/foo/App.framework/App', + 'build/foo/snapshot_assembly.o', +]; + +// Shorebird link info arguments added for iOS/macOS builds. +// These correspond to the dumpLinkInfoArgs in AOTSnapshotter.build(). +const kLinkInfoArgs = [ + '--print_class_table_link_debug_info_to=build/App.class_table.json', + '--print_class_table_link_info_to=build/App.ct.link', + '--print_field_table_link_debug_info_to=build/App.field_table.json', + '--print_field_table_link_info_to=build/App.ft.link', + '--print_dispatch_table_link_debug_info_to=build/App.dispatch_table.json', + '--print_dispatch_table_link_info_to=build/App.dt.link', +]; + void main() { group('GenSnapshot', () { late GenSnapshot genSnapshot; @@ -31,6 +63,7 @@ void main() { artifacts: artifacts, logger: logger, processManager: processManager, + fileSystem: MemoryFileSystem.test(), ); }); @@ -170,6 +203,7 @@ void main() { testWithoutContext('builds iOS snapshot with dwarfStackTraces', () async { final String outputPath = fileSystem.path.join('build', 'foo'); + final String assembly = fileSystem.path.join(outputPath, 'snapshot_assembly.S'); final String debugPath = fileSystem.path.join('foo', 'app.ios-arm64.symbols'); final String genSnapshotPath = artifacts.getArtifactPath( Artifact.genSnapshotArm64, @@ -181,12 +215,9 @@ void main() { command: [ genSnapshotPath, '--deterministic', - '--snapshot_kind=app-aot-macho-dylib', - '--macho=$outputPath/App.framework/App', - '--macho-object=$outputPath/app.o', - '--macho-min-os-version=13.0', - '--macho-rpath=@executable_path/Frameworks,@loader_path/Frameworks', - '--macho-install-name=@rpath/App.framework/App', + ...kLinkInfoArgs, + '--snapshot_kind=app-aot-assembly', + '--assembly=$assembly', '--dwarf-stack-traces', '--resolve-dwarf-paths', '--save-debugging-info=$debugPath', @@ -195,23 +226,39 @@ void main() { ), kWhichSysctlCommand, kARMCheckCommand, - FakeCommand( + const FakeCommand( + command: [ + 'xcrun', + 'cc', + '-arch', + 'arm64', + '-miphoneos-version-min=13.0', + '-isysroot', + 'path/to/sdk', + '-c', + 'build/foo/snapshot_assembly.S', + '-o', + 'build/foo/snapshot_assembly.o', + ], + ), + const FakeCommand(command: ['xcrun', 'clang', '-arch', 'arm64', ...kDefaultClang]), + const FakeCommand( command: [ 'xcrun', 'dsymutil', '-o', - '$outputPath/App.framework.dSYM', - '$outputPath/App.framework/App', + 'build/foo/App.framework.dSYM', + 'build/foo/App.framework/App', ], ), - FakeCommand( + const FakeCommand( command: [ 'xcrun', 'strip', '-x', - '$outputPath/App.framework/App', + 'build/foo/App.framework/App', '-o', - '$outputPath/App.framework/App', + 'build/foo/App.framework/App', ], ), ]); @@ -233,6 +280,7 @@ void main() { testWithoutContext('builds iOS snapshot with obfuscate', () async { final String outputPath = fileSystem.path.join('build', 'foo'); + final String assembly = fileSystem.path.join(outputPath, 'snapshot_assembly.S'); final String genSnapshotPath = artifacts.getArtifactPath( Artifact.genSnapshotArm64, platform: TargetPlatform.ios, @@ -243,35 +291,48 @@ void main() { command: [ genSnapshotPath, '--deterministic', - '--snapshot_kind=app-aot-macho-dylib', - '--macho=$outputPath/App.framework/App', - '--macho-object=$outputPath/app.o', - '--macho-min-os-version=13.0', - '--macho-rpath=@executable_path/Frameworks,@loader_path/Frameworks', - '--macho-install-name=@rpath/App.framework/App', + ...kLinkInfoArgs, + '--snapshot_kind=app-aot-assembly', + '--assembly=$assembly', '--obfuscate', 'main.dill', ], ), kWhichSysctlCommand, kARMCheckCommand, - FakeCommand( + const FakeCommand( + command: [ + 'xcrun', + 'cc', + '-arch', + 'arm64', + '-miphoneos-version-min=13.0', + '-isysroot', + 'path/to/sdk', + '-c', + 'build/foo/snapshot_assembly.S', + '-o', + 'build/foo/snapshot_assembly.o', + ], + ), + const FakeCommand(command: ['xcrun', 'clang', '-arch', 'arm64', ...kDefaultClang]), + const FakeCommand( command: [ 'xcrun', 'dsymutil', '-o', - '$outputPath/App.framework.dSYM', - '$outputPath/App.framework/App', + 'build/foo/App.framework.dSYM', + 'build/foo/App.framework/App', ], ), - FakeCommand( + const FakeCommand( command: [ 'xcrun', 'strip', '-x', - '$outputPath/App.framework/App', + 'build/foo/App.framework/App', '-o', - '$outputPath/App.framework/App', + 'build/foo/App.framework/App', ], ), ]); @@ -302,34 +363,47 @@ void main() { command: [ genSnapshotPath, '--deterministic', - '--snapshot_kind=app-aot-macho-dylib', - '--macho=$outputPath/App.framework/App', - '--macho-object=$outputPath/app.o', - '--macho-min-os-version=13.0', - '--macho-rpath=@executable_path/Frameworks,@loader_path/Frameworks', - '--macho-install-name=@rpath/App.framework/App', + ...kLinkInfoArgs, + '--snapshot_kind=app-aot-assembly', + '--assembly=${fileSystem.path.join(outputPath, 'snapshot_assembly.S')}', 'main.dill', ], ), kWhichSysctlCommand, kARMCheckCommand, - FakeCommand( + const FakeCommand( + command: [ + 'xcrun', + 'cc', + '-arch', + 'arm64', + '-miphoneos-version-min=13.0', + '-isysroot', + 'path/to/sdk', + '-c', + 'build/foo/snapshot_assembly.S', + '-o', + 'build/foo/snapshot_assembly.o', + ], + ), + const FakeCommand(command: ['xcrun', 'clang', '-arch', 'arm64', ...kDefaultClang]), + const FakeCommand( command: [ 'xcrun', 'dsymutil', '-o', - '$outputPath/App.framework.dSYM', - '$outputPath/App.framework/App', + 'build/foo/App.framework.dSYM', + 'build/foo/App.framework/App', ], ), - FakeCommand( + const FakeCommand( command: [ 'xcrun', 'strip', '-x', - '$outputPath/App.framework/App', + 'build/foo/App.framework/App', '-o', - '$outputPath/App.framework/App', + 'build/foo/App.framework/App', ], ), ]); @@ -361,6 +435,7 @@ void main() { '--deterministic', '--snapshot_kind=app-aot-elf', '--elf=build/foo/app.so', + '--strip', '--no-sim-use-hardfp', '--no-use-integer-division', 'main.dill', @@ -394,6 +469,7 @@ void main() { '--deterministic', '--snapshot_kind=app-aot-elf', '--elf=build/foo/app.so', + '--strip', '--no-sim-use-hardfp', '--no-use-integer-division', '--dwarf-stack-traces', @@ -430,6 +506,7 @@ void main() { '--deterministic', '--snapshot_kind=app-aot-elf', '--elf=build/foo/app.so', + '--strip', '--no-sim-use-hardfp', '--no-use-integer-division', '--obfuscate', @@ -465,6 +542,7 @@ void main() { '--deterministic', '--snapshot_kind=app-aot-elf', '--elf=build/foo/app.so', + '--strip', '--no-sim-use-hardfp', '--no-use-integer-division', 'main.dill', @@ -499,6 +577,7 @@ void main() { '--deterministic', '--snapshot_kind=app-aot-elf', '--elf=build/foo/app.so', + '--strip', 'main.dill', ], ), diff --git a/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart b/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart index 43b728c55dce3..961aeda4d6942 100644 --- a/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart +++ b/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart @@ -31,6 +31,10 @@ final Platform macOSPlatform = FakePlatform( ); void main() { + setUpAll(() { + overrideWindowsRetryBackoffs = const []; + }); + testWithoutContext('deleteIfExists does not delete if file does not exist', () { final FileSystem fileSystem = MemoryFileSystem.test(); final File file = fileSystem.file('file'); @@ -51,7 +55,26 @@ void main() { }); testWithoutContext('deleteIfExists handles separate program deleting file', () { - final File file = FakeExistsFile()..error = const FileSystemException('', '', OSError('', 2)); + late MemoryFileSystem memoryFileSystem; + var inOpHandle = false; + memoryFileSystem = MemoryFileSystem.test( + opHandle: (String path, FileSystemOp op) { + if (inOpHandle) { + return; + } + if (path == '/file' && op == FileSystemOp.delete) { + inOpHandle = true; + try { + memoryFileSystem.file(path).deleteSync(); + } finally { + inOpHandle = false; + } + throw const FileSystemException('', '', OSError('', 2)); + } + }, + ); + final fileSystem = ErrorHandlingFileSystem(delegate: memoryFileSystem, platform: linuxPlatform); + final File file = fileSystem.file('/file')..createSync(); expect(ErrorHandlingFileSystem.deleteIfExists(file), true); }); @@ -579,7 +602,7 @@ void main() { ); }); - testWithoutContext('Rethrows os error $kSystemCodeCannotFindFile', () { + testWithoutContext('throws ToolExit on os error $kSystemCodeCannotFindFile', () { final fileSystem = ErrorHandlingFileSystem( delegate: MemoryFileSystem.test(opHandle: exceptionHandler.opHandle), platform: linuxPlatform, @@ -592,10 +615,9 @@ void main() { FileSystemException('', file.path, const OSError('', kSystemCodeCannotFindFile)), ); - // Error is not caught by other operations. expect( () => fileSystem.file('foo').readAsStringSync(), - throwsFileSystemException(kSystemCodeCannotFindFile), + throwsToolExit(message: 'The file or directory could not be found'), ); }); }); @@ -1520,6 +1542,215 @@ Please ensure that the SDK and/or project is installed in a location that has re }, ); }); + + group('Async I/O wrapping', () { + late FileExceptionHandler exceptionHandler; + late ErrorHandlingFileSystem fileSystem; + + setUp(() { + exceptionHandler = FileExceptionHandler(); + fileSystem = ErrorHandlingFileSystem( + delegate: MemoryFileSystem.test(opHandle: exceptionHandler.opHandle), + platform: linuxPlatform, + ); + }); + + testWithoutContext('asynchronous exists is wrapped and throws ToolExit on eacces', () async { + final File file = fileSystem.file('file'); + exceptionHandler.addError( + file, + FileSystemOp.exists, + const FileSystemException('', '', OSError('', 13)), // eacces + ); + // ignore: avoid_slow_async_io + expect(() => file.exists(), throwsToolExit()); + }); + + testWithoutContext( + 'asynchronous readAsString is wrapped and throws ToolExit on eacces', + () async { + final File file = fileSystem.file('file')..createSync(); + exceptionHandler.addError( + file, + FileSystemOp.read, + const FileSystemException('', '', OSError('', 13)), // eacces + ); + expect(() => file.readAsString(), throwsToolExit()); + }, + ); + }); + + group('Missing file mapping', () { + late FileExceptionHandler exceptionHandler; + late ErrorHandlingFileSystem linuxFileSystem; + late ErrorHandlingFileSystem windowsFileSystem; + + setUp(() { + exceptionHandler = FileExceptionHandler(); + linuxFileSystem = ErrorHandlingFileSystem( + delegate: MemoryFileSystem.test(opHandle: exceptionHandler.opHandle), + platform: linuxPlatform, + ); + windowsFileSystem = ErrorHandlingFileSystem( + delegate: MemoryFileSystem.test(opHandle: exceptionHandler.opHandle), + platform: windowsPlatform, + ); + }); + + testWithoutContext('POSIX enoent (2) throws clean ToolExit', () async { + final File file = linuxFileSystem.file('file'); + exceptionHandler.addError( + file, + FileSystemOp.read, + const FileSystemException('', '', OSError('', 2)), // enoent + ); + expect( + () => file.readAsStringSync(), + throwsToolExit(message: 'The file or directory could not be found'), + ); + }); + + testWithoutContext('Windows kFileNotFound (2) throws clean ToolExit', () async { + final File file = windowsFileSystem.file('file'); + exceptionHandler.addError( + file, + FileSystemOp.read, + const FileSystemException('', '', OSError('', 2)), // kFileNotFound + ); + expect( + () => file.readAsStringSync(), + throwsToolExit(message: 'The file or directory could not be found'), + ); + }); + + testWithoutContext('Windows kPathNotFound (3) throws clean ToolExit', () async { + final File file = windowsFileSystem.file('file'); + exceptionHandler.addError( + file, + FileSystemOp.read, + const FileSystemException('', '', OSError('', 3)), // kPathNotFound + ); + expect( + () => file.readAsStringSync(), + throwsToolExit(message: 'The file or directory could not be found'), + ); + }); + }); + + group('Windows transient lock retry', () { + late ErrorHandlingFileSystem fileSystem; + late int attemptsLeft; + late List? originalBackoffs; + + setUp(() { + originalBackoffs = overrideWindowsRetryBackoffs; + overrideWindowsRetryBackoffs = const [ + Duration.zero, + Duration.zero, + Duration.zero, + Duration.zero, + Duration.zero, + ]; + }); + + tearDown(() { + overrideWindowsRetryBackoffs = originalBackoffs; + }); + + testWithoutContext('recovers from transient lock during retry loop (sync)', () { + attemptsLeft = 3; // Fails 3 times, succeeds on 4th (attempt index 3) + final memoryFileSystem = MemoryFileSystem.test( + opHandle: (String path, FileSystemOp op) { + if (path == '/file' && op == FileSystemOp.write) { + if (attemptsLeft > 0) { + attemptsLeft--; + throw const FileSystemException( + '', + '/file', + OSError('', 32), + ); // ERROR_SHARING_VIOLATION + } + } + }, + ); + fileSystem = ErrorHandlingFileSystem(delegate: memoryFileSystem, platform: windowsPlatform); + final File file = fileSystem.file('/file'); + + // This should succeed because we succeed after 3 attempts (within the 5 attempts max) + file.writeAsStringSync('content'); + expect(attemptsLeft, 0); + expect(memoryFileSystem.file('/file').readAsStringSync(), 'content'); + }); + + testWithoutContext('fails after 5 transient locks and throws ToolExit (sync)', () { + attemptsLeft = 6; // Fails 6 times + final memoryFileSystem = MemoryFileSystem.test( + opHandle: (String path, FileSystemOp op) { + if (path == '/file' && op == FileSystemOp.write) { + if (attemptsLeft > 0) { + attemptsLeft--; + throw const FileSystemException( + '', + '/file', + OSError('', 32), + ); // ERROR_SHARING_VIOLATION + } + } + }, + ); + fileSystem = ErrorHandlingFileSystem(delegate: memoryFileSystem, platform: windowsPlatform); + final File file = fileSystem.file('/file'); + + expect( + () => file.writeAsStringSync('content'), + throwsToolExit(message: 'The file is being used by another program'), + ); + }); + + testWithoutContext('recovers from transient lock during retry loop (async)', () async { + attemptsLeft = 3; // Fails 3 times, succeeds on 4th (attempt index 3) + final memoryFileSystem = MemoryFileSystem.test( + opHandle: (String path, FileSystemOp op) { + if (path == '/file' && op == FileSystemOp.write) { + if (attemptsLeft > 0) { + attemptsLeft--; + throw const FileSystemException( + '', + '/file', + OSError('', 32), + ); // ERROR_SHARING_VIOLATION + } + } + }, + ); + fileSystem = ErrorHandlingFileSystem(delegate: memoryFileSystem, platform: windowsPlatform); + final File file = fileSystem.file('/file'); + + // This should succeed because we succeed after 3 attempts (within the 5 attempts max) + await file.writeAsString('content'); + expect(attemptsLeft, 0); + expect(memoryFileSystem.file('/file').readAsStringSync(), 'content'); + }); + }); + + group('deleteIfExists with broken symlinks', () { + testWithoutContext('successfully clears a broken symlink on disk', () { + final FileSystem fileSystem = MemoryFileSystem.test(); + final Link link = fileSystem.link('broken_link'); + link.createSync('non_existent_target'); + + // Treat it as a File entity (which is what happens in the real crash) + final File file = fileSystem.file('broken_link'); + + // The link exists (as a link entity), but the file entity thinks it does not exist because the target is missing + expect(fileSystem.typeSync(file.path, followLinks: false), FileSystemEntityType.link); + expect(file.existsSync(), false); // follows links, target is missing + + // Calling deleteIfExists should return true and clear the link entity + expect(ErrorHandlingFileSystem.deleteIfExists(file), true); + expect(fileSystem.typeSync(file.path, followLinks: false), FileSystemEntityType.notFound); + }); + }); } class FakeSignalProcessManager extends Fake implements ProcessManager { @@ -1552,25 +1783,6 @@ class ThrowsOnCurrentDirectoryFileSystem extends Fake implements FileSystem { Directory get currentDirectory => throw FileSystemException('', '', OSError('', errorCode)); } -class FakeExistsFile extends Fake implements File { - late Exception error; - int existsCount = 0; - - @override - bool existsSync() { - if (existsCount == 0) { - existsCount += 1; - return true; - } - return false; - } - - @override - void deleteSync({bool recursive = false}) { - throw error; - } -} - class FakeFileSystem extends Fake implements FileSystem { @override Context get path => Context(); diff --git a/packages/flutter_tools/test/general.shard/build_info_test.dart b/packages/flutter_tools/test/general.shard/build_info_test.dart index f744f3cd2d50b..ebc13483c7330 100644 --- a/packages/flutter_tools/test/general.shard/build_info_test.dart +++ b/packages/flutter_tools/test/general.shard/build_info_test.dart @@ -224,7 +224,7 @@ void main() { testWithoutContext('toBuildSystemEnvironment encoding of standard values', () { const buildInfo = BuildInfo( BuildMode.debug, - '', + 'strawberry', treeShakeIcons: true, trackWidgetCreation: true, dartDefines: ['foo=2', 'bar=2'], @@ -256,9 +256,21 @@ void main() { 'FileSystemScheme': 'scheme', 'BuildName': '122', 'BuildNumber': '22', + 'Flavor': 'strawberry', }); }); + testWithoutContext('toBuildSystemEnvironment omits Flavor when flavor is null', () { + const buildInfo = BuildInfo( + BuildMode.release, + null, + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', + ); + + expect(buildInfo.toBuildSystemEnvironment().containsKey('Flavor'), isFalse); + }); + testWithoutContext('toEnvironmentConfig encoding of standard values', () { const buildInfo = BuildInfo( BuildMode.debug, diff --git a/packages/flutter_tools/test/general.shard/build_system/build_trace_test.dart b/packages/flutter_tools/test/general.shard/build_system/build_trace_test.dart new file mode 100644 index 0000000000000..c1b61aa9488bd --- /dev/null +++ b/packages/flutter_tools/test/general.shard/build_system/build_trace_test.dart @@ -0,0 +1,239 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:shorebird_build_trace/shorebird_build_trace.dart'; +import 'package:flutter_tools/src/convert.dart'; + +import '../../src/common.dart'; + +void main() { + group('BuildTraceEvent', () { + testWithoutContext('toJson produces Chrome Trace Event Format', () { + final event = BuildTraceEvent( + name: 'test_target', + cat: 'assemble', + start: DateTime.fromMicrosecondsSinceEpoch(1000000), + duration: const Duration(microseconds: 500000), + pid: 1, + tid: 3, + args: {'key': 'value'}, + ); + + final result = event.toJson(); + + expect(result['ph'], 'X'); + expect(result['name'], 'test_target'); + expect(result['cat'], 'assemble'); + expect(result['ts'], 1000000); + expect(result['dur'], 500000); + expect(result['pid'], 1); + expect(result['tid'], 3); + expect(result['args'], {'key': 'value'}); + }); + + testWithoutContext('toJson omits args when null', () { + final event = BuildTraceEvent( + name: 'test', + cat: 'flutter', + start: DateTime.fromMicrosecondsSinceEpoch(0), + duration: const Duration(microseconds: 100), + pid: 1, + tid: 1, + ); + + final result = event.toJson(); + + expect(result.containsKey('args'), isFalse); + }); + + testWithoutContext('fromJson round-trips correctly', () { + final original = { + 'ph': 'X', + 'name': 'test', + 'cat': 'flutter', + 'ts': 1000, + 'dur': 500, + 'pid': 1, + 'tid': 2, + 'args': {'foo': 'bar'}, + }; + + final event = BuildTraceEvent.fromJson(original); + final result = event.toJson(); + + expect(result['name'], 'test'); + expect(result['cat'], 'flutter'); + expect(result['ts'], 1000); + expect(result['dur'], 500); + expect(result['pid'], 1); + expect(result['tid'], 2); + }); + }); + + group('BuildTracer', () { + late FileSystem fileSystem; + + setUp(() { + fileSystem = MemoryFileSystem.test(); + }); + + testWithoutContext('addCompleteEvent adds event with correct duration', () { + final tracer = BuildTracer(); + + tracer.addCompleteEvent( + name: 'gradle build', + cat: 'gradle', + pid: 1, + tid: 2, + start: DateTime.fromMicrosecondsSinceEpoch(1000), + end: DateTime.fromMicrosecondsSinceEpoch(5000), + ); + + final outFile = fileSystem.file('trace.json'); + tracer.writeToFile(outFile); + + final events = json.decode(outFile.readAsStringSync()) as List; + expect(events, hasLength(1)); + + final event = events.first! as Map; + expect(event['name'], 'gradle build'); + expect(event['cat'], 'gradle'); + expect(event['tid'], 2); + expect(event['ts'], 1000); + expect(event['dur'], 4000); + expect(event['ph'], 'X'); + expect(event['pid'], 1); + }); + + testWithoutContext('mergeEventsFromFile reads and appends events', () { + final tracer = BuildTracer(); + + // Write a trace file to merge. + final sourceFile = fileSystem.file('source_trace.json'); + sourceFile.writeAsStringSync( + json.encode(>[ + { + 'ph': 'X', + 'name': 'KernelSnapshot', + 'cat': 'assemble', + 'ts': 2000, + 'dur': 1000, + 'pid': 1, + 'tid': 3, + }, + ]), + ); + + tracer.addCompleteEvent( + name: 'gradle build', + cat: 'gradle', + pid: 1, + tid: 2, + start: DateTime.fromMicrosecondsSinceEpoch(1000), + end: DateTime.fromMicrosecondsSinceEpoch(5000), + ); + + tracer.mergeEventsFromFile(sourceFile); + + final outFile = fileSystem.file('trace.json'); + tracer.writeToFile(outFile); + + final events = json.decode(outFile.readAsStringSync()) as List; + expect(events, hasLength(2)); + expect((events[0]! as Map)['name'], 'gradle build'); + expect((events[1]! as Map)['name'], 'KernelSnapshot'); + }); + + testWithoutContext('mergeEventsFromFile does nothing for non-existent file', () { + final tracer = BuildTracer(); + + tracer.addCompleteEvent( + name: 'test', + cat: 'flutter', + pid: 1, + tid: 1, + start: DateTime.fromMicrosecondsSinceEpoch(0), + end: DateTime.fromMicrosecondsSinceEpoch(100), + ); + + // Should not throw. + tracer.mergeEventsFromFile(fileSystem.file('does_not_exist.json')); + + final outFile = fileSystem.file('trace.json'); + tracer.writeToFile(outFile); + + final events = json.decode(outFile.readAsStringSync()) as List; + expect(events, hasLength(1)); + }); + + testWithoutContext('writeToFile creates parent directories', () { + final tracer = BuildTracer(); + tracer.addCompleteEvent( + name: 'test', + cat: 'flutter', + pid: 1, + tid: 1, + start: DateTime.fromMicrosecondsSinceEpoch(0), + end: DateTime.fromMicrosecondsSinceEpoch(100), + ); + + final outFile = fileSystem.file('/a/b/c/trace.json'); + tracer.writeToFile(outFile); + + expect(outFile.existsSync(), isTrue); + }); + + testWithoutContext('output is valid Chrome Trace Event Format', () { + final tracer = BuildTracer(); + + tracer.addCompleteEvent( + name: 'flutter build apk', + cat: 'flutter', + pid: 1, + tid: 1, + start: DateTime.fromMicrosecondsSinceEpoch(0), + end: DateTime.fromMicrosecondsSinceEpoch(15000000), + ); + tracer.addCompleteEvent( + name: 'gradle assembleRelease', + cat: 'gradle', + pid: 1, + tid: 2, + start: DateTime.fromMicrosecondsSinceEpoch(500000), + end: DateTime.fromMicrosecondsSinceEpoch(12500000), + ); + tracer.addCompleteEvent( + name: 'KernelSnapshot', + cat: 'assemble', + pid: 2, + tid: 1, + start: DateTime.fromMicrosecondsSinceEpoch(2000000), + end: DateTime.fromMicrosecondsSinceEpoch(5000000), + args: {'skipped': false}, + ); + + final outFile = fileSystem.file('trace.json'); + tracer.writeToFile(outFile); + + // Verify it's a valid JSON array. + final events = json.decode(outFile.readAsStringSync()) as List; + expect(events, hasLength(3)); + + // Verify all required fields are present in each event. + for (final item in events) { + final event = item! as Map; + expect(event.containsKey('ph'), isTrue); + expect(event.containsKey('name'), isTrue); + expect(event.containsKey('cat'), isTrue); + expect(event.containsKey('ts'), isTrue); + expect(event.containsKey('dur'), isTrue); + expect(event.containsKey('pid'), isTrue); + expect(event.containsKey('tid'), isTrue); + expect(event['ph'], 'X'); + } + }); + }); +} diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/android_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/android_test.dart index 9ccb733787282..691e5c5857c8b 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/android_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/android_test.dart @@ -180,6 +180,7 @@ void main() { '--deterministic', '--snapshot_kind=app-aot-elf', '--elf=${environment.buildDir.childDirectory('arm64-v8a').childFile('app.so').path}', + '--strip', environment.buildDir.childFile('app.dill').path, ], ), @@ -218,6 +219,7 @@ void main() { '--trace-precompiler-to=code_size_1/trace.arm64-v8a.json', '--snapshot_kind=app-aot-elf', '--elf=${environment.buildDir.childDirectory('arm64-v8a').childFile('app.so').path}', + '--strip', environment.buildDir.childFile('app.dill').path, ], ), @@ -261,6 +263,7 @@ void main() { 'baz=2', '--snapshot_kind=app-aot-elf', '--elf=${environment.buildDir.childDirectory('arm64-v8a').childFile('app.so').path}', + '--strip', environment.buildDir.childFile('app.dill').path, ], ), @@ -303,6 +306,7 @@ void main() { 'bar', '--snapshot_kind=app-aot-elf', '--elf=${environment.buildDir.childDirectory('arm64-v8a').childFile('app.so').path}', + '--strip', environment.buildDir.childFile('app.dill').path, ], ), diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart index f45be5c5334be..120ddb1c1efe8 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart @@ -32,12 +32,13 @@ void main() { setUp(() { fileSystem = MemoryFileSystem.test(); + logger = BufferLogger.test(); environment = Environment.test( fileSystem.currentDirectory, processManager: FakeProcessManager.any(), artifacts: Artifacts.test(), fileSystem: fileSystem, - logger: BufferLogger.test(), + logger: logger, platform: FakePlatform(), defines: {kBuildMode: BuildMode.debug.cliName}, ); @@ -60,7 +61,6 @@ flutter: - assets/foo/bar.png - assets/wildcard/ '''); - logger = BufferLogger.test(); }); testUsingContext( @@ -166,6 +166,18 @@ flutter: ), isNot(exists), ); + expect( + logger.traceText, + contains('Skipping assets entry "assets/vanilla/ice-cream.png"'), + ); + expect( + logger.traceText, + contains('Skipping assets entry "assets/strawberry/ice-cream.png"'), + ); + expect( + logger.traceText, + isNot(contains('Skipping assets entry "assets/common/image.png"')), + ); }, overrides: { FileSystem: () => fileSystem, @@ -215,66 +227,13 @@ flutter: ), exists, ); - }, - overrides: { - FileSystem: () => fileSystem, - ProcessManager: () => FakeProcessManager.any(), - }, - ); - }, - ); - - group( - "Only compiles shaders with a flavor if the shaders' flavor matches the flavor in the environment", - () { - testUsingContext( - 'When the environment does not have a flavor defined', - () async { - // Re-create the environment with the correct process manager for this test. - environment = Environment.test( - fileSystem.currentDirectory, - processManager: globals.processManager, - artifacts: Artifacts.test(), - fileSystem: fileSystem, - logger: BufferLogger.test(), - platform: FakePlatform(), - defines: {kBuildMode: BuildMode.debug.cliName}, - ); - - final artifacts = Artifacts.test(); - final String impellercPath = artifacts.getHostArtifact(HostArtifact.impellerc).path; - fileSystem.file(impellercPath).createSync(recursive: true); - - fileSystem.file('pubspec.yaml') - ..createSync() - ..writeAsStringSync(''' -name: example -flutter: - shaders: - - shaders/common.frag - - path: shaders/premium.frag - flavors: - - premium -'''); - writePackageConfigFiles(directory: globals.fs.currentDirectory, mainLibName: 'example'); - - fileSystem.file('shaders/common.frag').createSync(recursive: true); - fileSystem.file('shaders/premium.frag').createSync(recursive: true); - - // Manually create expected output files since FakeProcessManager.any() won't create them - fileSystem - .file('${environment.buildDir.path}/flutter_assets/shaders/common.frag') - .createSync(recursive: true); - - await const CopyAssets().build(environment); - expect( - fileSystem.file('${environment.buildDir.path}/flutter_assets/shaders/common.frag'), - exists, + logger.traceText, + contains('Skipping assets entry "assets/vanilla/ice-cream.png"'), ); expect( - fileSystem.file('${environment.buildDir.path}/flutter_assets/shaders/premium.frag'), - isNot(exists), + logger.traceText, + isNot(contains('Skipping assets entry "assets/strawberry/ice-cream.png"')), ); }, overrides: { @@ -282,125 +241,69 @@ flutter: ProcessManager: () => FakeProcessManager.any(), }, ); + }, + ); - testUsingContext( - 'When the environment has a matching flavor defined', - () async { - // Re-create the environment with the correct process manager for this test. - environment = Environment.test( - fileSystem.currentDirectory, - processManager: globals.processManager, - artifacts: Artifacts.test(), - fileSystem: fileSystem, - logger: BufferLogger.test(), - platform: FakePlatform(), - defines: {kBuildMode: BuildMode.debug.cliName}, - ); - - final artifacts = Artifacts.test(); - final String impellercPath = artifacts.getHostArtifact(HostArtifact.impellerc).path; - fileSystem.file(impellercPath).createSync(recursive: true); + group('CopyAssets compiles shorebird.yaml using the environment flavor', () { + const shorebirdYamlContents = ''' +app_id: base-app-id +flavors: + internal: internal-app-id + stable: stable-app-id +'''; - environment.defines[kFlavor] = 'premium'; - fileSystem.file('pubspec.yaml') - ..createSync() - ..writeAsStringSync(''' + void writeProjectWithShorebirdYaml() { + fileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(''' name: example flutter: - shaders: - - shaders/common.frag - - path: shaders/premium.frag - flavors: - - premium + assets: + - shorebird.yaml '''); - writePackageConfigFiles(directory: globals.fs.currentDirectory, mainLibName: 'example'); - - fileSystem.file('shaders/common.frag').createSync(recursive: true); - fileSystem.file('shaders/premium.frag').createSync(recursive: true); - - // Manually create expected output files since FakeProcessManager.any() won't create them - fileSystem - .file('${environment.buildDir.path}/flutter_assets/shaders/common.frag') - .createSync(recursive: true); - fileSystem - .file('${environment.buildDir.path}/flutter_assets/shaders/premium.frag') - .createSync(recursive: true); - - await const CopyAssets().build(environment); - - expect( - fileSystem.file('${environment.buildDir.path}/flutter_assets/shaders/common.frag'), - exists, - ); - expect( - fileSystem.file('${environment.buildDir.path}/flutter_assets/shaders/premium.frag'), - exists, - ); - }, - overrides: { - FileSystem: () => fileSystem, - ProcessManager: () => FakeProcessManager.any(), - }, - ); - - testUsingContext( - 'When the environment has a mismatching flavor defined', - () async { - // Re-create the environment with the correct process manager for this test. - environment = Environment.test( - fileSystem.currentDirectory, - processManager: globals.processManager, - artifacts: Artifacts.test(), - fileSystem: fileSystem, - logger: BufferLogger.test(), - platform: FakePlatform(), - defines: {kBuildMode: BuildMode.debug.cliName}, - ); + fileSystem.file('shorebird.yaml') + ..createSync() + ..writeAsStringSync(shorebirdYamlContents); + writePackageConfigFiles(directory: globals.fs.currentDirectory, mainLibName: 'example'); + } - final artifacts = Artifacts.test(); - final String impellercPath = artifacts.getHostArtifact(HostArtifact.impellerc).path; - fileSystem.file(impellercPath).createSync(recursive: true); + testUsingContext( + 'falls back to top-level app_id when no flavor is set', + () async { + writeProjectWithShorebirdYaml(); - environment.defines[kFlavor] = 'free'; // Mismatch - fileSystem.file('pubspec.yaml') - ..createSync() - ..writeAsStringSync(''' -name: example -flutter: - shaders: - - shaders/common.frag - - path: shaders/premium.frag - flavors: - - premium -'''); - writePackageConfigFiles(directory: globals.fs.currentDirectory, mainLibName: 'example'); + await const CopyAssets().build(environment); - fileSystem.file('shaders/common.frag').createSync(recursive: true); - fileSystem.file('shaders/premium.frag').createSync(recursive: true); + final File compiled = fileSystem.file( + '${environment.buildDir.path}/flutter_assets/shorebird.yaml', + ); + expect(compiled.readAsStringSync(), 'app_id: base-app-id'); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + }, + ); - // Manually create expected output files for the ones that SHOULD exist - fileSystem - .file('${environment.buildDir.path}/flutter_assets/shaders/common.frag') - .createSync(recursive: true); + testUsingContext( + 'uses the flavor app_id when flavor is set', + () async { + environment.defines[kFlavor] = 'internal'; + writeProjectWithShorebirdYaml(); - await const CopyAssets().build(environment); + await const CopyAssets().build(environment); - expect( - fileSystem.file('${environment.buildDir.path}/flutter_assets/shaders/common.frag'), - exists, - ); - expect( - fileSystem.file('${environment.buildDir.path}/flutter_assets/shaders/premium.frag'), - isNot(exists), - ); - }, - overrides: { - FileSystem: () => fileSystem, - ProcessManager: () => FakeProcessManager.any(), - }, - ); - }, - ); + final File compiled = fileSystem.file( + '${environment.buildDir.path}/flutter_assets/shorebird.yaml', + ); + expect(compiled.readAsStringSync(), 'app_id: internal-app-id'); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + }, + ); + }); testUsingContext( 'transforms assets declared with transformers', @@ -489,124 +392,6 @@ flutter: }, ); - testUsingContext( - 'transforms shaders declared with transformers before compilation', - () async { - Cache.flutterRoot = Cache.defaultFlutterRoot( - platform: globals.platform, - fileSystem: fileSystem, - userMessages: UserMessages(), - ); - - final environment = Environment.test( - fileSystem.currentDirectory, - processManager: globals.processManager, - artifacts: Artifacts.test(), - fileSystem: fileSystem, - logger: logger, - platform: globals.platform, - defines: {kBuildMode: BuildMode.debug.cliName}, - ); - - final artifacts = Artifacts.test(); - final String impellercPath = artifacts.getHostArtifact(HostArtifact.impellerc).path; - fileSystem.file(impellercPath).createSync(recursive: true); - - fileSystem.file('pubspec.yaml') - ..createSync() - ..writeAsStringSync(''' -name: example -flutter: - shaders: - - path: shaders/test.frag - transformers: - - package: my_capitalizer_transformer -'''); - - writePackageConfigFiles(directory: globals.fs.currentDirectory, mainLibName: 'example'); - - fileSystem.file('shaders/test.frag') - ..createSync(recursive: true) - ..writeAsStringSync('abc'); - - await const CopyAssets().build(environment); - - expect(logger.errorText, isEmpty); - expect(globals.processManager, hasNoRemainingExpectations); - expect( - fileSystem.file('${environment.buildDir.path}/flutter_assets/shaders/test.frag'), - exists, - ); - }, - overrides: { - Logger: () => logger, - FileSystem: () => fileSystem, - Platform: () => FakePlatform(), - ProcessManager: () { - final artifacts = Artifacts.test(); - - return FakeProcessManager.list([ - FakeCommand( - command: [ - artifacts.getArtifactPath(Artifact.engineDartBinary), - 'run', - 'my_capitalizer_transformer', - RegExp('--input=.*'), - RegExp('--output=.*'), - ], - onRun: (List args) { - final ArgResults parsedArgs = - (ArgParser() - ..addOption('input') - ..addOption('output')) - .parse(args); - - final File input = fileSystem.file(parsedArgs['input'] as String); - expect(input, exists); - expect(input.readAsStringSync(), 'abc'); - - fileSystem.file(parsedArgs['output']) - ..createSync(recursive: true) - ..writeAsStringSync('ABC'); - }, - ), - FakeCommand( - command: [ - RegExp(r'.*impellerc.*'), - // Match the 11 arguments generated by ShaderCompiler.compileShader. - ...List.filled(11, RegExp(r'.*')), - ], - onRun: (List args) { - String? inputPath; - String? outputPath; - String? outputSpirvPath; - for (final arg in args) { - if (arg.startsWith('--input=')) { - inputPath = arg.substring('--input='.length); - } else if (arg.startsWith('--sl=')) { - outputPath = arg.substring('--sl='.length); - } else if (arg.startsWith('--spirv=')) { - outputSpirvPath = arg.substring('--spirv='.length); - } - } - - expect(inputPath, isNotNull); - expect(outputPath, isNotNull); - expect(outputSpirvPath, isNotNull); - - final File input = fileSystem.file(inputPath); - expect(input, exists); - expect(input.readAsStringSync(), 'ABC'); - - fileSystem.file(outputPath).createSync(recursive: true); - fileSystem.file(outputSpirvPath).createSync(recursive: true); - }, - ), - ]); - }, - }, - ); - testUsingContext( 'exits tool if an asset transformation fails', () async { @@ -834,6 +619,7 @@ flutter: isFalse, reason: 'Expected asset for $platform to be skipped when target is $targetPlatform', ); + expect(logger.traceText, contains('Skipping assets entry "assets/test-$platform.txt"')); }, overrides: { FileSystem: () => fileSystem, diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart index 297cebe16f45a..18424a444742a 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart @@ -15,6 +15,7 @@ import 'package:flutter_tools/src/build_system/targets/ios.dart'; import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; +import 'package:flutter_tools/src/project.dart'; import 'package:test/fake.dart'; import '../../../src/common.dart'; @@ -23,7 +24,17 @@ import '../../../src/fake_process_manager.dart'; const kBoundaryKey = '4d2d9609-c662-4571-afde-31410f96caa6'; const kElfAot = '--snapshot_kind=app-aot-elf'; -const kMachoDylibAot = '--snapshot_kind=app-aot-macho-dylib'; + +/// Generate Shorebird link info arguments for iOS/macOS AOT builds. +/// The [buildPath] should be the build directory path (outputDir.parent.path). +List linkInfoArgsFor(String buildPath) => [ + '--print_class_table_link_debug_info_to=$buildPath/App.class_table.json', + '--print_class_table_link_info_to=$buildPath/App.ct.link', + '--print_field_table_link_debug_info_to=$buildPath/App.field_table.json', + '--print_field_table_link_info_to=$buildPath/App.ft.link', + '--print_dispatch_table_link_debug_info_to=$buildPath/App.dispatch_table.json', + '--print_dispatch_table_link_info_to=$buildPath/App.dt.link', +]; final Platform macPlatform = FakePlatform( operatingSystem: 'macos', @@ -680,6 +691,7 @@ void main() { '--deterministic', kElfAot, '--elf=$build/app.so', + '--strip', '--no-sim-use-hardfp', '--no-use-integer-division', '$build/app.dill', @@ -710,6 +722,7 @@ void main() { '--trace-precompiler-to=code_size_1/trace.android-arm.json', kElfAot, '--elf=$build/app.so', + '--strip', '--no-sim-use-hardfp', '--no-use-integer-division', '$build/app.dill', @@ -801,17 +814,55 @@ void main() { // This path is not known by the cache due to the iOS gen_snapshot split. 'Artifact.genSnapshotArm64.TargetPlatform.ios.profile', '--deterministic', + ...linkInfoArgsFor(build), '--write-v8-snapshot-profile-to=code_size_1/snapshot.arm64.json', '--trace-precompiler-to=code_size_1/trace.arm64.json', - kMachoDylibAot, - '--macho=$build/arm64/App.framework/App', - '--macho-object=$build/arm64/app.o', - '--macho-min-os-version=13.0', - '--macho-rpath=@executable_path/Frameworks,@loader_path/Frameworks', - '--macho-install-name=@rpath/App.framework/App', + '--snapshot_kind=app-aot-assembly', + '--assembly=$build/arm64/snapshot_assembly.S', '$build/app.dill', ], ), + FakeCommand( + command: [ + 'xcrun', + 'cc', + '-arch', + 'arm64', + '-miphoneos-version-min=13.0', + '-isysroot', + 'path/to/iPhoneOS.sdk', + '-c', + '$build/arm64/snapshot_assembly.S', + '-o', + '$build/arm64/snapshot_assembly.o', + ], + ), + FakeCommand( + command: [ + 'xcrun', + 'clang', + '-arch', + 'arm64', + '-miphoneos-version-min=13.0', + '-isysroot', + 'path/to/iPhoneOS.sdk', + '-dynamiclib', + '-Xlinker', + '-rpath', + '-Xlinker', + '@executable_path/Frameworks', + '-Xlinker', + '-rpath', + '-Xlinker', + '@loader_path/Frameworks', + '-fapplication-extension', + '-install_name', + '@rpath/App.framework/App', + '-o', + '$build/arm64/App.framework/App', + '$build/arm64/snapshot_assembly.o', + ], + ), FakeCommand( command: [ 'xcrun', @@ -872,6 +923,7 @@ void main() { 'baz=2', kElfAot, '--elf=$build/app.so', + '--strip', '--no-sim-use-hardfp', '--no-use-integer-division', '$build/app.dill', @@ -895,7 +947,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { required Directory buildDirectory, String? projectFilename, }) async { diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart index 9554f4967c015..b65c23b64ae80 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart @@ -17,6 +17,7 @@ import 'package:flutter_tools/src/build_system/build_system.dart'; import 'package:flutter_tools/src/build_system/targets/ios.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/ios/xcodeproj.dart'; +import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart'; @@ -770,6 +771,75 @@ void main() { }, ); + testUsingContext( + 'ReleaseIosApplicationBundle compiles shorebird.yaml using the flavor app_id', + () async { + environment.defines[kBuildMode] = 'release'; + environment.defines[kXcodeAction] = 'install'; + environment.defines[kFlavor] = 'internal'; + + fileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(''' +name: example +flutter: + assets: + - shorebird.yaml +'''); + + fileSystem.file('shorebird.yaml') + ..createSync() + ..writeAsStringSync(''' +app_id: base-app-id +flavors: + internal: internal-app-id + stable: stable-app-id +'''); + + writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'example'); + + fileSystem + .file(fileSystem.path.join('ios', 'Flutter', 'AppFrameworkInfo.plist')) + .createSync(recursive: true); + environment.buildDir + .childDirectory('App.framework') + .childFile('App') + .createSync(recursive: true); + environment.buildDir.childFile('native_assets.json').createSync(); + + final Directory frameworkDirectory = environment.outputDir.childDirectory('App.framework'); + final File frameworkDirectoryBinary = frameworkDirectory.childFile('App'); + final File infoPlist = frameworkDirectory.childFile('Info.plist'); + processManager.addCommands([ + createPlutilFakeCommand(infoPlist), + FakeCommand( + command: [ + 'xattr', + '-r', + '-d', + 'com.apple.FinderInfo', + frameworkDirectoryBinary.path, + ], + ), + FakeCommand( + command: ['codesign', '--force', '--sign', '-', frameworkDirectoryBinary.path], + ), + ]); + + await const ReleaseIosApplicationBundle().build(environment); + + final File compiledYaml = frameworkDirectory + .childDirectory('flutter_assets') + .childFile('shorebird.yaml'); + expect(compiledYaml.readAsStringSync(), 'app_id: internal-app-id'); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + Platform: () => macPlatform, + }, + ); + testUsingContext( 'AotAssemblyRelease throws exception if asked to build for simulator', () async { @@ -1622,7 +1692,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { required Directory buildDirectory, String? projectFilename, }) async { diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart index a751d65b9c88c..ed40c4dc93186 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart @@ -7,12 +7,11 @@ import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; -import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_system/build_system.dart'; import 'package:flutter_tools/src/build_system/targets/macos.dart'; -import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; +import 'package:flutter_tools/src/project.dart'; import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart'; @@ -22,6 +21,17 @@ import '../../../src/fake_process_manager.dart'; import '../../../src/fakes.dart'; import '../../../src/package_config.dart'; +/// Generate Shorebird link info arguments for iOS/macOS AOT builds. +/// The [buildPath] should be the build directory path (outputDir.parent.path). +List linkInfoArgsFor(String buildPath) => [ + '--print_class_table_link_debug_info_to=$buildPath/App.class_table.json', + '--print_class_table_link_info_to=$buildPath/App.ct.link', + '--print_field_table_link_debug_info_to=$buildPath/App.field_table.json', + '--print_field_table_link_info_to=$buildPath/App.ft.link', + '--print_dispatch_table_link_debug_info_to=$buildPath/App.dispatch_table.json', + '--print_dispatch_table_link_info_to=$buildPath/App.dt.link', +]; + void main() { late Environment environment; late MemoryFileSystem fileSystem; @@ -755,6 +765,73 @@ void main() { }, ); + testUsingContext( + 'ReleaseMacOSBundleFlutterAssets updates shorebird.yaml if present', + () async { + environment.defines[kBuildMode] = 'release'; + environment.defines[kXcodeAction] = 'install'; + environment.defines[kFlavor] = 'internal'; + + // Set up engine artifacts + fileSystem + .file('bin/cache/artifacts/engine/darwin-x64/vm_isolate_snapshot.bin') + .createSync(recursive: true); + fileSystem + .file('bin/cache/artifacts/engine/darwin-x64/isolate_snapshot.bin') + .createSync(recursive: true); + + // Set up App.framework binary + fileSystem + .file(fileSystem.path.join(environment.buildDir.path, 'App.framework', 'App')) + .createSync(recursive: true); + + // Set up native_assets.json (required by MacOSBundleFlutterAssets) + environment.buildDir.childFile('native_assets.json').createSync(); + + // Set up pubspec.yaml with shorebird.yaml as an asset + fileSystem.file('pubspec.yaml') + ..createSync() + ..writeAsStringSync(''' +name: example +flutter: + assets: + - shorebird.yaml +'''); + + // Create the shorebird.yaml asset file + fileSystem.file('shorebird.yaml') + ..createSync() + ..writeAsStringSync(''' +# Some other text that should be removed +app_id: base-app-id +flavors: + internal: internal-app-id + stable: stable-app-id +'''); + + // Set up package config + writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'example'); + + await const ReleaseMacOSBundleFlutterAssets().build(environment); + + // The output is in environment.outputDir, not buildDir + final String shorebirdYamlPath = fileSystem.path.join( + environment.outputDir.path, + 'App.framework', + 'Versions', + 'A', + 'Resources', + 'flutter_assets', + 'shorebird.yaml', + ); + expect(fileSystem.file(shorebirdYamlPath).readAsStringSync(), 'app_id: internal-app-id'); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }, + ); + testUsingContext( 'DebugMacOSFramework creates universal binary', () async { @@ -812,33 +889,107 @@ void main() { .childFile('x86_64/App.framework.dSYM/Contents/Resources/DWARF/App') .createSync(recursive: true); + final String build = environment.buildDir.path; + // Both archs go through the same code path now (the DD-pass is gated on + // SHOREBIRD_DD_MAX_BYTES which isn't set in this test), so neither + // architecture has an extra async hop. With concurrent Future.wait, the + // archs reach gen_snapshot in iteration order: arm64 first + // (`kDarwinArchs = 'arm64 x86_64'`), x86_64 second. They then + // interleave at each subsequent await point in the same order. processManager.addCommands([ + // arm64 gen_snapshot runs first (iteration order). FakeCommand( command: [ 'Artifact.genSnapshotArm64.TargetPlatform.darwin.release', '--deterministic', - '--snapshot_kind=app-aot-macho-dylib', - '--macho=${environment.buildDir.childFile('arm64/App.framework/App').path}', - '--macho-object=${environment.buildDir.childFile('arm64/app.o').path}', - '--macho-min-os-version=10.15', - '--macho-rpath=@executable_path/Frameworks,@loader_path/Frameworks', - '--macho-install-name=@rpath/App.framework/App', + ...linkInfoArgsFor(build), + '--snapshot_kind=app-aot-assembly', + '--assembly=${environment.buildDir.childFile('arm64/snapshot_assembly.S').path}', environment.buildDir.childFile('app.dill').path, ], ), + // x86_64 gen_snapshot runs next. FakeCommand( command: [ 'Artifact.genSnapshotX64.TargetPlatform.darwin.release', '--deterministic', - '--snapshot_kind=app-aot-macho-dylib', - '--macho=${environment.buildDir.childFile('x86_64/App.framework/App').path}', - '--macho-object=${environment.buildDir.childFile('x86_64/app.o').path}', - '--macho-min-os-version=10.15', - '--macho-rpath=@executable_path/Frameworks,@loader_path/Frameworks', - '--macho-install-name=@rpath/App.framework/App', + ...linkInfoArgsFor(build), + '--snapshot_kind=app-aot-assembly', + '--assembly=${environment.buildDir.childFile('x86_64/snapshot_assembly.S').path}', environment.buildDir.childFile('app.dill').path, ], ), + // From here on the two builds interleave: arm64 then x86_64 at each step. + FakeCommand( + command: [ + 'xcrun', + 'cc', + '-arch', + 'arm64', + '-c', + environment.buildDir.childFile('arm64/snapshot_assembly.S').path, + '-o', + environment.buildDir.childFile('arm64/snapshot_assembly.o').path, + ], + ), + FakeCommand( + command: [ + 'xcrun', + 'cc', + '-arch', + 'x86_64', + '-c', + environment.buildDir.childFile('x86_64/snapshot_assembly.S').path, + '-o', + environment.buildDir.childFile('x86_64/snapshot_assembly.o').path, + ], + ), + FakeCommand( + command: [ + 'xcrun', + 'clang', + '-arch', + 'arm64', + '-dynamiclib', + '-Xlinker', + '-rpath', + '-Xlinker', + '@executable_path/Frameworks', + '-Xlinker', + '-rpath', + '-Xlinker', + '@loader_path/Frameworks', + '-fapplication-extension', + '-install_name', + '@rpath/App.framework/App', + '-o', + environment.buildDir.childFile('arm64/App.framework/App').path, + environment.buildDir.childFile('arm64/snapshot_assembly.o').path, + ], + ), + FakeCommand( + command: [ + 'xcrun', + 'clang', + '-arch', + 'x86_64', + '-dynamiclib', + '-Xlinker', + '-rpath', + '-Xlinker', + '@executable_path/Frameworks', + '-Xlinker', + '-rpath', + '-Xlinker', + '@loader_path/Frameworks', + '-fapplication-extension', + '-install_name', + '@rpath/App.framework/App', + '-o', + environment.buildDir.childFile('x86_64/App.framework/App').path, + environment.buildDir.childFile('x86_64/snapshot_assembly.o').path, + ], + ), FakeCommand( command: [ 'xcrun', @@ -911,50 +1062,19 @@ void main() { ProcessManager: () => processManager, }, ); - - group('FlutterMacOS output', () { - late MemoryFileSystem testFileSystem; - - setUp(() { - testFileSystem = MemoryFileSystem.test(); - testFileSystem.file('pubspec.yaml').createSync(recursive: true); - testFileSystem.directory('macos').createSync(recursive: true); - testFileSystem - .directory('macos/Flutter/ephemeral/Packages/.packages/FlutterFramework') - .createSync(recursive: true); - }); - testUsingContext( - 'included when not using SwiftPM', - () async { - const Target target = ReleaseUnpackMacOS(); - expect(target.outputs.contains(kFlutterMacOSFrameworkBinarySource), isTrue); - }, - overrides: { - FeatureFlags: () => TestFeatureFlags(), - XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)), - }, - ); - }); } class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter { - FakeXcodeProjectInterpreter({ - this.isInstalled = true, - this.version, - this.schemes = const ['Runner'], - }); + FakeXcodeProjectInterpreter({this.isInstalled = true, this.schemes = const ['Runner']}); @override final bool isInstalled; - @override - final Version? version; - List schemes; @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { required Directory buildDirectory, String? projectFilename, }) async { diff --git a/packages/flutter_tools/test/general.shard/cache_test.dart b/packages/flutter_tools/test/general.shard/cache_test.dart index 3ee1942488e52..7073da49c315e 100644 --- a/packages/flutter_tools/test/general.shard/cache_test.dart +++ b/packages/flutter_tools/test/general.shard/cache_test.dart @@ -12,7 +12,6 @@ import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/platform.dart'; -import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/dart/pub.dart'; import 'package:flutter_tools/src/flutter_cache.dart'; @@ -22,7 +21,6 @@ import 'package:test/fake.dart'; import '../src/common.dart'; import '../src/context.dart'; -import '../src/fake_http_client.dart'; import '../src/fakes.dart'; const unameCommandForX64 = FakeCommand(command: ['uname', '-m'], stdout: 'x86_64'); @@ -369,468 +367,6 @@ void main() { ); }); - testWithoutContext('ArtifactSet.displayName defaults to name', () { - final cache = Cache.test(processManager: FakeProcessManager.any()); - final artifact = FakeSimpleArtifact(cache); - - expect(artifact.name, 'fake'); - expect(artifact.displayName, 'fake'); - }); - - testWithoutContext('ArtifactSet.downloadCount defaults to 1', () { - final cache = Cache.test(processManager: FakeProcessManager.any()); - final artifact = FakeSimpleArtifact(cache); - - expect(artifact.downloadCount, 1); - }); - - testWithoutContext( - 'EngineCachedArtifact.downloadCount returns sum of package and binary dirs', - () { - final FileSystem fileSystem = MemoryFileSystem.test(); - final Directory artifactDir = fileSystem.systemTempDirectory.createTempSync( - 'flutter_cache_test_artifact.', - ); - final cache = FakeSecondaryCache()..artifactDirectory = artifactDir; - - final artifact = FakeCachedArtifact( - cache: cache, - binaryDirs: >[ - ['dir1', 'url1'], - ['dir2', 'url2'], - ], - packageDirs: ['pkg1', 'pkg2', 'pkg3'], - requiredArtifacts: DevelopmentArtifact.universal, - ); - - expect(artifact.downloadCount, 5); - }, - ); - - group('ArtifactUpdater progress formatting', () { - late FileSystem fileSystem; - late Directory tempStorage; - late ArtifactUpdater artifactUpdater; - - setUp(() { - fileSystem = MemoryFileSystem.test(); - tempStorage = fileSystem.systemTempDirectory.createTempSync('temp.'); - artifactUpdater = ArtifactUpdater( - operatingSystemUtils: FakeOperatingSystemUtils(), - logger: BufferLogger.test(), - fileSystem: fileSystem, - tempStorage: tempStorage, - httpClient: FakeHttpClient.any(), - platform: FakePlatform(), - allowedBaseUrls: ['https://storage.googleapis.com'], - ); - }); - - testWithoutContext('formats single download with artifact index', () { - artifactUpdater.setProgressContext(artifactIndex: 2, artifactTotal: 5, downloadTotal: 1); - - expect(artifactUpdater.formatProgressMessage('Test Artifact'), '[2/5] Test Artifact'); - }); - - testWithoutContext('formats multiple downloads with tree prefix', () { - // First download gets โ”œโ”€ prefix - artifactUpdater.setProgressContext(artifactIndex: 1, artifactTotal: 3, downloadTotal: 4); - expect(artifactUpdater.formatProgressMessage('first'), ' โ”œโ”€ [1/4] first'); - - // Middle downloads also get โ”œโ”€ prefix - artifactUpdater.setProgressContext( - artifactIndex: 1, - artifactTotal: 3, - downloadTotal: 4, - downloadIndex: 1, - ); - expect(artifactUpdater.formatProgressMessage('second'), ' โ”œโ”€ [2/4] second'); - - artifactUpdater.setProgressContext( - artifactIndex: 1, - artifactTotal: 3, - downloadTotal: 4, - downloadIndex: 2, - ); - expect(artifactUpdater.formatProgressMessage('third'), ' โ”œโ”€ [3/4] third'); - - // Last download gets โ””โ”€ prefix - artifactUpdater.setProgressContext( - artifactIndex: 1, - artifactTotal: 3, - downloadTotal: 4, - downloadIndex: 3, - ); - expect(artifactUpdater.formatProgressMessage('fourth'), ' โ””โ”€ [4/4] fourth'); - }); - - testWithoutContext('resetProgressContext resets download index', () { - artifactUpdater.setProgressContext(artifactIndex: 2, artifactTotal: 5, downloadTotal: 1); - expect(artifactUpdater.formatProgressMessage('first'), '[2/5] first'); - - artifactUpdater.resetProgressContext(); - artifactUpdater.setProgressContext(artifactIndex: 1, artifactTotal: 3, downloadTotal: 1); - - // After reset, index should start fresh - expect(artifactUpdater.formatProgressMessage('new'), '[1/3] new'); - }); - }); - - group('DownloadProgress', () { - testWithoutContext('fraction and percent with known total', () { - final progress = DownloadProgress()..totalBytes = 1000; - progress.addBytesReceived(250); - - expect(progress.fractionReceived, 0.25); - expect(progress.percentReceived, 25); - }); - - testWithoutContext('fraction and percent with unknown total', () { - final progress = DownloadProgress(); - progress.addBytesReceived(500); - - expect(progress.fractionReceived, 0.0); - expect(progress.percentReceived, 0); - expect(progress.hasKnownSize, false); - }); - - testWithoutContext('fraction clamps to 1.0 when bytesReceived exceeds total', () { - final progress = DownloadProgress()..totalBytes = 100; - progress.addBytesReceived(150); - - expect(progress.fractionReceived, 1.0); - expect(progress.percentReceived, 100); - }); - - testWithoutContext('speed calculation', () { - final progress = DownloadProgress(); - progress.addBytesReceived(5000000); - const elapsed = Duration(seconds: 2); - - expect(progress.speedBytesPerSecond(elapsed), 2500000.0); - }); - - testWithoutContext('speed is zero when elapsed is zero', () { - final progress = DownloadProgress(); - progress.addBytesReceived(5000000); - - expect(progress.speedBytesPerSecond(Duration.zero), 0.0); - }); - - testWithoutContext('timeRemaining with known total', () { - final progress = DownloadProgress()..totalBytes = 10000000; - progress.addBytesReceived(5000000); - const elapsed = Duration(seconds: 2); - - // 5MB received in 2s = 2.5MB/s, 5MB timeRemaining = 2s timeRemaining - final Duration? rem = progress.timeRemaining(elapsed); - expect(rem, isNotNull); - expect(rem!.inSeconds, 2); - }); - - testWithoutContext('timeRemaining is null with unknown total', () { - final progress = DownloadProgress(); - progress.addBytesReceived(5000000); - - expect(progress.timeRemaining(const Duration(seconds: 2)), isNull); - }); - - testWithoutContext('timeRemaining is null when speed is zero', () { - final progress = DownloadProgress()..totalBytes = 10000000; - progress.addBytesReceived(5000000); - - expect(progress.timeRemaining(Duration.zero), isNull); - }); - - testWithoutContext('renderProgressBar at 50%', () { - final progress = DownloadProgress()..totalBytes = 100; - progress.addBytesReceived(50); - - final String bar = progress.renderProgressBar(20); - expect(bar.length, 20); - expect(bar, '${'โ–ˆ' * 10}${' ' * 10}'); - }); - - testWithoutContext('renderProgressBar uses sub-character partial block', () { - // 46% of 20 cells: totalEighths = round(0.46 * 20 * 8) = 74 - // fullBlocks=9, remainder=2 โ†’ partial='โ–Ž', emptyBlocks=10 - final progress = DownloadProgress()..totalBytes = 100; - progress.addBytesReceived(46); - - final String bar = progress.renderProgressBar(20); - expect(bar.length, 20); - expect(bar, '${'โ–ˆ' * 9}โ–Ž${' ' * 10}'); - }); - - testWithoutContext('renderProgressBar returns empty with unknown total', () { - final progress = DownloadProgress(); - progress.addBytesReceived(50); - - expect(progress.renderProgressBar(20), ''); - }); - - testWithoutContext('renderProgressBar returns empty with zero width', () { - final progress = DownloadProgress()..totalBytes = 100; - progress.addBytesReceived(50); - - expect(progress.renderProgressBar(0), ''); - }); - - testWithoutContext('formatBytes with known total', () { - final progress = DownloadProgress()..totalBytes = 10000000; - progress.addBytesReceived(5000000); - - expect(progress.formatBytes(), matches(r'^\d+\.\d+MB/\d+\.\d+MB$')); - }); - - testWithoutContext('formatBytes with unknown total', () { - final progress = DownloadProgress(); - progress.addBytesReceived(5000000); - - expect(progress.formatBytes(), matches(r'^\d+\.\d+MB$')); - }); - - testWithoutContext('formatSpeed', () { - final progress = DownloadProgress(); - progress.addBytesReceived(2000000); - - expect(progress.formatSpeed(const Duration(seconds: 1)), matches(r'^\d+\.\d+MB/s$')); - }); - - testWithoutContext('formatRemaining with known total and speed', () { - final progress = DownloadProgress()..totalBytes = 10000000; - progress.addBytesReceived(5000000); - - final String eta = progress.formatRemaining(const Duration(seconds: 2)); - expect(eta, startsWith('ETA ')); - expect(eta, contains('s')); - }); - - testWithoutContext('formatRemaining is empty with unknown total', () { - final progress = DownloadProgress(); - progress.addBytesReceived(5000000); - - expect(progress.formatRemaining(const Duration(seconds: 2)), ''); - }); - - testWithoutContext('formatProgressLine includes progress bar with known total', () { - final progress = DownloadProgress()..totalBytes = 10000000; - progress.addBytesReceived(5000000); - - final String line = progress.formatProgressLine( - elapsed: const Duration(seconds: 2), - terminalWidth: 80, - ); - - expect(line, contains('50%')); - expect(line, matches(r'\d+\.\d+MB/\d+\.\d+MB')); - expect(line, contains('/s')); - expect(line, contains('โ–ˆ')); - expect(line, contains('โ–')); - expect(line, contains('โ–•')); - }); - - testWithoutContext('formatProgressLine has fixed bar width and right-aligns info', () { - final progress = DownloadProgress()..totalBytes = 10000000; - progress.addBytesReceived(5000000); - - final String line = progress.formatProgressLine( - elapsed: const Duration(seconds: 2), - terminalWidth: 80, - ); - - // Bar is fixed at 28 inner chars + 2 brackets = 30 visual chars. - // Line total must equal terminalWidth. - expect(line.length, 80); - // Info is right-aligned: the line must end with the info, no trailing spaces. - expect(line, endsWith('ETA 2.0s')); - }); - - testWithoutContext('formatProgressLine omits bar with unknown total', () { - final progress = DownloadProgress(); - progress.addBytesReceived(5000000); - - final String line = progress.formatProgressLine( - elapsed: const Duration(seconds: 2), - terminalWidth: 80, - ); - - expect(line, isNot(contains('โ–ˆ'))); - expect(line, matches(r'\d+\.\d+MB')); - expect(line, contains('/s')); - }); - - testWithoutContext('formatProgressLine omits bar when terminal too narrow', () { - final progress = DownloadProgress()..totalBytes = 10000000; - progress.addBytesReceived(5000000); - - final String line = progress.formatProgressLine( - elapsed: const Duration(seconds: 2), - terminalWidth: 30, - ); - - expect(line, isNot(contains('โ–ˆ'))); - expect(line, contains('50%')); - }); - - testWithoutContext('formatProgressLine never exceeds terminalWidth', () { - final progress = DownloadProgress()..totalBytes = 10000000; - progress.addBytesReceived(5000000); - - final String line = progress.formatProgressLine( - elapsed: const Duration(seconds: 2), - terminalWidth: 30, - ); - - expect(line.length, lessThanOrEqualTo(30)); - }); - - testWithoutContext( - 'formatProgressLine does not have trailing whitespace with unknown total', - () { - final progress = DownloadProgress(); - progress.addBytesReceived(5000000); - - final String line = progress.formatProgressLine( - elapsed: const Duration(seconds: 2), - terminalWidth: 80, - ); - - expect(line, isNot(endsWith(' '))); - }, - ); - - testWithoutContext('formatCompletionSummary', () { - final progress = DownloadProgress(); - progress.addBytesReceived(21100000); - - expect( - progress.formatCompletionSummary(const Duration(seconds: 5)), - matches(r'^\(\d+\.\d+MB in \d+\.\d+s\)$'), - ); - }); - }); - - group('ArtifactUpdater download progress display', () { - late FileSystem fileSystem; - late Directory tempStorage; - late FakeStdio stdio; - late BufferLogger logger; - - setUp(() { - fileSystem = MemoryFileSystem.test(); - tempStorage = fileSystem.systemTempDirectory.createTempSync('temp.'); - stdio = FakeStdio(); - logger = BufferLogger.test(terminal: Terminal.test(supportsColor: true)); - }); - - testWithoutContext( - 'shows ANSI progress when stdio is provided and color is supported', - () async { - final body = List.filled(1000, 0); - final updater = ArtifactUpdater( - operatingSystemUtils: FakeOperatingSystemUtils(), - logger: logger, - fileSystem: fileSystem, - tempStorage: tempStorage, - httpClient: FakeHttpClient.list([ - FakeRequest( - Uri.parse('https://storage.googleapis.com/test-artifact.zip'), - response: FakeResponse(body: body), - ), - ]), - platform: FakePlatform(), - allowedBaseUrls: ['https://storage.googleapis.com'], - stdio: stdio, - ); - - updater.setProgressContext(artifactIndex: 1, artifactTotal: 1, downloadTotal: 1); - - await updater.downloadZipArchive( - 'Test Artifact', - Uri.parse('https://storage.googleapis.com/test-artifact.zip'), - fileSystem.directory('output')..createSync(), - ); - - final String allOutput = stdio.writtenToStdout.join(); - - // Should have written the status message followed by newline - expect(allOutput, contains('[1/1] Test Artifact')); - - // Should contain completion summary with size and time - expect(allOutput, contains('MB in')); - expect(allOutput, contains('s)')); - - // Should contain ANSI clear codes for cleanup - expect(allOutput, contains('\x1B[K')); // clear to end of line - expect(allOutput, contains('\x1B[1A')); // cursor up - - // Completion line should be right-aligned to terminal width (80). - final String completionLine = stdio.writtenToStdout.last.replaceAll('\n', ''); - expect(completionLine.length, 80); - }, - ); - - testWithoutContext('falls back to logger progress when stdio is not provided', () async { - final updater = ArtifactUpdater( - operatingSystemUtils: FakeOperatingSystemUtils(), - logger: logger, - fileSystem: fileSystem, - tempStorage: tempStorage, - httpClient: FakeHttpClient.list([ - FakeRequest( - Uri.parse('https://storage.googleapis.com/test-artifact.zip'), - response: const FakeResponse(body: [0, 0, 0]), - ), - ]), - platform: FakePlatform(), - allowedBaseUrls: ['https://storage.googleapis.com'], - ); - - updater.setProgressContext(artifactIndex: 1, artifactTotal: 1, downloadTotal: 1); - - await updater.downloadZipArchive( - 'Test Artifact', - Uri.parse('https://storage.googleapis.com/test-artifact.zip'), - fileSystem.directory('output')..createSync(), - ); - - // Without stdio, nothing should be written to stdout directly - expect(stdio.writtenToStdout, isEmpty); - }); - - testWithoutContext('falls back to logger progress when color is not supported', () async { - final noColorLogger = BufferLogger.test(); - - final updater = ArtifactUpdater( - operatingSystemUtils: FakeOperatingSystemUtils(), - logger: noColorLogger, - fileSystem: fileSystem, - tempStorage: tempStorage, - httpClient: FakeHttpClient.list([ - FakeRequest( - Uri.parse('https://storage.googleapis.com/test-artifact.zip'), - response: const FakeResponse(body: [0, 0, 0]), - ), - ]), - platform: FakePlatform(), - allowedBaseUrls: ['https://storage.googleapis.com'], - stdio: stdio, - ); - - updater.setProgressContext(artifactIndex: 1, artifactTotal: 1, downloadTotal: 1); - - await updater.downloadZipArchive( - 'Test Artifact', - Uri.parse('https://storage.googleapis.com/test-artifact.zip'), - fileSystem.directory('output')..createSync(), - ); - - // With stdio but no color support, should not write progress to stdout - expect(stdio.writtenToStdout, isEmpty); - }); - }); - testWithoutContext( 'EngineCachedArtifact makes binary dirs readable and executable by all', () async { @@ -1348,7 +884,7 @@ void main() { expect(messages, ['Web SDK']); expect(downloads, [ - 'https://storage.googleapis.com/flutter_infra_release/flutter/hijklmnop/flutter-web-sdk.zip', + 'https://download.shorebird.dev/flutter_infra_release/flutter/hijklmnop/flutter-web-sdk.zip', ]); expect(locations, ['/bin/cache/flutter_web_sdk']); @@ -1469,7 +1005,7 @@ void main() { expect(messages, ['Engine Information']); expect(downloads, [ - 'https://storage.googleapis.com/flutter_infra_release/flutter/hijklmnop/engine_stamp.json', + 'https://download.shorebird.dev/flutter_infra_release/flutter/hijklmnop/engine_stamp.json', ]); expect(locations, ['/bin/cache']); // file copy is done by the real uploader; not the fake. @@ -1902,33 +1438,22 @@ class FakeArtifactUpdater extends Fake implements ArtifactUpdater { void Function(String, Uri, Directory)? onDownloadFile; @override - Future downloadZippedTarball(String artifactName, Uri url, Directory location) async { - onDownloadZipTarball?.call(artifactName, url, location); + Future downloadZippedTarball(String message, Uri url, Directory location) async { + onDownloadZipTarball?.call(message, url, location); } @override - Future downloadZipArchive(String artifactName, Uri url, Directory location) async { - onDownloadZipArchive?.call(artifactName, url, location); + Future downloadZipArchive(String message, Uri url, Directory location) async { + onDownloadZipArchive?.call(message, url, location); } @override - Future downloadFile(String artifactName, Uri url, Directory location) async { - onDownloadFile?.call(artifactName, url, location); + Future downloadFile(String message, Uri url, Directory location) async { + onDownloadFile?.call(message, url, location); } @override void removeDownloadedFiles() {} - - @override - void setProgressContext({ - required int artifactIndex, - required int artifactTotal, - required int downloadTotal, - int downloadIndex = 0, - }) {} - - @override - void resetProgressContext() {} } class FakeArtifactUpdaterDownload extends ArtifactUpdater { diff --git a/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart b/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart index 3ff24d612ee30..dfaf60097552c 100644 --- a/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart @@ -41,6 +41,13 @@ class LocalFileSystemFake extends Fake implements LocalFileSystem { @override File file(dynamic path) => memoryFileSystem.file(path); + @override + Link link(dynamic path) => memoryFileSystem.link(path); + + @override + FileSystemEntityType typeSync(String path, {bool followLinks = true}) => + memoryFileSystem.typeSync(path, followLinks: followLinks); + @override Context get path => memoryFileSystem.path; @@ -78,7 +85,6 @@ void main() { final processUtils = ProcessUtils(processManager: processManager, logger: logger); final fakeLLDB = FakeLLDB(); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: fakeCoreDeviceControl, logger: logger, xcodeDebug: FakeXcodeDebug(), @@ -105,7 +111,6 @@ void main() { final logger = BufferLogger.test(); final processUtils = ProcessUtils(processManager: processManager, logger: logger); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: fakeCoreDeviceControl, logger: logger, xcodeDebug: FakeXcodeDebug(), @@ -134,7 +139,6 @@ void main() { final logger = BufferLogger.test(); final processUtils = ProcessUtils(processManager: processManager, logger: logger); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: fakeCoreDeviceControl, logger: logger, xcodeDebug: FakeXcodeDebug(), @@ -159,7 +163,6 @@ void main() { final logger = BufferLogger.test(); final processUtils = ProcessUtils(processManager: processManager, logger: logger); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: fakeCoreDeviceControl, logger: logger, xcodeDebug: FakeXcodeDebug(), @@ -207,7 +210,6 @@ void main() { final processUtils = ProcessUtils(processManager: processManager, logger: logger); final fakeLLDB = FakeLLDB(); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: fakeCoreDeviceControl, logger: logger, xcodeDebug: FakeXcodeDebug(), @@ -222,6 +224,7 @@ void main() { bundleId: 'bundle-id', launchArguments: [], shutdownHooks: FakeShutdownHooks(), + mode: BuildMode.debug, ); expect(result, isTrue); @@ -266,7 +269,6 @@ void main() { final processUtils = ProcessUtils(processManager: processManager, logger: logger); final fakeLLDB = FakeLLDB(); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: fakeCoreDeviceControl, logger: logger, xcodeDebug: FakeXcodeDebug(), @@ -281,6 +283,7 @@ void main() { bundleId: 'bundle-id', launchArguments: [], shutdownHooks: FakeShutdownHooks(), + mode: BuildMode.debug, ); expect(result, isTrue); @@ -318,7 +321,6 @@ void main() { final processUtils = ProcessUtils(processManager: processManager, logger: logger); final fakeLLDB = FakeLLDB(); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: fakeCoreDeviceControl, logger: logger, xcodeDebug: FakeXcodeDebug(), @@ -333,6 +335,7 @@ void main() { bundleId: 'bundle-id', launchArguments: [], shutdownHooks: FakeShutdownHooks(), + mode: BuildMode.debug, ); expect(result, isFalse); @@ -363,7 +366,6 @@ void main() { final processUtils = ProcessUtils(processManager: processManager, logger: logger); final fakeLLDB = FakeLLDB(); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: fakeCoreDeviceControl, logger: logger, xcodeDebug: FakeXcodeDebug(), @@ -378,6 +380,7 @@ void main() { bundleId: 'bundle-id', launchArguments: [], shutdownHooks: FakeShutdownHooks(), + mode: BuildMode.debug, ); expect(result, isFalse); @@ -414,7 +417,6 @@ void main() { final processUtils = ProcessUtils(processManager: processManager, logger: logger); final fakeLLDB = FakeLLDB(); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: fakeCoreDeviceControl, logger: logger, xcodeDebug: FakeXcodeDebug(), @@ -429,6 +431,7 @@ void main() { bundleId: 'bundle-id', launchArguments: [], shutdownHooks: FakeShutdownHooks(), + mode: BuildMode.debug, ); expect(result, isFalse); @@ -459,7 +462,6 @@ void main() { final processUtils = ProcessUtils(processManager: processManager, logger: logger); final fakeLLDB = FakeLLDB(); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: fakeCoreDeviceControl, logger: logger, xcodeDebug: FakeXcodeDebug(), @@ -474,6 +476,7 @@ void main() { bundleId: 'bundle-id', launchArguments: [], shutdownHooks: FakeShutdownHooks(), + mode: BuildMode.debug, ); expect(result, isFalse); @@ -506,7 +509,6 @@ void main() { final processUtils = ProcessUtils(processManager: processManager, logger: logger); final fakeLLDB = FakeLLDB(); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: fakeCoreDeviceControl, logger: logger, xcodeDebug: FakeXcodeDebug(), @@ -521,6 +523,7 @@ void main() { bundleId: 'bundle-id', launchArguments: [], shutdownHooks: FakeShutdownHooks(), + mode: BuildMode.debug, ); expect(result, isFalse); @@ -555,7 +558,6 @@ void main() { final processUtils = ProcessUtils(processManager: processManager, logger: logger); final fakeLLDB = FakeLLDB(attachSuccess: false); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: fakeCoreDeviceControl, logger: logger, xcodeDebug: FakeXcodeDebug(), @@ -570,6 +572,7 @@ void main() { bundleId: 'bundle-id', launchArguments: [], shutdownHooks: FakeShutdownHooks(), + mode: BuildMode.debug, ); expect(result, isFalse); @@ -591,7 +594,6 @@ void main() { ); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: FakeIOSCoreDeviceControl(), logger: logger, xcodeDebug: fakeXcodeDebug, @@ -625,7 +627,6 @@ void main() { ); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: FakeIOSCoreDeviceControl(), logger: logger, xcodeDebug: fakeXcodeDebug, @@ -659,7 +660,6 @@ void main() { ); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: FakeIOSCoreDeviceControl(), logger: logger, xcodeDebug: fakeXcodeDebug, @@ -693,7 +693,6 @@ void main() { ); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: FakeIOSCoreDeviceControl(), logger: logger, xcodeDebug: fakeXcodeDebug, @@ -726,7 +725,6 @@ void main() { final processUtils = ProcessUtils(processManager: processManager, logger: logger); final fakeLLDB = FakeLLDB(); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: fakeCoreDeviceControl, logger: logger, xcodeDebug: xcodeDebug, @@ -756,7 +754,6 @@ void main() { final processUtils = ProcessUtils(processManager: processManager, logger: logger); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: fakeCoreDeviceControl, logger: logger, xcodeDebug: xcodeDebug, @@ -785,7 +782,6 @@ void main() { final processUtils = ProcessUtils(processManager: processManager, logger: logger); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: fakeCoreDeviceControl, logger: logger, xcodeDebug: xcodeDebug, @@ -813,7 +809,6 @@ void main() { final processUtils = ProcessUtils(processManager: processManager, logger: logger); final launcher = IOSCoreDeviceLauncher( - xcode: FakeXcode(), coreDeviceControl: fakeCoreDeviceControl, logger: logger, xcodeDebug: xcodeDebug, @@ -4040,6 +4035,7 @@ class FakeLLDB extends Fake implements LLDB { required String deviceId, required int appProcessId, required LLDBLogForwarder lldbLogForwarder, + required BuildMode mode, }) async { attemptedToAttach = true; attachedProcessId = appProcessId; @@ -4157,10 +4153,3 @@ class FakeShutdownHooks extends Fake implements ShutdownHooks { _isShuttingDown = true; } } - -class FakeXcode extends Fake implements Xcode { - FakeXcode({Version? currentVersion}) : currentVersion = currentVersion ?? Version(15, 0, 0); - - @override - final Version currentVersion; -} diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart index 52a944293298f..9d9bcea57ef7d 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart @@ -1554,24 +1554,24 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete List xcrunCommand() => ['xcrun']; @override - Future> xcodebuildProjectCommand( - String projectPath, + Future> fetchDependenciesAndGenerateXcodebuildArgs( + XcodeBasedProject xcodeProject, Directory buildDirectory, { - bool skipPackageResolution = true, + bool skipPackageUpdatesAndValidation = true, }) async { return ['xcrun', 'xcodebuild']; } @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async => projectInfo; @override Future> getBuildSettings( - String projectPath, { + XcodeBasedProject xcodeProject, { required XcodeProjectBuildContext buildContext, Duration timeout = const Duration(minutes: 1), }) async => buildSettings; @@ -1747,6 +1747,7 @@ class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher { required String bundlePath, required String bundleId, required List launchArguments, + required BuildMode mode, required ShutdownHooks shutdownHooks, }) async { return true; diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart index 1af8521986b33..4a48846c0e785 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart @@ -1871,6 +1871,7 @@ class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher { required String bundlePath, required String bundleId, required List launchArguments, + required BuildMode mode, required ShutdownHooks shutdownHooks, }) async { launchedWithLLDB = true; diff --git a/packages/flutter_tools/test/general.shard/ios/lldb_test.dart b/packages/flutter_tools/test/general.shard/ios/lldb_test.dart index 9cbcd86d3b9d3..971118c97d69e 100644 --- a/packages/flutter_tools/test/general.shard/ios/lldb_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/lldb_test.dart @@ -10,9 +10,8 @@ import 'package:fake_async/fake_async.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/process.dart'; -import 'package:flutter_tools/src/base/version.dart'; +import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/ios/lldb.dart'; -import 'package:flutter_tools/src/macos/xcode.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; @@ -38,12 +37,13 @@ void main() { final processManager = FakeLLDBProcessManager([lldbCommand]); final processUtils = ProcessUtils(processManager: processManager, logger: logger); - final lldb = LLDB(logger: logger, processUtils: processUtils, xcode: FakeXcode()); + final lldb = LLDB(logger: logger, processUtils: processUtils); final bool success = await lldb.attachAndStart( deviceId: deviceId, appProcessId: appProcessId, lldbLogForwarder: FakeLLDBLogForwarder(), + mode: BuildMode.debug, ); expect(success, isFalse); expect(lldb.isRunning, isFalse); @@ -82,16 +82,16 @@ void main() { final processManager = FakeLLDBProcessManager([lldbCommand]); final processUtils = ProcessUtils(processManager: processManager, logger: logger); - final lldb = LLDB(logger: logger, processUtils: processUtils, xcode: FakeXcode()); + final lldb = LLDB(logger: logger, processUtils: processUtils); - const breakPointMatcher = - r"breakpoint set --auto-continue true --func-regex '^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$'"; + const breakPointMatcher = r"breakpoint set --func-regex '^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$'"; const processAttachMatcher = 'device process attach --pid $appProcessId'; const processResumedMatcher = 'process continue'; final expectedInputs = [ 'device select $deviceId', breakPointMatcher, 'breakpoint command add --script-type python $breakpointId', + 'script lldb.debugger.SetAsync(False)', processAttachMatcher, processResumedMatcher, ]; @@ -121,7 +121,7 @@ Target 0: (Runner) stopped. ); } if (line == processResumedMatcher) { - processResumedCompleted.complete(utf8.encode('Process $appProcessId resuming\n')); + processResumedCompleted.complete(utf8.encode('1 location added to breakpoint 1\n')); } }); @@ -129,6 +129,7 @@ Target 0: (Runner) stopped. deviceId: deviceId, appProcessId: appProcessId, lldbLogForwarder: FakeLLDBLogForwarder(), + mode: BuildMode.debug, ); expect(success, isTrue); expect(lldb.isRunning, isTrue); @@ -138,21 +139,16 @@ Target 0: (Runner) stopped. expect(logger.errorText, isEmpty); }); - testWithoutContext('attachAndStart sets breakpoint for Xcode 26.4.0 and handles stop', () async { + testWithoutContext('attachAndStart returns true on success for profile mode', () async { const deviceId = '123'; const appProcessId = 5678; - const breakpointId = 123; - final breakPointCompleter = Completer>(); final processAttachCompleter = Completer>(); final processResumedCompleted = Completer>(); - final logAfterAttachCompleter = Completer>(); final stdoutStream = Stream>.fromFutures([ - breakPointCompleter.future, processAttachCompleter.future, processResumedCompleted.future, - logAfterAttachCompleter.future, ]); final stdinController = StreamController>(); @@ -170,33 +166,16 @@ Target 0: (Runner) stopped. final processManager = FakeLLDBProcessManager([lldbCommand]); final processUtils = ProcessUtils(processManager: processManager, logger: logger); - final lldb = LLDB( - logger: logger, - processUtils: processUtils, - xcode: FakeXcode(currentVersion: Version(26, 4, 0)), - ); + final lldb = LLDB(logger: logger, processUtils: processUtils); - const breakPointMatcher = r"breakpoint set --func-regex '^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$'"; const processAttachMatcher = 'device process attach --pid $appProcessId'; const processResumedMatcher = 'process continue'; - final expectedInputs = [ - 'device select $deviceId', - breakPointMatcher, - 'breakpoint command add --script-type python $breakpointId', - processAttachMatcher, - processResumedMatcher, - processResumedMatcher, // Expect second continue on breakpoint - ]; + final expectedInputs = ['device select $deviceId', processAttachMatcher, processResumedMatcher]; stdinController.stream.transform(utf8.decoder).transform(const LineSplitter()).listen(( String line, ) { expectedInputs.remove(line); - if (line == breakPointMatcher) { - breakPointCompleter.complete( - utf8.encode('Breakpoint $breakpointId: no locations (pending).\n'), - ); - } if (line == processAttachMatcher) { processAttachCompleter.complete( utf8.encode(''' @@ -213,36 +192,19 @@ Target 0: (Runner) stopped. ); } if (line == processResumedMatcher) { - if (!processResumedCompleted.isCompleted) { - processResumedCompleted.complete(utf8.encode('Process $appProcessId resuming\n')); - } + processResumedCompleted.complete(utf8.encode('Process 568 resuming\n')); } }); - final lldbLogForwarder = FakeLLDBLogForwarder(); - final bool success = await lldb.attachAndStart( deviceId: deviceId, appProcessId: appProcessId, - lldbLogForwarder: lldbLogForwarder, + lldbLogForwarder: FakeLLDBLogForwarder(), + mode: BuildMode.profile, ); expect(success, isTrue); expect(lldb.isRunning, isTrue); expect(lldb.appProcessId, appProcessId); - - // Simulate breakpoint hit - logAfterAttachCompleter.complete( - utf8.encode(''' -Process 568 stopped -* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 -frame #0: ... -Process 568 resuming -'''), - ); - - // Wait for the stream to be processed and inputs to be removed - await Future.delayed(const Duration(milliseconds: 100)); - expect(expectedInputs, isEmpty); expect(processManager.hasRemainingExpectations, isFalse); expect(logger.errorText, isEmpty); @@ -274,10 +236,9 @@ Process 568 resuming final processManager = FakeLLDBProcessManager([lldbCommand]); final processUtils = ProcessUtils(processManager: processManager, logger: logger); - final lldb = LLDB(logger: logger, processUtils: processUtils, xcode: FakeXcode()); + final lldb = LLDB(logger: logger, processUtils: processUtils); - const breakPointMatcher = - r"breakpoint set --auto-continue true --func-regex '^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$'"; + const breakPointMatcher = r"breakpoint set --func-regex '^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$'"; final expectedInputs = ['device select $deviceId', breakPointMatcher]; const errorText = "error: 'device' is not a valid command.\n"; @@ -294,6 +255,7 @@ Process 568 resuming deviceId: deviceId, appProcessId: appProcessId, lldbLogForwarder: FakeLLDBLogForwarder(), + mode: BuildMode.debug, ); expect(success, isFalse); expect(lldb.isRunning, isFalse); @@ -329,7 +291,7 @@ Process 568 resuming final processManager = FakeLLDBProcessManager([lldbCommand]); final processUtils = ProcessUtils(processManager: processManager, logger: logger); - final lldb = LLDB(logger: logger, processUtils: processUtils, xcode: FakeXcode()); + final lldb = LLDB(logger: logger, processUtils: processUtils); final expectedInputs = ['device select $deviceId']; const errorText = "error: 'device' is not a valid command.\n"; @@ -344,6 +306,7 @@ Process 568 resuming deviceId: deviceId, appProcessId: appProcessId, lldbLogForwarder: FakeLLDBLogForwarder(), + mode: BuildMode.debug, ); expect(success, isFalse); expect(lldb.isRunning, isFalse); @@ -372,7 +335,7 @@ Process 568 resuming final processManager = FakeLLDBProcessManager([lldbCommand]); final processUtils = ProcessUtils(processManager: processManager, logger: logger); - final lldb = LLDB(logger: logger, processUtils: processUtils, xcode: FakeXcode()); + final lldb = LLDB(logger: logger, processUtils: processUtils); final completer = Completer(); @@ -389,6 +352,7 @@ Process 568 resuming deviceId: deviceId, appProcessId: appProcessId, lldbLogForwarder: FakeLLDBLogForwarder(), + mode: BuildMode.debug, ); time.elapse(const Duration(minutes: 2)); time.flushMicrotasks(); @@ -433,16 +397,16 @@ Process 568 resuming final processManager = FakeLLDBProcessManager([lldbCommand]); final processUtils = ProcessUtils(processManager: processManager, logger: logger); - final lldb = LLDB(logger: logger, processUtils: processUtils, xcode: FakeXcode()); + final lldb = LLDB(logger: logger, processUtils: processUtils); - const breakPointMatcher = - r"breakpoint set --auto-continue true --func-regex '^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$'"; + const breakPointMatcher = r"breakpoint set --func-regex '^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$'"; const processAttachMatcher = 'device process attach --pid $appProcessId'; const processResumedMatcher = 'process continue'; final expectedInputs = [ 'device select $deviceId', breakPointMatcher, 'breakpoint command add --script-type python $breakpointId', + 'script lldb.debugger.SetAsync(False)', processAttachMatcher, processResumedMatcher, ]; @@ -472,7 +436,7 @@ Target 0: (Runner) stopped. ); } if (line == processResumedMatcher) { - processResumedCompleted.complete(utf8.encode('Process $appProcessId resuming\n')); + processResumedCompleted.complete(utf8.encode('1 location added to breakpoint 1\n')); } }); @@ -484,6 +448,7 @@ Target 0: (Runner) stopped. deviceId: deviceId, appProcessId: appProcessId, lldbLogForwarder: lldbLogForwarder, + mode: BuildMode.debug, ); logAfterAttachCompleter.complete(utf8.encode('$ignoreLog\n$expectedForwardedLog\n')); @@ -499,238 +464,6 @@ Target 0: (Runner) stopped. expect(lldbLogForwarder.logs, contains(expectedForwardedLog)); }); - testWithoutContext('attachAndStart suppresses logs between breakpoint stop and resume', () async { - const deviceId = '123'; - const appProcessId = 5678; - const breakpointId = 123; - - final breakPointCompleter = Completer>(); - final processAttachCompleter = Completer>(); - final processResumedCompleted = Completer>(); - final logAfterAttachCompleter = Completer>(); - - final stdoutStream = Stream>.fromFutures([ - breakPointCompleter.future, - processAttachCompleter.future, - processResumedCompleted.future, - logAfterAttachCompleter.future, - ]); - - final stdinController = StreamController>(); - - final processCompleter = Completer(); - final lldbCommand = FakeLLDBCommand( - command: const ['lldb'], - completer: processCompleter, - stdin: io.IOSink(stdinController.sink), - stdout: stdoutStream, - stderr: const Stream.empty(), - ); - - final logger = BufferLogger.test(); - - final processManager = FakeLLDBProcessManager([lldbCommand]); - final processUtils = ProcessUtils(processManager: processManager, logger: logger); - final lldb = LLDB(logger: logger, processUtils: processUtils, xcode: FakeXcode()); - - const breakPointMatcher = - r"breakpoint set --auto-continue true --func-regex '^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$'"; - const processAttachMatcher = 'device process attach --pid $appProcessId'; - const processResumedMatcher = 'process continue'; - final expectedInputs = [ - 'device select $deviceId', - breakPointMatcher, - 'breakpoint command add --script-type python $breakpointId', - processAttachMatcher, - processResumedMatcher, - ]; - - stdinController.stream.transform(utf8.decoder).transform(const LineSplitter()).listen(( - String line, - ) { - expectedInputs.remove(line); - if (line == breakPointMatcher) { - breakPointCompleter.complete( - utf8.encode('Breakpoint $breakpointId: no locations (pending).\n'), - ); - } - if (line == processAttachMatcher) { - processAttachCompleter.complete( - utf8.encode(''' -Process 568 stopped -* thread #1, stop reason = signal SIGSTOP - frame #0: 0x0000000102c7b240 dyld`_dyld_start -dyld`_dyld_start: --> 0x102c7b240 <+0>: mov x0, sp - 0x102c7b244 <+4>: and sp, x0, #0xfffffffffffffff0 - 0x102c7b248 <+8>: mov x29, #0x0 ; =0 - 0x102c7b24c <+12>: mov x30, #0x0 ; =0 -Target 0: (Runner) stopped. -'''), - ); - } - if (line == processResumedMatcher) { - if (!processResumedCompleted.isCompleted) { - processResumedCompleted.complete(utf8.encode('Process $appProcessId resuming\n')); - } - } - }); - - const logBeforeBreakpoint = 'Log before breakpoint'; - const logDuringBreakpoint1 = 'Process 568 stopped'; - const logDuringBreakpoint2 = '* thread #1, stop reason = breakpoint 1.1'; - const logDuringBreakpoint3 = 'frame #0: ...'; - const logDuringBreakpoint4 = 'Process 568 resuming'; - const logAfterBreakpoint = 'Log after breakpoint'; - - final lldbLogForwarder = FakeLLDBLogForwarder(expectedLog: logAfterBreakpoint); - - final bool success = await lldb.attachAndStart( - deviceId: deviceId, - appProcessId: appProcessId, - lldbLogForwarder: lldbLogForwarder, - ); - - logAfterAttachCompleter.complete( - utf8.encode(''' -$logBeforeBreakpoint -$logDuringBreakpoint1 -$logDuringBreakpoint2 -$logDuringBreakpoint3 -$logDuringBreakpoint4 -$logAfterBreakpoint -'''), - ); - - await lldbLogForwarder.expectedLogCompleter.future; - - expect(success, isTrue); - expect(lldb.isRunning, isTrue); - expect(lldb.appProcessId, appProcessId); - expect(expectedInputs, isEmpty); - expect(processManager.hasRemainingExpectations, isFalse); - expect(logger.errorText, isEmpty); - expect(lldbLogForwarder.logs.length, 2); - expect(lldbLogForwarder.logs, contains(logBeforeBreakpoint)); - expect(lldbLogForwarder.logs, contains(logAfterBreakpoint)); - expect(lldbLogForwarder.logs, isNot(contains(logDuringBreakpoint1))); - expect(lldbLogForwarder.logs, isNot(contains(logDuringBreakpoint2))); - expect(lldbLogForwarder.logs, isNot(contains(logDuringBreakpoint3))); - expect(lldbLogForwarder.logs, isNot(contains(logDuringBreakpoint4))); - }); - - testWithoutContext('attachAndStart does not suppress logs if not a breakpoint stop', () async { - const deviceId = '123'; - const appProcessId = 5678; - const breakpointId = 123; - - final breakPointCompleter = Completer>(); - final processAttachCompleter = Completer>(); - final processResumedCompleted = Completer>(); - final logAfterAttachCompleter = Completer>(); - - final stdoutStream = Stream>.fromFutures([ - breakPointCompleter.future, - processAttachCompleter.future, - processResumedCompleted.future, - logAfterAttachCompleter.future, - ]); - - final stdinController = StreamController>(); - - final processCompleter = Completer(); - final lldbCommand = FakeLLDBCommand( - command: const ['lldb'], - completer: processCompleter, - stdin: io.IOSink(stdinController.sink), - stdout: stdoutStream, - stderr: const Stream.empty(), - ); - - final logger = BufferLogger.test(); - - final processManager = FakeLLDBProcessManager([lldbCommand]); - final processUtils = ProcessUtils(processManager: processManager, logger: logger); - final lldb = LLDB(logger: logger, processUtils: processUtils, xcode: FakeXcode()); - - const breakPointMatcher = - r"breakpoint set --auto-continue true --func-regex '^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$'"; - const processAttachMatcher = 'device process attach --pid $appProcessId'; - const processResumedMatcher = 'process continue'; - final expectedInputs = [ - 'device select $deviceId', - breakPointMatcher, - 'breakpoint command add --script-type python $breakpointId', - processAttachMatcher, - processResumedMatcher, - ]; - - stdinController.stream.transform(utf8.decoder).transform(const LineSplitter()).listen(( - String line, - ) { - expectedInputs.remove(line); - if (line == breakPointMatcher) { - breakPointCompleter.complete( - utf8.encode('Breakpoint $breakpointId: no locations (pending).\n'), - ); - } - if (line == processAttachMatcher) { - processAttachCompleter.complete( - utf8.encode(''' -Process 568 stopped -* thread #1, stop reason = signal SIGSTOP - frame #0: 0x0000000102c7b240 dyld`_dyld_start -dyld`_dyld_start: --> 0x102c7b240 <+0>: mov x0, sp - 0x102c7b244 <+4>: and sp, x0, #0xfffffffffffffff0 - 0x102c7b248 <+8>: mov x29, #0x0 ; =0 - 0x102c7b24c <+12>: mov x30, #0x0 ; =0 -Target 0: (Runner) stopped. -'''), - ); - } - if (line == processResumedMatcher) { - processResumedCompleted.complete(utf8.encode('Process $appProcessId resuming\n')); - } - }); - - const logBeforeStop = 'Log before stop'; - const logStop = 'Process 568 stopped'; - const logSignal = '* thread #1, stop reason = signal SIGSTOP'; - const logAfterStop = 'Log after stop'; - - final lldbLogForwarder = FakeLLDBLogForwarder(expectedLog: logAfterStop); - - final bool success = await lldb.attachAndStart( - deviceId: deviceId, - appProcessId: appProcessId, - lldbLogForwarder: lldbLogForwarder, - ); - - logAfterAttachCompleter.complete( - utf8.encode(''' -$logBeforeStop -$logStop -$logSignal -$logAfterStop -'''), - ); - - await lldbLogForwarder.expectedLogCompleter.future; - - expect(success, isTrue); - expect(lldb.isRunning, isTrue); - expect(lldb.appProcessId, appProcessId); - expect(expectedInputs, isEmpty); - expect(processManager.hasRemainingExpectations, isFalse); - expect(logger.errorText, isEmpty); - expect(lldbLogForwarder.logs.length, 4); - expect(lldbLogForwarder.logs, contains(logBeforeStop)); - expect(lldbLogForwarder.logs, contains(logStop)); - expect(lldbLogForwarder.logs, contains(logSignal)); - expect(lldbLogForwarder.logs, contains(logAfterStop)); - }); - testWithoutContext('exit returns true and kills process', () async { const deviceId = '123'; const appProcessId = 5678; @@ -750,7 +483,7 @@ $logAfterStop final processManager = FakeLLDBProcessManager([lldbCommand]); final processUtils = ProcessUtils(processManager: processManager, logger: logger); - final lldb = LLDB(logger: logger, processUtils: processUtils, xcode: FakeXcode()); + final lldb = LLDB(logger: logger, processUtils: processUtils); final lldbStarted = Completer(); @@ -767,6 +500,7 @@ $logAfterStop deviceId: deviceId, appProcessId: appProcessId, lldbLogForwarder: FakeLLDBLogForwarder(), + mode: BuildMode.debug, ), ); @@ -784,7 +518,7 @@ $logAfterStop final processManager = FakeLLDBProcessManager([]); final processUtils = ProcessUtils(processManager: processManager, logger: logger); - final lldb = LLDB(logger: logger, processUtils: processUtils, xcode: FakeXcode()); + final lldb = LLDB(logger: logger, processUtils: processUtils); expect(lldb.isRunning, isFalse); final bool exitStatus = lldb.exit(); expect(exitStatus, isTrue); @@ -1024,10 +758,3 @@ class FakeLLDBLogForwarder extends Fake implements LLDBLogForwarder { } } } - -class FakeXcode extends Fake implements Xcode { - FakeXcode({Version? currentVersion}) : currentVersion = currentVersion ?? Version(15, 0, 0); - - @override - final Version currentVersion; -} diff --git a/packages/flutter_tools/test/general.shard/ios/mac_test.dart b/packages/flutter_tools/test/general.shard/ios/mac_test.dart index a16b28f643e3e..d84ab794d542c 100644 --- a/packages/flutter_tools/test/general.shard/ios/mac_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/mac_test.dart @@ -1225,7 +1225,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { @@ -1238,10 +1238,10 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete } @override - Future> xcodebuildProjectCommand( - String projectPath, + Future> fetchDependenciesAndGenerateXcodebuildArgs( + XcodeBasedProject xcodeProject, Directory buildDirectory, { - bool skipPackageResolution = true, + bool skipPackageUpdatesAndValidation = true, }) async { return ['xcrun', 'xcodebuild']; } diff --git a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart index 013c0e6e96c45..cf7c9a16ec25b 100644 --- a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart @@ -17,6 +17,7 @@ import 'package:flutter_tools/src/ios/xcode_build_settings.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/macos/xcode.dart'; import 'package:flutter_tools/src/project.dart'; +import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; @@ -56,10 +57,6 @@ void main() { '-resolvePackageDependencies', ]; - const kFindProcessResolvePackagesCommand = FakeCommand( - command: ['pgrep', '-n', ...kResolvePackagesCommandList], - ); - const kResolvePackagesCommand = FakeCommand(command: kResolvePackagesCommandList); late FakeProcessManager fakeProcessManager; @@ -274,7 +271,6 @@ void main() { fakeProcessManager.addCommands([ kWhichSysctlCommand, const FakeCommand(command: ['sysctl', 'hw.optional.arm64'], exitCode: 1), - kFindProcessResolvePackagesCommand, kResolvePackagesCommand, FakeCommand( command: [ @@ -282,7 +278,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-disableAutomaticPackageResolution', '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', @@ -301,7 +296,7 @@ void main() { expect( await xcodeProjectInterpreter.getBuildSettings( - '', + FakeXcodeBasedProject('', fileSystem), buildContext: const XcodeProjectBuildContext(deviceId: '123', scheme: 'Free'), ), const {}, @@ -322,7 +317,6 @@ void main() { fakeProcessManager.addCommands([ kWhichSysctlCommand, kx64CheckCommand, - kFindProcessResolvePackagesCommand, kResolvePackagesCommand, FakeCommand( command: [ @@ -330,7 +324,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-disableAutomaticPackageResolution', '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', @@ -349,7 +342,7 @@ void main() { expect( await xcodeProjectInterpreter.getBuildSettings( - '', + FakeXcodeBasedProject('', fileSystem), buildContext: const XcodeProjectBuildContext(sdk: XcodeSdk.IPhoneSimulator), ), const {}, @@ -370,7 +363,6 @@ void main() { fakeProcessManager.addCommands([ kWhichSysctlCommand, kx64CheckCommand, - kFindProcessResolvePackagesCommand, kResolvePackagesCommand, FakeCommand( command: [ @@ -378,7 +370,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-disableAutomaticPackageResolution', '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', @@ -395,7 +386,7 @@ void main() { expect( await xcodeProjectInterpreter.getBuildSettings( - '', + FakeXcodeBasedProject('', fileSystem), buildContext: const XcodeProjectBuildContext(), ), const {}, @@ -418,7 +409,6 @@ void main() { fakeProcessManager.addCommands([ kWhichSysctlCommand, kx64CheckCommand, - kFindProcessResolvePackagesCommand, kResolvePackagesCommand, FakeCommand( command: [ @@ -426,7 +416,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-disableAutomaticPackageResolution', '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', @@ -445,7 +434,7 @@ void main() { ]); expect( await xcodeProjectInterpreter.getBuildSettings( - '', + FakeXcodeBasedProject('', fileSystem), buildContext: const XcodeProjectBuildContext(scheme: 'Free'), ), const {}, @@ -466,7 +455,6 @@ void main() { fakeProcessManager.addCommands([ kWhichSysctlCommand, kx64CheckCommand, - kFindProcessResolvePackagesCommand, kResolvePackagesCommand, FakeCommand( command: [ @@ -474,7 +462,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-disableAutomaticPackageResolution', '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', @@ -491,7 +478,7 @@ void main() { expect( await xcodeProjectInterpreter.getBuildSettings( - '', + FakeXcodeBasedProject('', fileSystem), buildContext: const XcodeProjectBuildContext(sdk: XcodeSdk.WatchOS), ), const {}, @@ -512,7 +499,6 @@ void main() { fakeProcessManager.addCommands([ kWhichSysctlCommand, kx64CheckCommand, - kFindProcessResolvePackagesCommand, kResolvePackagesCommand, FakeCommand( command: [ @@ -520,7 +506,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-disableAutomaticPackageResolution', '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', @@ -537,7 +522,7 @@ void main() { expect( await xcodeProjectInterpreter.getBuildSettings( - '', + FakeXcodeBasedProject('', fileSystem), buildContext: const XcodeProjectBuildContext(sdk: XcodeSdk.WatchSimulator), ), const {}, @@ -558,17 +543,6 @@ void main() { fakeProcessManager.addCommands([ kWhichSysctlCommand, kx64CheckCommand, - const FakeCommand( - command: [ - 'pgrep', - '-n', - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/build/macos/SourcePackages', - '-resolvePackageDependencies', - ], - ), const FakeCommand( command: [ 'xcrun', @@ -584,7 +558,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/macos/SourcePackages', - '-disableAutomaticPackageResolution', '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', @@ -601,7 +574,7 @@ void main() { expect( await xcodeProjectInterpreter.getBuildSettings( - '', + FakeXcodeBasedProject('', fileSystem), buildContext: const XcodeProjectBuildContext(sdk: XcodeSdk.MacOSX), ), const {}, @@ -624,7 +597,6 @@ void main() { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, kx64CheckCommand, - kFindProcessResolvePackagesCommand, kResolvePackagesCommand, FakeCommand( command: [ @@ -632,7 +604,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-disableAutomaticPackageResolution', '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', @@ -649,6 +620,7 @@ void main() { ]); await xcodeProjectInterpreter.cleanWorkspace( + FakeXcodeBasedProject('', fileSystem), 'workspace_path', 'Free', buildDirectory: buildDirectory, @@ -664,7 +636,6 @@ void main() { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, kx64CheckCommand, - kFindProcessResolvePackagesCommand, kResolvePackagesCommand, FakeCommand( command: [ @@ -672,7 +643,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-disableAutomaticPackageResolution', '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', @@ -690,7 +660,10 @@ void main() { ); expect( - await xcodeProjectInterpreter.getInfo(workingDirectory, buildDirectory: buildDirectory), + await xcodeProjectInterpreter.getInfo( + FakeXcodeBasedProject(workingDirectory, fileSystem), + buildDirectory: buildDirectory, + ), isNotNull, ); expect(fakeProcessManager, hasNoRemainingExpectations); @@ -707,7 +680,6 @@ void main() { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, kx64CheckCommand, - kFindProcessResolvePackagesCommand, kResolvePackagesCommand, FakeCommand( command: [ @@ -715,7 +687,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-disableAutomaticPackageResolution', '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', @@ -735,7 +706,10 @@ void main() { ); await expectLater( - () => xcodeProjectInterpreter.getInfo(workingDirectory, buildDirectory: buildDirectory), + () => xcodeProjectInterpreter.getInfo( + FakeXcodeBasedProject(workingDirectory, fileSystem), + buildDirectory: buildDirectory, + ), throwsToolExit(message: stderr), ); expect(fakeProcessManager, hasNoRemainingExpectations); @@ -752,7 +726,6 @@ void main() { fakeProcessManager.addCommands(const [ kWhichSysctlCommand, kx64CheckCommand, - kFindProcessResolvePackagesCommand, kResolvePackagesCommand, FakeCommand( command: [ @@ -760,7 +733,6 @@ void main() { 'xcodebuild', '-clonedSourcePackagesDirPath', '/build/ios/SourcePackages', - '-disableAutomaticPackageResolution', '-skipPackageUpdates', '-skipPackagePluginValidation', '-skipPackageSignatureValidation', @@ -780,7 +752,10 @@ void main() { ); await expectLater( - () => xcodeProjectInterpreter.getInfo(workingDirectory, buildDirectory: buildDirectory), + () => xcodeProjectInterpreter.getInfo( + FakeXcodeBasedProject(workingDirectory, fileSystem), + buildDirectory: buildDirectory, + ), throwsToolExit(message: stderr), ); expect(fakeProcessManager, hasNoRemainingExpectations); @@ -2423,339 +2398,27 @@ flutter: }, ); }); +} - testWithoutContext( - 'prefetchSwiftPackages kills previously running process before starting new one', - () async { - final fs = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); - final platform = FakePlatform(operatingSystem: 'macos'); - const projectPath = 'path/to/project'; - final Directory buildDirectory = fileSystem.directory('$projectPath/build/ios'); - final fakeProcessManager = FakeProcessManager.empty(); - fakeProcessManager.addCommands([ - kWhichSysctlCommand, - kx64CheckCommand, - FakeCommand( - command: [ - 'pgrep', - '-n', - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/${buildDirectory.path}/SourcePackages', - '-resolvePackageDependencies', - ], - stdout: '12345', - ), - const FakeCommand(command: ['kill', '12345']), - FakeCommand( - command: [ - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/${buildDirectory.path}/SourcePackages', - '-resolvePackageDependencies', - ], - stdout: ''' -Resolve Package Graph - -Fetching from https://github.com/apple/swift-algorithms.git (cached) - -Fetching from https://github.com/apple/swift-numerics.git (cached) - -Creating working copy of package โ€˜swift-numericsโ€™ - -Creating working copy of package โ€˜swift-algorithmsโ€™ - -Checking out 1.2.2 of package โ€˜swift-algorithmsโ€™ - -Checking out 1.1.1 of package โ€˜swift-numericsโ€™ - - -Resolved source packages: - swift-numerics: https://github.com/apple/swift-numerics.git @ 1.1.1 - swift-algorithms: https://github.com/apple/swift-algorithms.git @ 1.2.2 -''', - ), - ]); - - final xcodeProjectInterpreter = XcodeProjectInterpreter( - logger: testLogger, - fileSystem: fs, - platform: platform, - processManager: fakeProcessManager, - analytics: const NoOpAnalytics(), - ); - await xcodeProjectInterpreter.prefetchSwiftPackages( - projectPath, - buildDirectory: buildDirectory, - quiet: false, - ); - expect(fakeProcessManager, hasNoRemainingExpectations); - expect( - testLogger.statusText, - contains(''' -Xcode is fetching Swift Package Manager dependencies. This may take several minutes... - Fetching from https://github.com/apple/swift-algorithms.git (cached)... - Fetching from https://github.com/apple/swift-numerics.git (cached)... -'''), - ); - }, - ); - - testWithoutContext('prefetchSwiftPackages skips running if already completed', () async { - final fs = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); - final platform = FakePlatform(operatingSystem: 'macos'); - const projectPath = 'path/to/project'; - final Directory buildDirectory = fs.directory('$projectPath/build/ios'); - final fakeProcessManager = FakeProcessManager.empty(); - - fakeProcessManager.addCommands([ - kWhichSysctlCommand, - kx64CheckCommand, - FakeCommand( - command: [ - 'pgrep', - '-n', - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/${buildDirectory.path}/SourcePackages', - '-resolvePackageDependencies', - ], - exitCode: 1, - ), - FakeCommand( - command: [ - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/${buildDirectory.path}/SourcePackages', - '-resolvePackageDependencies', - ], - ), - ]); - - final xcodeProjectInterpreter = XcodeProjectInterpreter( - logger: testLogger, - fileSystem: fs, - platform: platform, - processManager: fakeProcessManager, - analytics: const NoOpAnalytics(), - ); - - await xcodeProjectInterpreter.prefetchSwiftPackages( - projectPath, - buildDirectory: buildDirectory, - quiet: false, - ); - expect(fakeProcessManager, hasNoRemainingExpectations); - - await xcodeProjectInterpreter.prefetchSwiftPackages( - projectPath, - buildDirectory: buildDirectory, - quiet: false, - ); - - expect(fakeProcessManager, hasNoRemainingExpectations); - }); - - testWithoutContext('prefetchSwiftPackages does not print when quiet is true', () async { - final fs = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); - final platform = FakePlatform(operatingSystem: 'macos'); - const projectPath = 'path/to/project'; - final Directory buildDirectory = fileSystem.directory('$projectPath/build/ios'); - final fakeProcessManager = FakeProcessManager.empty(); - fakeProcessManager.addCommands([ - kWhichSysctlCommand, - kx64CheckCommand, - FakeCommand( - command: [ - 'pgrep', - '-n', - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/${buildDirectory.path}/SourcePackages', - '-resolvePackageDependencies', - ], - stdout: '12345', - ), - const FakeCommand(command: ['kill', '12345']), - FakeCommand( - command: [ - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/${buildDirectory.path}/SourcePackages', - '-resolvePackageDependencies', - ], - stdout: ''' -Resolve Package Graph - -Fetching from https://github.com/apple/swift-algorithms.git (cached) - -Fetching from https://github.com/apple/swift-numerics.git (cached) - -Creating working copy of package โ€˜swift-numericsโ€™ - -Creating working copy of package โ€˜swift-algorithmsโ€™ - -Checking out 1.2.2 of package โ€˜swift-algorithmsโ€™ - -Checking out 1.1.1 of package โ€˜swift-numericsโ€™ - - -Resolved source packages: - swift-numerics: https://github.com/apple/swift-numerics.git @ 1.1.1 - swift-algorithms: https://github.com/apple/swift-algorithms.git @ 1.2.2 -''', - ), - ]); - - final xcodeProjectInterpreter = XcodeProjectInterpreter( - logger: testLogger, - fileSystem: fs, - platform: platform, - processManager: fakeProcessManager, - analytics: const NoOpAnalytics(), - ); - await xcodeProjectInterpreter.prefetchSwiftPackages( - projectPath, - buildDirectory: buildDirectory, - ); - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(testLogger.statusText, isEmpty); - }); - - testWithoutContext( - 'prefetchSwiftPackages does not wait for command to complete when waitForCompletion is false', - () async { - final fs = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); - final platform = FakePlatform(operatingSystem: 'macos'); - const projectPath = 'path/to/project'; - final Directory buildDirectory = fileSystem.directory('$projectPath/build/ios'); - final fakeProcessManager = FakeProcessManager.empty(); - final commandCompleter = Completer(); - fakeProcessManager.addCommands([ - kWhichSysctlCommand, - kx64CheckCommand, - FakeCommand( - command: [ - 'pgrep', - '-n', - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/${buildDirectory.path}/SourcePackages', - '-resolvePackageDependencies', - ], - stdout: '12345', - ), - const FakeCommand(command: ['kill', '12345']), - FakeCommand( - command: [ - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/${buildDirectory.path}/SourcePackages', - '-resolvePackageDependencies', - ], - stdout: ''' -Resolve Package Graph - -Fetching from https://github.com/apple/swift-algorithms.git (cached) - -Fetching from https://github.com/apple/swift-numerics.git (cached) - -Creating working copy of package โ€˜swift-numericsโ€™ - -Creating working copy of package โ€˜swift-algorithmsโ€™ - -Checking out 1.2.2 of package โ€˜swift-algorithmsโ€™ +class FakeFlutterProject extends Fake implements FlutterProject {} -Checking out 1.1.1 of package โ€˜swift-numericsโ€™ +class FakeXcodeBasedProject extends IosProject { + FakeXcodeBasedProject(this.path, [FileSystem? fileSystem]) + : fs = fileSystem ?? MemoryFileSystem.test(), + super.fromFlutter(FakeFlutterProject()); + final String path; + final FileSystem fs; -Resolved source packages: - swift-numerics: https://github.com/apple/swift-numerics.git @ 1.1.1 - swift-algorithms: https://github.com/apple/swift-algorithms.git @ 1.2.2 -''', - completer: commandCompleter, - ), - ]); + @override + Directory get hostAppRoot => fs.directory(path); - final xcodeProjectInterpreter = XcodeProjectInterpreter( - logger: testLogger, - fileSystem: fs, - platform: platform, - processManager: fakeProcessManager, - analytics: const NoOpAnalytics(), - ); - await xcodeProjectInterpreter.prefetchSwiftPackages( - projectPath, - buildDirectory: buildDirectory, - waitForCompletion: false, - ); - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(testLogger.statusText, isEmpty); - expect(commandCompleter.isCompleted, isFalse); - commandCompleter.complete(); - }, - ); + @override + Directory get xcodeProject => fs.directory(path); - testWithoutContext('prefetchSwiftPackages throws exception when resolving fails', () async { - final fs = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); - final platform = FakePlatform(operatingSystem: 'macos'); - const projectPath = 'path/to/project'; - final Directory buildDirectory = fileSystem.directory('$projectPath/build/ios'); - final fakeProcessManager = FakeProcessManager.empty(); - fakeProcessManager.addCommands([ - kWhichSysctlCommand, - kx64CheckCommand, - FakeCommand( - command: [ - 'pgrep', - '-n', - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/${buildDirectory.path}/SourcePackages', - '-resolvePackageDependencies', - ], - ), - FakeCommand( - command: [ - 'xcrun', - 'xcodebuild', - '-clonedSourcePackagesDirPath', - '/${buildDirectory.path}/SourcePackages', - '-resolvePackageDependencies', - ], - exitCode: 1, - stderr: 'error: some error', - ), - ]); + @override + bool get usesSwiftPackageManager => true; - final xcodeProjectInterpreter = XcodeProjectInterpreter( - logger: testLogger, - fileSystem: fs, - platform: platform, - processManager: fakeProcessManager, - analytics: const NoOpAnalytics(), - ); - await expectLater( - xcodeProjectInterpreter.prefetchSwiftPackages( - projectPath, - buildDirectory: buildDirectory, - quiet: false, - ), - throwsToolExit(), - ); - }); + @override + bool get flutterPluginSwiftPackageInProjectSettings => true; } 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 ea7028f151cbd..23074d1dc11f5 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 @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io' as io; + import 'package:code_assets/code_assets.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; @@ -179,7 +181,7 @@ CMAKE_LINKER:FILEPATH=/usr/bin/ld # CMAKE_LINKER:FILEPATH=/some/path/to/ld.lld '''); - expect(cCompilerConfigLinux(cmakeDirectory: environment.buildDir), throwsA(isA())); + expect(cCompilerConfigLinux(cmakeDirectory: environment.outputDir), throwsA(isA())); }, ); @@ -200,7 +202,7 @@ CMAKE_AR:FILEPATH=/some/path/to/llvm-ar CMAKE_LINKER:FILEPATH=/some/path/to/ld.lld '''); - expect(cCompilerConfigLinux(cmakeDirectory: environment.buildDir), throwsA(isA())); + expect(cCompilerConfigLinux(cmakeDirectory: environment.outputDir), throwsA(isA())); }, ); @@ -219,6 +221,176 @@ CMAKE_LINKER:FILEPATH=/some/path/to/ld.lld expect(cCompilerConfigLinux(), completes); }, ); + + testUsingContext( + 'cCompilerConfigLinux missing CMakeCache and throwIfNotFound: false', + overrides: { + ProcessManager: () => FakeProcessManager.empty(), + FileSystem: () => fileSystem, + }, + () async { + if (!const LocalPlatform().isLinux) { + return; + } + + final CCompilerConfig? result = await cCompilerConfigLinux( + cmakeDirectory: environment.buildDir, + throwIfNotFound: false, + ); + expect(result, isNull); + }, + ); + + testUsingContext( + 'cCompilerConfigLinux missing entry and throwIfNotFound: false', + overrides: { + ProcessManager: () => FakeProcessManager.empty(), + FileSystem: () => fileSystem, + }, + () async { + if (!const LocalPlatform().isLinux) { + return; + } + + await environment.outputDir.childFile('CMakeCache.txt').writeAsString(''' +//CMAKE_CXX_COMPILER:FILEPATH=/some/path/to/clang++ +//CMAKE_AR:FILEPATH=/some/path/to/llvm-ar +# CMAKE_LINKER:FILEPATH=/some/path/to/ld.lld +'''); + + final CCompilerConfig? result = await cCompilerConfigLinux( + cmakeDirectory: environment.outputDir, + throwIfNotFound: false, + ); + expect(result, isNull); + }, + ); + + testUsingContext( + 'cCompilerConfigLinux invalid paths and throwIfNotFound: false', + overrides: { + ProcessManager: () => FakeProcessManager.empty(), + FileSystem: () => fileSystem, + }, + () async { + if (!const LocalPlatform().isLinux) { + return; + } + + await environment.outputDir.childFile('CMakeCache.txt').writeAsString(''' +CMAKE_CXX_COMPILER:FILEPATH=/some/path/to/clang++ +CMAKE_AR:FILEPATH=/some/path/to/llvm-ar +CMAKE_LINKER:FILEPATH=/some/path/to/ld.lld +'''); + + final CCompilerConfig? result = await cCompilerConfigLinux( + cmakeDirectory: environment.outputDir, + throwIfNotFound: false, + ); + expect(result, isNull); + }, + ); + + testUsingContext( + 'cCompilerConfigLinux FileSystemException on resolveSymbolicLinks and throwIfNotFound: false', + overrides: { + ProcessManager: () => FakeProcessManager.empty(), + FileSystem: () => + _ThrowingResolveFileSystem(fileSystem, '${environment.outputDir.path}/mock_clang++'), + }, + () async { + if (!const LocalPlatform().isLinux) { + return; + } + + await environment.outputDir.childFile('CMakeCache.txt').writeAsString(''' +CMAKE_CXX_COMPILER:FILEPATH=${environment.outputDir.path}/mock_clang++ +CMAKE_AR:FILEPATH=/some/path/to/llvm-ar +CMAKE_LINKER:FILEPATH=/some/path/to/ld.lld +'''); + + // Create the file so requireTool passes existsSync() + await environment.outputDir.childFile('mock_clang++').create(); + + final CCompilerConfig? result = await cCompilerConfigLinux( + cmakeDirectory: environment.outputDir, + throwIfNotFound: false, + ); + expect(result, isNull); + }, + ); + + testUsingContext( + 'cCompilerConfigLinux FileSystemException on resolveSymbolicLinks and throwIfNotFound: true', + overrides: { + ProcessManager: () => FakeProcessManager.empty(), + FileSystem: () => + _ThrowingResolveFileSystem(fileSystem, '${environment.outputDir.path}/mock_clang++'), + }, + () async { + if (!const LocalPlatform().isLinux) { + return; + } + + await environment.outputDir.childFile('CMakeCache.txt').writeAsString(''' +CMAKE_CXX_COMPILER:FILEPATH=${environment.outputDir.path}/mock_clang++ +CMAKE_AR:FILEPATH=/some/path/to/llvm-ar +CMAKE_LINKER:FILEPATH=/some/path/to/ld.lld +'''); + + // Create the file so requireTool passes existsSync() + await environment.outputDir.childFile('mock_clang++').create(); + + expect( + cCompilerConfigLinux(cmakeDirectory: environment.outputDir), + throwsA(isA()), + ); + }, + ); } class _BuildRunnerWithoutClang extends FakeFlutterNativeAssetsBuildRunner {} + +class _ThrowingResolveFileSystem extends ForwardingFileSystem { + _ThrowingResolveFileSystem(super.delegate, this.throwingPath); + + final String throwingPath; + + @override + File file(dynamic path) { + final File delegateFile = super.file(path); + if (delegateFile.path == throwingPath) { + return _ThrowingResolveFile(this, delegateFile); + } + return delegateFile; + } +} + +class _ThrowingResolveFile extends ForwardingFileSystemEntity with ForwardingFile { + _ThrowingResolveFile(this.fileSystem, this.delegate); + + @override + final io.File delegate; + + @override + final FileSystem fileSystem; + + @override + File wrapFile(io.File delegate) => _ThrowingResolveFile(fileSystem, delegate); + + @override + Directory wrapDirectory(io.Directory delegate) => throw UnimplementedError(); + + @override + Link wrapLink(io.Link delegate) => throw UnimplementedError(); + + @override + Future resolveSymbolicLinks() async { + throw const FileSystemException('Mock FileSystemException during resolveSymbolicLinks'); + } + + @override + String resolveSymbolicLinksSync() { + throw const FileSystemException('Mock FileSystemException during resolveSymbolicLinksSync'); + } +} diff --git a/packages/flutter_tools/test/general.shard/macos/darwin_dependency_management_test.dart b/packages/flutter_tools/test/general.shard/macos/darwin_dependency_management_test.dart index 5cd365864c9ae..6534a5882bd70 100644 --- a/packages/flutter_tools/test/general.shard/macos/darwin_dependency_management_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/darwin_dependency_management_test.dart @@ -4,17 +4,15 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_tools/src/base/config.dart'; import 'package:flutter_tools/src/base/logger.dart'; -import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/darwin/darwin.dart'; -import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/macos/cocoapods.dart'; import 'package:flutter_tools/src/macos/darwin_dependency_management.dart'; import 'package:flutter_tools/src/macos/swift_package_manager.dart'; import 'package:flutter_tools/src/platform_plugins.dart'; import 'package:flutter_tools/src/plugins.dart'; import 'package:flutter_tools/src/project.dart'; +import 'package:pub_semver/pub_semver.dart'; import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart'; @@ -34,7 +32,6 @@ void main() { group('when using Swift Package Manager', () { testWithoutContext('with only CocoaPod plugins', () async { final testFileSystem = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); final FakeAnalytics fakeAnalytics = getInitializedFakeAnalyticsInstance( fs: testFileSystem, fakeFlutterVersion: FakeFlutterVersion(), @@ -57,30 +54,14 @@ void main() { usesSwiftPackageManager: true, fileSystem: testFileSystem, ), - plugins: plugins, cocoapods: cocoaPods, swiftPackageManager: swiftPackageManager, fileSystem: testFileSystem, featureFlags: TestFeatureFlags(isSwiftPackageManagerEnabled: true), - logger: testLogger, analytics: fakeAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), - config: FakeConfig(), ); - await dependencyManagement.setUp(platform: platform); + await dependencyManagement.setUp(platform: platform, plugins: plugins); expect(swiftPackageManager.generated, isTrue); - expect( - testLogger.warningText, - contains( - 'The following plugins do not support Swift Package Manager for ' - '${platform.name}:\n' - ' - cocoapod_plugin_1\n' - 'This will become an error in a future version of Flutter. Please contact the ' - 'plugin maintainers to request Swift Package Manager adoption.\n', - ), - ); - expect(testLogger.statusText, isEmpty); expect(cocoaPods.podfileSetup, isTrue); expect( fakeAnalytics.sentEvents, @@ -104,7 +85,6 @@ void main() { 'with only Swift Package Manager plugins and no pod integration', () async { final testFileSystem = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); final FakeAnalytics testAnalytics = getInitializedFakeAnalyticsInstance( fs: testFileSystem, fakeFlutterVersion: FakeFlutterVersion(), @@ -135,21 +115,15 @@ void main() { final dependencyManagement = DarwinDependencyManagement( project: project, - plugins: plugins, cocoapods: cocoaPods, swiftPackageManager: swiftPackageManager, fileSystem: testFileSystem, featureFlags: TestFeatureFlags(isSwiftPackageManagerEnabled: true), - logger: testLogger, + analytics: testAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), - config: FakeConfig(), ); - await dependencyManagement.setUp(platform: platform); + await dependencyManagement.setUp(platform: platform, plugins: plugins); expect(swiftPackageManager.generated, isTrue); - expect(testLogger.warningText, isEmpty); - expect(testLogger.statusText, isEmpty); expect(cocoaPods.podfileSetup, isFalse); expect( testAnalytics.sentEvents, @@ -174,7 +148,6 @@ void main() { 'with only Swift Package Manager plugins but project not migrated', () async { final testFileSystem = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); final FakeAnalytics testAnalytics = getInitializedFakeAnalyticsInstance( fs: testFileSystem, fakeFlutterVersion: FakeFlutterVersion(), @@ -206,21 +179,15 @@ void main() { final dependencyManagement = DarwinDependencyManagement( project: project, - plugins: plugins, cocoapods: cocoaPods, swiftPackageManager: swiftPackageManager, fileSystem: testFileSystem, featureFlags: TestFeatureFlags(isSwiftPackageManagerEnabled: true), - logger: testLogger, + analytics: testAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), - config: FakeConfig(), ); - await dependencyManagement.setUp(platform: platform); + await dependencyManagement.setUp(platform: platform, plugins: plugins); expect(swiftPackageManager.generated, isTrue); - expect(testLogger.warningText, isEmpty); - expect(testLogger.statusText, isEmpty); expect(cocoaPods.podfileSetup, isFalse); expect( testAnalytics.sentEvents, @@ -245,7 +212,6 @@ void main() { 'with only Swift Package Manager plugins with preexisting standard CocoaPods Podfile', () async { final testFileSystem = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); final FakeAnalytics testAnalytics = getInitializedFakeAnalyticsInstance( fs: testFileSystem, fakeFlutterVersion: FakeFlutterVersion(), @@ -280,36 +246,15 @@ void main() { ); final dependencyManagement = DarwinDependencyManagement( project: project, - plugins: plugins, cocoapods: cocoaPods, swiftPackageManager: swiftPackageManager, fileSystem: testFileSystem, featureFlags: TestFeatureFlags(isSwiftPackageManagerEnabled: true), - logger: testLogger, + analytics: testAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), - config: FakeConfig(), ); - await dependencyManagement.setUp(platform: platform); + await dependencyManagement.setUp(platform: platform, plugins: plugins); expect(swiftPackageManager.generated, isTrue); - final xcconfigPrefix = platform == FlutterDarwinPlatform.macos ? 'Flutter-' : ''; - expect( - testLogger.warningText, - contains( - 'All plugins found for ${platform.name} are Swift Packages, ' - 'but your project still has CocoaPods integration. To remove ' - 'CocoaPods integration, complete the following steps:\n' - ' * In the ${platform.name}/ directory run "pod deintegrate"\n' - ' * Also in the ${platform.name}/ directory, delete the Podfile\n' - ' * Remove the include to "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" ' - 'in your ${platform.name}/Flutter/${xcconfigPrefix}Debug.xcconfig\n' - ' * Remove the include to "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" ' - 'in your ${platform.name}/Flutter/${xcconfigPrefix}Release.xcconfig\n\n' - "Removing CocoaPods integration will improve the project's build time.\n", - ), - ); - expect(testLogger.statusText, isEmpty); expect(cocoaPods.podfileSetup, isFalse); expect( testAnalytics.sentEvents, @@ -334,7 +279,6 @@ void main() { 'with only Swift Package Manager plugins with preexisting custom CocoaPods Podfile', () async { final testFileSystem = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); final FakeAnalytics testAnalytics = getInitializedFakeAnalyticsInstance( fs: testFileSystem, fakeFlutterVersion: FakeFlutterVersion(), @@ -370,40 +314,15 @@ void main() { final dependencyManagement = DarwinDependencyManagement( project: project, - plugins: plugins, cocoapods: cocoaPods, swiftPackageManager: swiftPackageManager, fileSystem: testFileSystem, featureFlags: TestFeatureFlags(isSwiftPackageManagerEnabled: true), - logger: testLogger, + analytics: testAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), - config: FakeConfig(), ); - await dependencyManagement.setUp(platform: platform); + await dependencyManagement.setUp(platform: platform, plugins: plugins); expect(swiftPackageManager.generated, isTrue); - final xcconfigPrefix = platform == FlutterDarwinPlatform.macos ? 'Flutter-' : ''; - expect( - testLogger.warningText, - contains( - 'All plugins found for ${platform.name} are Swift Packages, ' - 'but your project still has CocoaPods integration. Your ' - 'project uses a non-standard Podfile and will need to be ' - 'migrated to Swift Package Manager manually. Some steps you ' - 'may need to complete include:\n' - ' * In the ${platform.name}/ directory run "pod deintegrate"\n' - ' * Transition any Pod dependencies to Swift Package equivalents. ' - 'See https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app\n' - ' * Transition any custom logic\n' - ' * Remove the include to "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" ' - 'in your ${platform.name}/Flutter/${xcconfigPrefix}Debug.xcconfig\n' - ' * Remove the include to "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" ' - 'in your ${platform.name}/Flutter/${xcconfigPrefix}Release.xcconfig\n\n' - "Removing CocoaPods integration will improve the project's build time.\n", - ), - ); - expect(testLogger.statusText, isEmpty); expect(cocoaPods.podfileSetup, isFalse); expect( testAnalytics.sentEvents, @@ -426,7 +345,6 @@ void main() { testWithoutContext('with mixed plugins', () async { final testFileSystem = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); final FakeAnalytics testAnalytics = getInitializedFakeAnalyticsInstance( fs: testFileSystem, fakeFlutterVersion: FakeFlutterVersion(), @@ -469,24 +387,15 @@ void main() { final dependencyManagement = DarwinDependencyManagement( project: project, - plugins: plugins, cocoapods: cocoaPods, swiftPackageManager: swiftPackageManager, fileSystem: testFileSystem, featureFlags: TestFeatureFlags(isSwiftPackageManagerEnabled: true), - logger: testLogger, + analytics: testAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), - config: FakeConfig(), ); - await dependencyManagement.setUp(platform: platform); + await dependencyManagement.setUp(platform: platform, plugins: plugins); expect(swiftPackageManager.generated, isTrue); - expect( - testLogger.warningText, - contains('The following plugins do not support Swift Package Manager'), - ); - expect(testLogger.statusText, isEmpty); expect(cocoaPods.podfileSetup, isTrue); expect( testAnalytics.sentEvents, @@ -510,7 +419,6 @@ void main() { group('when not using Swift Package Manager', () { testWithoutContext('but project already migrated', () async { final testFileSystem = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); final FakeAnalytics testAnalytics = getInitializedFakeAnalyticsInstance( fs: testFileSystem, fakeFlutterVersion: FakeFlutterVersion(), @@ -541,24 +449,14 @@ void main() { final dependencyManagement = DarwinDependencyManagement( project: project, - plugins: plugins, cocoapods: cocoaPods, swiftPackageManager: swiftPackageManager, fileSystem: testFileSystem, featureFlags: TestFeatureFlags(isSwiftPackageManagerEnabled: true), - logger: testLogger, analytics: testAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), - config: FakeConfig(), ); - await dependencyManagement.setUp(platform: platform); + await dependencyManagement.setUp(platform: platform, plugins: plugins); expect(swiftPackageManager.generated, isTrue); - expect( - testLogger.warningText, - contains('The following plugins do not support Swift Package Manager'), - ); - expect(testLogger.statusText, isEmpty); expect(cocoaPods.podfileSetup, isTrue); expect( testAnalytics.sentEvents, @@ -580,7 +478,6 @@ void main() { testWithoutContext('with only CocoaPod plugins', () async { final testFileSystem = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); final FakeAnalytics testAnalytics = getInitializedFakeAnalyticsInstance( fs: testFileSystem, fakeFlutterVersion: FakeFlutterVersion(), @@ -600,24 +497,14 @@ void main() { final dependencyManagement = DarwinDependencyManagement( project: FakeFlutterProject(fileSystem: testFileSystem), - plugins: plugins, cocoapods: cocoaPods, swiftPackageManager: swiftPackageManager, fileSystem: testFileSystem, featureFlags: TestFeatureFlags(), - logger: testLogger, analytics: testAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), - config: FakeConfig(), ); - await dependencyManagement.setUp(platform: platform); + await dependencyManagement.setUp(platform: platform, plugins: plugins); expect(swiftPackageManager.generated, isFalse); - expect( - testLogger.warningText, - contains('The following plugins do not support Swift Package Manager'), - ); - expect(testLogger.statusText, isEmpty); expect(cocoaPods.podfileSetup, isTrue); expect( testAnalytics.sentEvents, @@ -639,7 +526,6 @@ void main() { testWithoutContext('with only Swift Package Manager plugins', () async { final testFileSystem = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); final FakeAnalytics testAnalytics = getInitializedFakeAnalyticsInstance( fs: testFileSystem, fakeFlutterVersion: FakeFlutterVersion(), @@ -662,129 +548,35 @@ void main() { fileSystem: testFileSystem, compatibleWithSwiftPackageManager: true, ), - plugins: plugins, cocoapods: cocoaPods, swiftPackageManager: swiftPackageManager, fileSystem: testFileSystem, featureFlags: TestFeatureFlags(), - logger: testLogger, analytics: testAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), - config: FakeConfig(), ); - await expectLater( - () => dependencyManagement.setUp(platform: platform), - throwsToolExit( - message: - 'Plugin swift_package_plugin_1 is only compatible with Swift Package Manager. Try ' - 'enabling Swift Package Manager by running ' - '"flutter config --enable-swift-package-manager" or remove the ' - 'plugin as a dependency.', + await dependencyManagement.setUp(platform: platform, plugins: plugins); + expect(swiftPackageManager.generated, isFalse); + expect(cocoaPods.podfileSetup, isTrue); + expect( + testAnalytics.sentEvents, + contains( + Event.flutterInjectDarwinPlugins( + platform: platform.name, + isModule: false, + swiftPackageManagerUsable: false, + swiftPackageManagerFeatureEnabled: false, + projectDisabledSwiftPackageManager: true, + projectHasSwiftPackageManagerIntegration: false, + pluginCount: 1, + swiftPackageCount: 1, + podCount: 0, + ), ), ); - expect(swiftPackageManager.generated, isFalse); - expect(cocoaPods.podfileSetup, isFalse); - expect(testAnalytics.sentEvents, isEmpty); }); - testWithoutContext( - 'with only Swift Package Manager plugins and project does not support', - () async { - final testFileSystem = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); - final FakeAnalytics testAnalytics = getInitializedFakeAnalyticsInstance( - fs: testFileSystem, - fakeFlutterVersion: FakeFlutterVersion(), - ); - final File swiftPackagePluginPodspec = testFileSystem.file( - '/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1/Package.swift', - )..createSync(recursive: true); - final plugins = [ - FakePlugin( - name: 'swift_package_plugin_1', - platforms: {platform.name: FakePluginPlatform()}, - pluginSwiftPackageManifestPath: swiftPackagePluginPodspec.path, - ), - ]; - final swiftPackageManager = FakeSwiftPackageManager(expectedPlugins: plugins); - final cocoaPods = FakeCocoaPods(); - - final dependencyManagement = DarwinDependencyManagement( - project: FakeFlutterProject( - fileSystem: testFileSystem, - compatibleWithSwiftPackageManager: false, - ), - plugins: plugins, - cocoapods: cocoaPods, - swiftPackageManager: swiftPackageManager, - fileSystem: testFileSystem, - featureFlags: TestFeatureFlags(), - logger: testLogger, - analytics: testAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), - config: FakeConfig(), - ); - await expectLater( - () => dependencyManagement.setUp(platform: platform), - throwsToolExit( - message: - 'Plugin swift_package_plugin_1 is only compatible with Swift Package ' - 'Manager, but your project does not currently support it.', - ), - ); - expect(swiftPackageManager.generated, isFalse); - expect(cocoaPods.podfileSetup, isFalse); - expect(testAnalytics.sentEvents, isEmpty); - }, - ); - - testWithoutContext( - 'with only Swift Package Manager plugins does not throw error on non-mac', - () async { - final testFileSystem = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); - final FakeAnalytics testAnalytics = getInitializedFakeAnalyticsInstance( - fs: testFileSystem, - fakeFlutterVersion: FakeFlutterVersion(), - ); - final File swiftPackagePluginPodspec = testFileSystem.file( - '/path/to/cocoapod_plugin_1/darwin/cocoapod_plugin_1/Package.swift', - )..createSync(recursive: true); - final plugins = [ - FakePlugin( - name: 'swift_package_plugin_1', - platforms: {platform.name: FakePluginPlatform()}, - pluginSwiftPackageManifestPath: swiftPackagePluginPodspec.path, - ), - ]; - final swiftPackageManager = FakeSwiftPackageManager(expectedPlugins: plugins); - final cocoaPods = FakeCocoaPods(); - - final dependencyManagement = DarwinDependencyManagement( - project: FakeFlutterProject(fileSystem: testFileSystem), - plugins: plugins, - cocoapods: cocoaPods, - swiftPackageManager: swiftPackageManager, - fileSystem: testFileSystem, - featureFlags: TestFeatureFlags(), - logger: testLogger, - analytics: testAnalytics, - platform: FakePlatform(operatingSystem: 'windows'), - xcodeProjectInterpreter: null, - config: FakeConfig(), - ); - await dependencyManagement.setUp(platform: platform); - expect(swiftPackageManager.generated, isFalse); - expect(cocoaPods.podfileSetup, isTrue); - expect(testAnalytics.sentEvents, isNotEmpty); - }, - ); - testWithoutContext('when project is a module', () async { final testFileSystem = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); final FakeAnalytics testAnalytics = getInitializedFakeAnalyticsInstance( fs: testFileSystem, fakeFlutterVersion: FakeFlutterVersion(), @@ -804,21 +596,15 @@ void main() { final dependencyManagement = DarwinDependencyManagement( project: FakeFlutterProject(fileSystem: testFileSystem, isModule: true), - plugins: plugins, cocoapods: cocoaPods, swiftPackageManager: swiftPackageManager, fileSystem: testFileSystem, featureFlags: TestFeatureFlags(), - logger: testLogger, + analytics: testAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), - config: FakeConfig(), ); - await dependencyManagement.setUp(platform: platform); + await dependencyManagement.setUp(platform: platform, plugins: plugins); expect(swiftPackageManager.generated, isFalse); - expect(testLogger.warningText, isEmpty); - expect(testLogger.statusText, isEmpty); expect(cocoaPods.podfileSetup, isFalse); expect(testAnalytics.sentEvents, isEmpty); }); @@ -831,7 +617,6 @@ void main() { 'does not reset output files not containing FlutterMacOS when using SwiftPM with macOS project', () async { final testFileSystem = MemoryFileSystem.test(); - final testLogger = BufferLogger.test(); final FakeAnalytics testAnalytics = getInitializedFakeAnalyticsInstance( fs: testFileSystem, fakeFlutterVersion: FakeFlutterVersion(), @@ -851,18 +636,13 @@ void main() { final dependencyManagement = DarwinDependencyManagement( project: project, - plugins: [], cocoapods: cocoaPods, swiftPackageManager: swiftPackageManager, fileSystem: testFileSystem, featureFlags: TestFeatureFlags(isSwiftPackageManagerEnabled: true), - logger: testLogger, analytics: testAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), - config: FakeConfig(), ); - await dependencyManagement.setUp(platform: FlutterDarwinPlatform.macos); + await dependencyManagement.setUp(platform: FlutterDarwinPlatform.macos, plugins: []); expect(xcodeProject.outputFileList.readAsStringSync(), 'Something else'); }, ); @@ -1146,19 +926,15 @@ version: 0.0.1 // Directory is NOT an example app (default 'app_name'), so validation should be skipped. final dependencyManagement = DarwinDependencyManagement( project: FakeFlutterProject(fileSystem: testFileSystem), - plugins: [plugin], cocoapods: FakeCocoaPods(), swiftPackageManager: FakeSwiftPackageManager(), fileSystem: testFileSystem, featureFlags: TestFeatureFlags(), - logger: testLogger, + analytics: fakeAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), - config: FakeConfig(), ); - await dependencyManagement.setUp(platform: FlutterDarwinPlatform.ios); + await dependencyManagement.setUp(platform: FlutterDarwinPlatform.ios, plugins: [plugin]); // The example-app-specific warning (always contains the docs URL) should not fire. expect(testLogger.warningText, isNot(contains(kSwiftPackageManagerDocsUrl))); }); @@ -1188,19 +964,15 @@ version: 0.0.1 fileSystem: testFileSystem, directoryOverride: '/my_plugin/example', ), - plugins: [plugin], cocoapods: FakeCocoaPods(), swiftPackageManager: FakeSwiftPackageManager(), fileSystem: testFileSystem, featureFlags: TestFeatureFlags(), - logger: testLogger, + analytics: fakeAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), - config: FakeConfig(), ); - await dependencyManagement.setUp(platform: FlutterDarwinPlatform.ios); + await dependencyManagement.setUp(platform: FlutterDarwinPlatform.ios, plugins: [plugin]); // Should not fire the example-app-specific warning (always contains the docs URL). expect(testLogger.warningText, isNot(contains(kSwiftPackageManagerDocsUrl))); }); @@ -1227,19 +999,15 @@ version: 0.0.1 fileSystem: testFileSystem, directoryOverride: '/my_plugin/example', ), - plugins: [plugin], cocoapods: FakeCocoaPods(), swiftPackageManager: FakeSwiftPackageManager(expectedPlugins: [plugin]), fileSystem: testFileSystem, featureFlags: TestFeatureFlags(), - logger: testLogger, + analytics: fakeAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), - config: FakeConfig(), ); - await dependencyManagement.setUp(platform: FlutterDarwinPlatform.ios); + await dependencyManagement.setUp(platform: FlutterDarwinPlatform.ios, plugins: [plugin]); expect(testLogger.warningText, isNot(contains(kSwiftPackageManagerDocsUrl))); }); @@ -1267,19 +1035,15 @@ version: 0.0.1 fileSystem: testFileSystem, directoryOverride: '/my_plugin/example', ), - plugins: [plugin], cocoapods: FakeCocoaPods(), swiftPackageManager: FakeSwiftPackageManager(expectedPlugins: [plugin]), fileSystem: testFileSystem, featureFlags: TestFeatureFlags(), - logger: testLogger, + analytics: fakeAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), - config: FakeConfig(), ); - await dependencyManagement.setUp(platform: FlutterDarwinPlatform.ios); + await dependencyManagement.setUp(platform: FlutterDarwinPlatform.ios, plugins: [plugin]); expect(testLogger.warningText, isNot(contains(kSwiftPackageManagerDocsUrl))); }); @@ -1317,23 +1081,394 @@ flutter: fileSystem: testFileSystem, directoryOverride: '/my_plugin/example', ), - plugins: [plugin], cocoapods: FakeCocoaPods(), swiftPackageManager: FakeSwiftPackageManager(expectedPlugins: [plugin]), fileSystem: testFileSystem, featureFlags: TestFeatureFlags(), - logger: testLogger, + analytics: fakeAnalytics, - platform: FakePlatform(operatingSystem: 'macos'), - xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), - config: FakeConfig(), ); - await dependencyManagement.setUp(platform: FlutterDarwinPlatform.ios); + await dependencyManagement.setUp(platform: FlutterDarwinPlatform.ios, plugins: [plugin]); expect(testLogger.warningText, isNot(contains(kSwiftPackageManagerDocsUrl))); }); }); }); + + group('validatePluginSupport', () { + testWithoutContext('when plugin does not support platform', () async { + final fileSystem = MemoryFileSystem.test(); + final logger = BufferLogger.test(); + final cocoapods = FakeCocoaPods(); + final project = FakeIosProject(fileSystem: fileSystem, usesSwiftPackageManager: true); + final plugins = [FakePlugin(name: 'plugin', platforms: {})]; + + await DarwinDependencyManagement.validatePluginSupport( + platform: FlutterDarwinPlatform.ios, + xcodeProject: project, + plugins: plugins, + fileSystem: fileSystem, + logger: logger, + cocoapods: cocoapods, + ); + + expect(logger.warningText, isEmpty); + }); + + testWithoutContext('filters plugins for the correct implementation', () async { + final fileSystem = MemoryFileSystem.test(); + final logger = BufferLogger.test(); + final cocoapods = FakeCocoaPods(); + final project = FakeIosProject( + fileSystem: fileSystem, + usesSwiftPackageManager: false, + compatibleWithSwiftPackageManager: true, + ); + + final appFacingPlugin = Plugin( + name: 'foo', + path: '', + defaultPackagePlatforms: const {IOSPlugin.kConfigKey: 'foo_ios'}, + pluginDartClassPlatforms: const {}, + platforms: const {}, + dependencies: const [], + isDirectDependency: true, + isDevDependency: false, + ); + final iosPlatformPlugin = Plugin( + name: 'foo_ios', + path: '/foo_ios', + implementsPackage: 'foo', + defaultPackagePlatforms: const {}, + pluginDartClassPlatforms: const {}, + platforms: const { + IOSPlugin.kConfigKey: IOSPlugin(name: 'foo_ios', classPrefix: ''), + }, + dependencies: const [], + isDirectDependency: false, + isDevDependency: false, + ); + final overrideIosPlatformPlugin = Plugin( + name: 'foo_ios_override', + path: '/foo_ios_override', + implementsPackage: 'foo', + defaultPackagePlatforms: const {}, + pluginDartClassPlatforms: const {}, + platforms: const { + IOSPlugin.kConfigKey: IOSPlugin(name: 'foo_ios_override', classPrefix: ''), + }, + dependencies: const [], + isDirectDependency: true, + isDevDependency: false, + ); + final androidPlatformPlugin = Plugin( + name: 'foo_android', + path: '/foo_android', + implementsPackage: 'foo', + defaultPackagePlatforms: const {}, + pluginDartClassPlatforms: const {}, + platforms: { + AndroidPlugin.kConfigKey: AndroidPlugin( + name: 'foo_android', + package: 'com.example', + pluginPath: '', + fileSystem: fileSystem, + ), + }, + dependencies: const [], + isDirectDependency: false, + isDevDependency: false, + ); + fileSystem.file('/foo_ios/ios/foo_ios.podspec').createSync(recursive: true); + fileSystem + .file('/foo_ios_override/ios/foo_ios_override/Package.swift') + .createSync(recursive: true); + fileSystem.file('/foo_ios_override/ios/foo_ios_override.podspec').createSync(recursive: true); + + final plugins = [ + appFacingPlugin, + iosPlatformPlugin, + androidPlatformPlugin, + overrideIosPlatformPlugin, + ]; + + await DarwinDependencyManagement.validatePluginSupport( + platform: FlutterDarwinPlatform.ios, + xcodeProject: project, + plugins: plugins, + fileSystem: fileSystem, + logger: logger, + cocoapods: cocoapods, + ); + expect(logger.warningText, isEmpty); + expect(logger.errorText, isEmpty); + }); + + testWithoutContext('when plugin supports platform but has no podspec or manifest', () async { + final fileSystem = MemoryFileSystem.test(); + final logger = BufferLogger.test(); + final cocoapods = FakeCocoaPods(); + final project = FakeIosProject(fileSystem: fileSystem, usesSwiftPackageManager: true); + final plugins = [ + FakePlugin( + name: 'plugin', + platforms: {IOSPlugin.kConfigKey: FakePluginPlatform()}, + ), + ]; + + await DarwinDependencyManagement.validatePluginSupport( + platform: FlutterDarwinPlatform.ios, + xcodeProject: project, + plugins: plugins, + fileSystem: fileSystem, + logger: logger, + cocoapods: cocoapods, + ); + + expect(logger.warningText, isEmpty); + }); + + testWithoutContext('throws when using SwiftPM-only plugin but SwiftPM is disabled', () async { + final fileSystem = MemoryFileSystem.test(); + final logger = BufferLogger.test(); + final cocoapods = FakeCocoaPods(); + final project = FakeIosProject( + fileSystem: fileSystem, + usesSwiftPackageManager: false, + compatibleWithSwiftPackageManager: true, + ); + final plugins = [ + FakePlugin( + name: 'plugin', + platforms: {IOSPlugin.kConfigKey: FakePluginPlatform()}, + pluginSwiftPackageManifestPath: '/plugin/ios/plugin/Package.swift', + ), + ]; + + expect( + () => DarwinDependencyManagement.validatePluginSupport( + platform: FlutterDarwinPlatform.ios, + xcodeProject: project, + plugins: plugins, + fileSystem: fileSystem, + logger: logger, + cocoapods: cocoapods, + ), + throwsToolExit( + message: + 'The following plugin(s) are only compatible with Swift Package Manager:\n' + ' - plugin\n\n' + 'Try enabling Swift Package Manager by running ' + '"flutter config --enable-swift-package-manager" or remove the ' + 'plugin as a dependency.', + ), + ); + }); + + testWithoutContext( + 'throws when using SwiftPM-only plugin but project does not support SwiftPM', + () async { + final fileSystem = MemoryFileSystem.test(); + final logger = BufferLogger.test(); + final cocoapods = FakeCocoaPods(); + final project = FakeIosProject( + fileSystem: fileSystem, + usesSwiftPackageManager: false, + compatibleWithSwiftPackageManager: false, + ); + final plugins = [ + FakePlugin( + name: 'plugin', + platforms: {IOSPlugin.kConfigKey: FakePluginPlatform()}, + pluginSwiftPackageManifestPath: '/plugin/ios/plugin/Package.swift', + ), + ]; + + expect( + () => DarwinDependencyManagement.validatePluginSupport( + platform: FlutterDarwinPlatform.ios, + xcodeProject: project, + plugins: plugins, + fileSystem: fileSystem, + logger: logger, + cocoapods: cocoapods, + ), + throwsToolExit( + message: + 'The following plugin(s) are only compatible with Swift Package Manager:\n' + ' - plugin\n\n' + 'Your project does not currently support Swift Package Manager. To support Swift Package Manager:\n' + ' - Ensure Xcode 15+ is being used ' + ' - Enable Swift Package Manager feature by running ' + '"flutter config --enable-swift-package-manager"', + ), + ); + }, + ); + + testWithoutContext('warns when CocoaPod-only plugins', () async { + final fileSystem = MemoryFileSystem.test(); + final logger = BufferLogger.test(); + final cocoapods = FakeCocoaPods(); + final project = FakeIosProject(fileSystem: fileSystem, usesSwiftPackageManager: true); + final plugins = [ + FakePlugin( + name: 'plugin', + platforms: {IOSPlugin.kConfigKey: FakePluginPlatform()}, + pluginPodspecPath: '/plugin/ios/plugin.podspec', + ), + ]; + + await DarwinDependencyManagement.validatePluginSupport( + platform: FlutterDarwinPlatform.ios, + xcodeProject: project, + plugins: plugins, + fileSystem: fileSystem, + logger: logger, + cocoapods: cocoapods, + ); + + expect( + logger.warningText, + 'The following plugins do not support Swift Package Manager for ios:\n' + ' - plugin\n' + 'This will become an error in a future version of Flutter. Please contact the plugin ' + 'maintainers to request Swift Package Manager adoption.\n', + ); + }); + + testWithoutContext('warns when CocoaPod is removeable and podfile matches original', () async { + final fileSystem = MemoryFileSystem.test(); + final logger = BufferLogger.test(); + final project = FakeIosProject(fileSystem: fileSystem, usesSwiftPackageManager: true); + project.xcodeProjectInfoFile.createSync(recursive: true); + project.xcodeProjectInfoFile.writeAsStringSync('FlutterGeneratedPluginSwiftPackage'); + project.podfile.createSync(recursive: true); + project.podfile.writeAsStringSync('template'); + + final cocoapods = FakeCocoaPods( + podFile: fileSystem.file('template') + ..createSync() + ..writeAsStringSync('template'), + configIncludesPods: false, + ); + + final plugins = [ + FakePlugin( + name: 'plugin', + platforms: {IOSPlugin.kConfigKey: FakePluginPlatform()}, + pluginSwiftPackageManifestPath: '/plugin/ios/plugin/Package.swift', + ), + ]; + + await DarwinDependencyManagement.validatePluginSupport( + platform: FlutterDarwinPlatform.ios, + xcodeProject: project, + plugins: plugins, + fileSystem: fileSystem, + logger: logger, + cocoapods: cocoapods, + ); + + expect( + logger.warningText, + 'All plugins found for ios are Swift Packages, but your project still has CocoaPods integration. ' + 'To remove CocoaPods integration, complete the following steps:\n' + ' * In the ios/ directory run "pod deintegrate"\n' + ' * Also in the ios/ directory, delete the Podfile\n\n' + "Removing CocoaPods integration will improve the project's build time.\n", + ); + }); + + testWithoutContext('warns when CocoaPod is removeable and podfile does not match original', () async { + final fileSystem = MemoryFileSystem.test(); + final logger = BufferLogger.test(); + final project = FakeIosProject(fileSystem: fileSystem, usesSwiftPackageManager: true); + project.xcodeProjectInfoFile.createSync(recursive: true); + project.xcodeProjectInfoFile.writeAsStringSync('FlutterGeneratedPluginSwiftPackage'); + project.podfile.createSync(recursive: true); + project.podfile.writeAsStringSync('modified'); + + final cocoapods = FakeCocoaPods( + podFile: fileSystem.file('template') + ..createSync() + ..writeAsStringSync('template'), + configIncludesPods: false, + ); + + final plugins = [ + FakePlugin( + name: 'plugin', + platforms: {IOSPlugin.kConfigKey: FakePluginPlatform()}, + pluginSwiftPackageManifestPath: '/plugin/ios/plugin/Package.swift', + ), + ]; + + await DarwinDependencyManagement.validatePluginSupport( + platform: FlutterDarwinPlatform.ios, + xcodeProject: project, + plugins: plugins, + fileSystem: fileSystem, + logger: logger, + cocoapods: cocoapods, + ); + + expect( + logger.warningText, + 'All plugins found for ios are Swift Packages, but your project still has CocoaPods integration. ' + 'Your project uses a non-standard Podfile and will need to be migrated to Swift Package Manager manually. ' + 'Some steps you may need to complete include:\n' + ' * In the ios/ directory run "pod deintegrate"\n' + ' * Transition any Pod dependencies to Swift Package equivalents. See https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app\n' + ' * Transition any custom logic\n\n' + "Removing CocoaPods integration will improve the project's build time.\n", + ); + }); + + testWithoutContext('warning when CocoaPod is removeable includes step to edit xcconfig', () async { + final fileSystem = MemoryFileSystem.test(); + final logger = BufferLogger.test(); + final project = FakeIosProject(fileSystem: fileSystem, usesSwiftPackageManager: true); + project.xcodeProjectInfoFile.createSync(recursive: true); + project.xcodeProjectInfoFile.writeAsStringSync('FlutterGeneratedPluginSwiftPackage'); + project.podfile.createSync(recursive: true); + project.podfile.writeAsStringSync('template'); + + final cocoapods = FakeCocoaPods( + podFile: fileSystem.file('template') + ..createSync() + ..writeAsStringSync('template'), + ); + + final plugins = [ + FakePlugin( + name: 'plugin', + platforms: {IOSPlugin.kConfigKey: FakePluginPlatform()}, + pluginSwiftPackageManifestPath: '/plugin/ios/plugin/Package.swift', + ), + ]; + + await DarwinDependencyManagement.validatePluginSupport( + platform: FlutterDarwinPlatform.ios, + xcodeProject: project, + plugins: plugins, + fileSystem: fileSystem, + logger: logger, + cocoapods: cocoapods, + ); + + expect( + logger.warningText, + 'All plugins found for ios are Swift Packages, but your project still has CocoaPods integration. ' + 'To remove CocoaPods integration, complete the following steps:\n' + ' * In the ios/ directory run "pod deintegrate"\n' + ' * Also in the ios/ directory, delete the Podfile\n' + ' * Remove the include to "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" in your ios/Flutter/Debug.xcconfig\n' + ' * Remove the include to "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" in your ios/Flutter/Release.xcconfig\n\n' + "Removing CocoaPods integration will improve the project's build time.\n", + ); + }); + }); } class FakeIosProject extends Fake implements IosProject { @@ -1341,12 +1476,17 @@ class FakeIosProject extends Fake implements IosProject { required MemoryFileSystem fileSystem, required this.usesSwiftPackageManager, bool? compatibleWithSwiftPackageManager, + FlutterProject? parent, }) : _compatibleWithSwiftPackageManager = compatibleWithSwiftPackageManager, + parent = parent ?? FakeFlutterProject(fileSystem: fileSystem), hostAppRoot = fileSystem.directory('app_name').childDirectory('ios'); @override Directory hostAppRoot; + @override + final FlutterProject parent; + @override File get podfile => hostAppRoot.childFile('Podfile'); @@ -1386,12 +1526,17 @@ class FakeMacOSProject extends Fake implements MacOSProject { required MemoryFileSystem fileSystem, required this.usesSwiftPackageManager, bool? compatibleWithSwiftPackageManager, + FlutterProject? parent, }) : _compatibleWithSwiftPackageManager = compatibleWithSwiftPackageManager, + parent = parent ?? FakeFlutterProject(fileSystem: fileSystem), hostAppRoot = fileSystem.directory('app_name').childDirectory('macos'); @override Directory hostAppRoot; + @override + final FlutterProject parent; + @override File get podfile => hostAppRoot.childFile('Podfile'); @@ -1539,6 +1684,25 @@ class FakePlugin extends Fake implements Plugin { @override final Map platforms; + @override + String? get implementsPackage => null; + + @override + Map get defaultPackagePlatforms => const {}; + + @override + Map get pluginDartClassPlatforms => + const {}; + + @override + bool get isDirectDependency => true; + + @override + bool get isDevDependency => false; + + @override + VersionConstraint? get flutterConstraint => null; + @override String? pluginSwiftPackageManifestPath(FileSystem fileSystem, String platform) { return _pluginSwiftPackageManifestPath; @@ -1548,21 +1712,16 @@ class FakePlugin extends Fake implements Plugin { String? pluginPodspecPath(FileSystem fileSystem, String platform) { return _pluginPodspecPath; } -} -class FakePluginPlatform extends Fake implements PluginPlatform {} - -class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter { @override - Future prefetchSwiftPackages( - String projectPath, { - required Directory buildDirectory, - bool quiet = true, - bool waitForCompletion = true, - }) async {} -} + bool supportSwiftPackageManagerForPlatform(FileSystem fileSystem, String platform) { + return _pluginSwiftPackageManifestPath != null; + } -class FakeConfig extends Fake implements Config { @override - Object? getValue(String key) => null; + bool supportCocoapodsForPlatform(FileSystem fileSystem, String platform) { + return _pluginPodspecPath != null; + } } + +class FakePluginPlatform extends Fake implements PluginPlatform {} diff --git a/packages/flutter_tools/test/general.shard/macos/swift_package_manager_test.dart b/packages/flutter_tools/test/general.shard/macos/swift_package_manager_test.dart index da5825584ea08..2c861ff948761 100644 --- a/packages/flutter_tools/test/general.shard/macos/swift_package_manager_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/swift_package_manager_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io' as io; + import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:file_testing/file_testing.dart'; @@ -559,6 +561,182 @@ let package = Package( '''); expect(processManager, hasNoRemainingExpectations); }); + + group('robust symlink creation', () { + testWithoutContext('directory already exists at symlink path', () async { + final fs = MemoryFileSystem(); + final processManager = FakeProcessManager.any(); + final logger = BufferLogger.test(); + final project = FakeXcodeProject(platform: platform.name, fileSystem: fs); + + final validPlugin1 = FakePlugin( + name: 'valid_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + ); + fs + .file('${validPlugin1.path}/${platform.name}/${validPlugin1.name}/Package.swift') + .createSync(recursive: true); + + // Pre-create a directory at the symlink path. + final Directory symlinkPath = project.relativeSwiftPackagesDirectory.childDirectory( + 'valid_plugin_1-1.0.0', + ); + symlinkPath.createSync(recursive: true); + + final spm = SwiftPackageManager( + fileSystem: fs, + templateRenderer: const MustacheTemplateRenderer(), + processUtils: ProcessUtils(processManager: processManager, logger: logger), + config: FakeConfig(), + ); + await spm.generatePluginsSwiftPackage([validPlugin1], platform, project); + + expect( + project.relativeSwiftPackagesDirectory.childLink('valid_plugin_1-1.0.0'), + exists, + ); + expect( + project.relativeSwiftPackagesDirectory + .childLink('valid_plugin_1-1.0.0') + .targetSync(), + '${validPlugin1.path}/${platform.name}/valid_plugin_1', + ); + }); + + testWithoutContext('file already exists at symlink path', () async { + final fs = MemoryFileSystem(); + final processManager = FakeProcessManager.any(); + final logger = BufferLogger.test(); + final project = FakeXcodeProject(platform: platform.name, fileSystem: fs); + + final validPlugin1 = FakePlugin( + name: 'valid_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + ); + fs + .file('${validPlugin1.path}/${platform.name}/${validPlugin1.name}/Package.swift') + .createSync(recursive: true); + + // Pre-create a file at the symlink path. + final File symlinkPath = project.relativeSwiftPackagesDirectory.childFile( + 'valid_plugin_1-1.0.0', + ); + symlinkPath.createSync(recursive: true); + + final spm = SwiftPackageManager( + fileSystem: fs, + templateRenderer: const MustacheTemplateRenderer(), + processUtils: ProcessUtils(processManager: processManager, logger: logger), + config: FakeConfig(), + ); + await spm.generatePluginsSwiftPackage([validPlugin1], platform, project); + + expect( + project.relativeSwiftPackagesDirectory.childLink('valid_plugin_1-1.0.0'), + exists, + ); + expect( + project.relativeSwiftPackagesDirectory + .childLink('valid_plugin_1-1.0.0') + .targetSync(), + '${validPlugin1.path}/${platform.name}/valid_plugin_1', + ); + }); + + testWithoutContext('broken symlink already exists at symlink path', () async { + final fs = MemoryFileSystem(); + final processManager = FakeProcessManager.any(); + final logger = BufferLogger.test(); + final project = FakeXcodeProject(platform: platform.name, fileSystem: fs); + + final validPlugin1 = FakePlugin( + name: 'valid_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + ); + fs + .file('${validPlugin1.path}/${platform.name}/${validPlugin1.name}/Package.swift') + .createSync(recursive: true); + + // Pre-create a broken symlink at the symlink path pointing to a non-existent path. + final Link symlinkPath = project.relativeSwiftPackagesDirectory.childLink( + 'valid_plugin_1-1.0.0', + ); + symlinkPath.parent.createSync(recursive: true); + symlinkPath.createSync('/nonexistent/path'); + + final spm = SwiftPackageManager( + fileSystem: fs, + templateRenderer: const MustacheTemplateRenderer(), + processUtils: ProcessUtils(processManager: processManager, logger: logger), + config: FakeConfig(), + ); + await spm.generatePluginsSwiftPackage([validPlugin1], platform, project); + + expect( + project.relativeSwiftPackagesDirectory.childLink('valid_plugin_1-1.0.0'), + exists, + ); + expect( + project.relativeSwiftPackagesDirectory + .childLink('valid_plugin_1-1.0.0') + .targetSync(), + '${validPlugin1.path}/${platform.name}/valid_plugin_1', + ); + }); + + testWithoutContext( + 'parallel build concurrently creates correct symlink and throws OS error 17', + () async { + final delegateFs = MemoryFileSystem(); + final fs = ErrorInjectingForwardingFileSystem(delegateFs); + final processManager = FakeProcessManager.any(); + final logger = BufferLogger.test(); + final project = FakeXcodeProject(platform: platform.name, fileSystem: fs); + + final validPlugin1 = FakePlugin( + name: 'valid_plugin_1', + platforms: {platform.name: FakePluginPlatform()}, + ); + fs + .file( + '${validPlugin1.path}/${platform.name}/${validPlugin1.name}/Package.swift', + ) + .createSync(recursive: true); + + fs.errorToThrowOnLinkCreate = const FileSystemException( + 'File exists', + '', + OSError('File exists', 17), + ); + + final Link symlinkPath = project.relativeSwiftPackagesDirectory.childLink( + 'valid_plugin_1-1.0.0', + ); + delegateFs.link(symlinkPath.path) + ..parent.createSync(recursive: true) + ..createSync('${validPlugin1.path}/${platform.name}/valid_plugin_1'); + + final spm = SwiftPackageManager( + fileSystem: fs, + templateRenderer: const MustacheTemplateRenderer(), + processUtils: ProcessUtils(processManager: processManager, logger: logger), + config: FakeConfig(), + ); + await spm.generatePluginsSwiftPackage([validPlugin1], platform, project); + + expect( + project.relativeSwiftPackagesDirectory.childLink('valid_plugin_1-1.0.0'), + exists, + ); + expect( + project.relativeSwiftPackagesDirectory + .childLink('valid_plugin_1-1.0.0') + .targetSync(), + '${validPlugin1.path}/${platform.name}/valid_plugin_1', + ); + }, + ); + }); }); }); } @@ -566,7 +744,7 @@ let package = Package( } class FakeXcodeProject extends Fake implements IosProject { - FakeXcodeProject({required MemoryFileSystem fileSystem, required String platform}) + FakeXcodeProject({required FileSystem fileSystem, required String platform}) : hostAppRoot = fileSystem.directory('app_name').childDirectory(platform); @override @@ -649,3 +827,47 @@ class FakeConfig extends Fake implements Config { @override Object? getValue(String key) => null; } + +class ErrorInjectingForwardingFileSystem extends ForwardingFileSystem { + ErrorInjectingForwardingFileSystem(super.delegate); + + FileSystemException? errorToThrowOnLinkCreate; + + @override + Link link(dynamic path) { + final Link delegateLink = delegate.link(path); + return _ErrorInjectingLink(this, delegateLink); + } +} + +class _ErrorInjectingLink extends ForwardingFileSystemEntity with ForwardingLink { + _ErrorInjectingLink(ErrorInjectingForwardingFileSystem fileSystem, this.delegate) + : _fileSystem = fileSystem; + + final ErrorInjectingForwardingFileSystem _fileSystem; + + @override + final io.Link delegate; + + @override + FileSystem get fileSystem => _fileSystem; + + @override + File wrapFile(io.File delegate) => delegate as File; + + @override + Directory wrapDirectory(io.Directory delegate) => delegate as Directory; + + @override + Link wrapLink(io.Link delegate) => _ErrorInjectingLink(_fileSystem, delegate); + + @override + void createSync(String target, {bool recursive = false}) { + if (_fileSystem.errorToThrowOnLinkCreate != null) { + final FileSystemException err = _fileSystem.errorToThrowOnLinkCreate!; + _fileSystem.errorToThrowOnLinkCreate = null; + throw err; + } + super.createSync(target, recursive: recursive); + } +} diff --git a/packages/flutter_tools/test/general.shard/migrations/swift_package_manager_integration_migration_test.dart b/packages/flutter_tools/test/general.shard/migrations/swift_package_manager_integration_migration_test.dart index df6e5d4aee199..3df6840f32cc1 100644 --- a/packages/flutter_tools/test/general.shard/migrations/swift_package_manager_integration_migration_test.dart +++ b/packages/flutter_tools/test/general.shard/migrations/swift_package_manager_integration_migration_test.dart @@ -4519,7 +4519,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart index 602858ef26380..108f5faeae433 100644 --- a/packages/flutter_tools/test/general.shard/plugins_test.dart +++ b/packages/flutter_tools/test/general.shard/plugins_test.dart @@ -2732,6 +2732,234 @@ flutter: ProcessManager: () => FakeProcessManager.empty(), }, ); + + group('resolvePluginImplementationsForPlatform', () { + testWithoutContext('filters plugins based on platformKey', () { + final iosPlugin = Plugin( + name: 'ios_plugin', + path: '', + defaultPackagePlatforms: const {}, + pluginDartClassPlatforms: const {}, + platforms: const { + IOSPlugin.kConfigKey: IOSPlugin(name: 'ios_plugin', classPrefix: ''), + }, + dependencies: const [], + isDirectDependency: true, + isDevDependency: false, + ); + final macosPlugin = Plugin( + name: 'macos_plugin', + path: '', + defaultPackagePlatforms: const {}, + pluginDartClassPlatforms: const {}, + platforms: const { + MacOSPlugin.kConfigKey: MacOSPlugin(name: 'macos_plugin'), + }, + dependencies: const [], + isDirectDependency: true, + isDevDependency: false, + ); + final plugins = [iosPlugin, macosPlugin]; + + final List iosResolved = resolvePluginImplementationsForPlatform( + plugins, + IOSPlugin.kConfigKey, + ); + expect(iosResolved, contains(iosPlugin)); + expect(iosResolved, isNot(contains(macosPlugin))); + + final List macosResolved = resolvePluginImplementationsForPlatform( + plugins, + MacOSPlugin.kConfigKey, + ); + expect(macosResolved, contains(macosPlugin)); + expect(macosResolved, isNot(contains(iosPlugin))); + }); + + testWithoutContext('ensures the correct federated implementation is selected', () { + final fs = MemoryFileSystem.test(); + final appFacingPlugin = Plugin( + name: 'foo', + path: '', + defaultPackagePlatforms: const {IOSPlugin.kConfigKey: 'foo_ios'}, + pluginDartClassPlatforms: const {}, + platforms: const {}, + dependencies: const [], + isDirectDependency: true, + isDevDependency: false, + ); + final iosPlatformPlugin = Plugin( + name: 'foo_ios', + path: '', + implementsPackage: 'foo', + defaultPackagePlatforms: const {}, + pluginDartClassPlatforms: const {}, + platforms: const { + IOSPlugin.kConfigKey: IOSPlugin(name: 'foo_ios', classPrefix: ''), + }, + dependencies: const [], + isDirectDependency: false, + isDevDependency: false, + ); + final overrideIosPlatformPlugin = Plugin( + name: 'foo_ios_override', + path: '', + implementsPackage: 'foo', + defaultPackagePlatforms: const {}, + pluginDartClassPlatforms: const {}, + platforms: const { + IOSPlugin.kConfigKey: IOSPlugin(name: 'foo_ios_override', classPrefix: ''), + }, + dependencies: const [], + isDirectDependency: true, + isDevDependency: false, + ); + final androidPlatformPlugin = Plugin( + name: 'foo_android', + path: '', + implementsPackage: 'foo', + defaultPackagePlatforms: const {}, + pluginDartClassPlatforms: const {}, + platforms: { + AndroidPlugin.kConfigKey: AndroidPlugin( + name: 'foo_android', + package: 'com.example', + pluginPath: '', + fileSystem: fs, + ), + }, + dependencies: const [], + isDirectDependency: false, + isDevDependency: false, + ); + final plugins = [ + appFacingPlugin, + iosPlatformPlugin, + androidPlatformPlugin, + overrideIosPlatformPlugin, + ]; + + final List iosResolved = resolvePluginImplementationsForPlatform( + plugins, + IOSPlugin.kConfigKey, + ); + expect(iosResolved, contains(overrideIosPlatformPlugin)); + expect(iosResolved, isNot(contains(iosPlatformPlugin))); + expect(iosResolved, isNot(contains(appFacingPlugin))); + expect(iosResolved, isNot(contains(androidPlatformPlugin))); + }); + + testUsingContext('respects quiet parameter', () { + final appFacingPluginNoDefaultPackage = Plugin( + name: 'foo', + path: '', + defaultPackagePlatforms: const {IOSPlugin.kConfigKey: 'foo_ios'}, + pluginDartClassPlatforms: const {}, + platforms: const {}, + dependencies: const [], + isDirectDependency: true, + isDevDependency: false, + ); + final appFacingPluginWithNoInlineImplDefault = Plugin( + name: 'bar', + path: '', + defaultPackagePlatforms: const {IOSPlugin.kConfigKey: 'bar_ios'}, + pluginDartClassPlatforms: const {}, + platforms: const {}, + dependencies: const [], + isDirectDependency: true, + isDevDependency: false, + ); + final barIosWithNoInlineImpl = Plugin( + name: 'bar_ios', + path: '', + defaultPackagePlatforms: const {}, + pluginDartClassPlatforms: const {}, + platforms: const {}, + dependencies: const [], + isDirectDependency: false, + isDevDependency: false, + ); + final invalidPlugin = Plugin( + name: 'invalid_plugin', + path: '', + implementsPackage: 'some_package', + defaultPackagePlatforms: const {IOSPlugin.kConfigKey: 'some_default'}, + pluginDartClassPlatforms: const {}, + platforms: const {}, + dependencies: const [], + isDirectDependency: true, + isDevDependency: false, + ); + final directImpl1 = Plugin( + name: 'impl1', + path: '', + implementsPackage: 'conflict_foo', + defaultPackagePlatforms: const {}, + pluginDartClassPlatforms: const {}, + platforms: const { + IOSPlugin.kConfigKey: IOSPlugin(name: 'impl1', classPrefix: ''), + }, + dependencies: const [], + isDirectDependency: true, + isDevDependency: false, + ); + final directImpl2 = Plugin( + name: 'impl2', + path: '', + implementsPackage: 'conflict_foo', + defaultPackagePlatforms: const {}, + pluginDartClassPlatforms: const {}, + platforms: const { + IOSPlugin.kConfigKey: IOSPlugin(name: 'impl2', classPrefix: ''), + }, + dependencies: const [], + isDirectDependency: true, + isDevDependency: false, + ); + + final plugins = [ + appFacingPluginNoDefaultPackage, + appFacingPluginWithNoInlineImplDefault, + barIosWithNoInlineImpl, + invalidPlugin, + directImpl1, + directImpl2, + ]; + + expect( + () => resolvePluginImplementationsForPlatform(plugins, IOSPlugin.kConfigKey, quiet: true), + throwsToolExit(), + ); + expect(testLogger.warningText, isEmpty); + expect(testLogger.errorText, isEmpty); + + expect( + () => resolvePluginImplementationsForPlatform(plugins, IOSPlugin.kConfigKey), + throwsToolExit(), + ); + expect( + testLogger.warningText, + contains('references foo_ios:ios as the default plugin, but the package does not exist'), + ); + expect( + testLogger.warningText, + contains( + 'references bar_ios:ios as the default plugin, but it does not provide an inline implementation', + ), + ); + expect( + testLogger.errorText, + contains( + 'invalid_plugin:ios provides an implementation for some_package and also references a default implementation', + ), + ); + expect( + testLogger.errorText, + contains('conflict_foo:ios has conflicting direct dependency implementations'), + ); + }); + }); } class FakeFlutterManifest extends Fake implements FlutterManifest { @@ -2964,7 +3192,10 @@ class FakeDarwinDependencyManagement extends Fake implements DarwinDependencyMan List setupPlatforms = []; @override - Future setUp({required FlutterDarwinPlatform platform}) async { + Future setUp({ + required FlutterDarwinPlatform platform, + required List plugins, + }) async { setupPlatforms.add(platform); } } diff --git a/packages/flutter_tools/test/general.shard/project_test.dart b/packages/flutter_tools/test/general.shard/project_test.dart index 9868839091091..93f620bc640a2 100644 --- a/packages/flutter_tools/test/general.shard/project_test.dart +++ b/packages/flutter_tools/test/general.shard/project_test.dart @@ -1002,6 +1002,111 @@ plugins { FlutterProjectFactory: () => flutterProjectFactory, }, ); + + group('_kotlinCompilerOptionsPattern', () { + void testRegex(String description, String content, {bool expected = true}) { + testUsingContext( + description, + () async { + final FlutterProject project = await someProject(); + addAndroidGradleFile(project.directory, gradleFileContent: () => content); + expect(project.android.isKotlin, expected); + }, + overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + XcodeProjectInterpreter: () => xcodeProjectInterpreter, + FlutterProjectFactory: () => flutterProjectFactory, + }, + ); + } + + testRegex('matches standard formatting', ''' +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} +'''); + + testRegex('matches irregular whitespace', ''' +kotlin +{ + compilerOptions + { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} +'''); + + testRegex('matches even when there are additional blocks in kotlin{}', ''' +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } + + sourceSets { + main { + kotlin.srcDir("src/main/custom_kotlin") + } + } +} +'''); + + testRegex( + 'matches no spacing', + 'kotlin{compilerOptions{jvmTarget=org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17}}', + ); + + testRegex('does not match generic block without kotlin.compilerOptions{} DSL', ''' +android { + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} +''', expected: false); + }); + + testUsingContext( + 'isKotlin is true if src/main/kotlin directory exists', + () async { + final FlutterProject project = await someProject(); + project.android.hostAppGradleRoot + .childDirectory('app') + .childDirectory('src') + .childDirectory('main') + .childDirectory('kotlin') + .createSync(recursive: true); + expect(project.android.isKotlin, isTrue); + }, + overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + XcodeProjectInterpreter: () => xcodeProjectInterpreter, + FlutterProjectFactory: () => flutterProjectFactory, + }, + ); + + testUsingContext( + 'isKotlin is false if src/main/kotlin directory does not exist', + () async { + final FlutterProject project = await someProject(); + project.android.hostAppGradleRoot + .childDirectory('app') + .childDirectory('src') + .childDirectory('main') + .childDirectory('java') + .createSync(recursive: true); + expect(project.android.isKotlin, isFalse); + }, + overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + XcodeProjectInterpreter: () => xcodeProjectInterpreter, + FlutterProjectFactory: () => flutterProjectFactory, + }, + ); }); group('With mocked context', () { @@ -2534,7 +2639,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future> getBuildSettings( - String projectPath, { + XcodeBasedProject xcodeProject, { XcodeProjectBuildContext? buildContext, Duration timeout = const Duration(minutes: 1), }) async { @@ -2546,7 +2651,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index e18709837fcd5..e4c64d739a2e1 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -1963,6 +1963,33 @@ flutter: }, ); + testUsingContext( + 'ResidentWebRunner throws ToolExit when DWDS debug connection times out', + () async { + final logger = BufferLogger.test(); + final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, logger: logger); + fakeVmServiceHost = FakeVmServiceHost(requests: []); + setupMocks(); + webDevFS.exception = TimeoutException('Connection timed out'); + + await expectLater( + residentWebRunner.run, + throwsToolExit(message: 'Failed to connect to the web debug service.'), + ); + expect( + logger.errorText, + contains( + 'Failed to establish connection with the web debug service: TimeoutException: Connection timed out', + ), + ); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + Pub: ThrowingPub.new, + }, + ); + testUsingContext( 'throws when port is an integer outside the valid TCP range', () async { diff --git a/packages/flutter_tools/test/general.shard/shorebird/shorebird_yaml_test.dart b/packages/flutter_tools/test/general.shard/shorebird/shorebird_yaml_test.dart new file mode 100644 index 0000000000000..240ae45f5bb18 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/shorebird/shorebird_yaml_test.dart @@ -0,0 +1,113 @@ +// Copyright 2024 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:flutter_tools/src/shorebird/shorebird_yaml.dart'; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; + +void main() { + group('ShorebirdYaml', () { + test('yaml ignores comments', () { + const String yamlContents = ''' +# This file is used to configure the Shorebird updater used by your app. +app_id: 6160a7d8-cc18-4928-1233-05b51c0bb02c + +# auto_update controls if Shorebird should automatically update in the background on launch. +auto_update: false +'''; + final YamlDocument input = loadYamlDocument(yamlContents); + final YamlMap yamlMap = input.contents as YamlMap; + final Map compiled = compileShorebirdYaml( + yamlMap, + flavor: null, + environment: {}, + ); + expect(compiled, { + 'app_id': '6160a7d8-cc18-4928-1233-05b51c0bb02c', + 'auto_update': false, + }); + }); + test('flavors', () { + // These are invalid app_ids but make for easy testing. + const String yamlContents = ''' +app_id: 1-a +auto_update: false +flavors: + foo: 2-a + bar: 3-a +'''; + final YamlDocument input = loadYamlDocument(yamlContents); + final YamlMap yamlMap = input.contents as YamlMap; + expect(appIdForFlavor(yamlMap, flavor: null), '1-a'); + expect(appIdForFlavor(yamlMap, flavor: 'foo'), '2-a'); + expect(appIdForFlavor(yamlMap, flavor: 'bar'), '3-a'); + expect(() => appIdForFlavor(yamlMap, flavor: 'unknown'), throwsException); + }); + test('all values', () { + // These are invalid app_ids but make for easy testing. + const String yamlContents = ''' +app_id: 1-a +auto_update: false +flavors: + foo: 2-a + bar: 3-a +base_url: https://example.com +patch_verification: strict +'''; + final YamlDocument input = loadYamlDocument(yamlContents); + final YamlMap yamlMap = input.contents as YamlMap; + final Map compiled1 = compileShorebirdYaml( + yamlMap, + flavor: null, + environment: {}, + ); + expect(compiled1, { + 'app_id': '1-a', + 'auto_update': false, + 'base_url': 'https://example.com', + 'patch_verification': 'strict', + }); + final Map compiled2 = compileShorebirdYaml( + yamlMap, + flavor: 'foo', + environment: {'SHOREBIRD_PUBLIC_KEY': '4-a'}, + ); + expect(compiled2, { + 'app_id': '2-a', + 'auto_update': false, + 'base_url': 'https://example.com', + 'patch_verification': 'strict', + 'patch_public_key': '4-a', + }); + }); + test('edit in place', () { + const String yamlContents = ''' +app_id: 1-a +auto_update: false +flavors: + foo: 2-a + bar: 3-a +base_url: https://example.com +'''; + // Make a temporary file to test editing in place. + final Directory tempDir = Directory.systemTemp.createTempSync('shorebird_yaml_test.'); + final File tempFile = File('${tempDir.path}/shorebird.yaml'); + tempFile.writeAsStringSync(yamlContents); + updateShorebirdYaml( + 'foo', + tempFile.path, + environment: {'SHOREBIRD_PUBLIC_KEY': '4-a'}, + ); + final String updatedContents = tempFile.readAsStringSync(); + // Order is not guaranteed, so parse as YAML to compare. + final YamlDocument updated = loadYamlDocument(updatedContents); + final YamlMap yamlMap = updated.contents as YamlMap; + expect(yamlMap['app_id'], '2-a'); + expect(yamlMap['auto_update'], false); + expect(yamlMap['base_url'], 'https://example.com'); + }); + }); +} diff --git a/packages/flutter_tools/test/general.shard/version_test.dart b/packages/flutter_tools/test/general.shard/version_test.dart index 1d8360c14866c..90681d938b5b0 100644 --- a/packages/flutter_tools/test/general.shard/version_test.dart +++ b/packages/flutter_tools/test/general.shard/version_test.dart @@ -67,6 +67,20 @@ void main() { required int commitsBetweenRefs, }) { return [ + // Shorebird release branch check (returns empty to fall through to + // regular version lookup). + FakeCommand( + command: [ + 'git', + 'for-each-ref', + '--contains', + headRef, + '--format', + '%(refname:short)', + 'refs/remotes/origin/flutter_release/*', + ], + stdout: '', + ), FakeCommand( command: const [ 'git', @@ -1524,6 +1538,123 @@ void main() { expect(processManager, hasNoRemainingExpectations); }, overrides: {Git: () => git}); + testUsingContext('determine resolves version from shorebird flutter_release branch', () { + processManager.addCommands([ + const FakeCommand( + command: ['git', 'tag', '--points-at', 'HEAD'], + // No tag at HEAD. + ), + const FakeCommand( + command: [ + 'git', + 'for-each-ref', + '--contains', + 'HEAD', + '--format', + '%(refname:short)', + 'refs/remotes/origin/flutter_release/*', + ], + stdout: 'origin/flutter_release/3.41.4', + ), + ]); + final platform = FakePlatform(); + + final GitTagVersion gitTagVersion = GitTagVersion.determine( + platform, + git: git, + workingDirectory: '.', + ); + expect(gitTagVersion.frameworkVersionFor('abcd1234'), '3.41.4'); + expect(processManager, hasNoRemainingExpectations); + }); + + testUsingContext('determine skips non-stable shorebird flutter_release branches', () { + const headRevision = 'abcd1234'; + processManager.addCommands([ + const FakeCommand( + command: ['git', 'tag', '--points-at', 'HEAD'], + // No tag at HEAD. + ), + // Shorebird release branch check returns only a non-stable branch. + const FakeCommand( + command: [ + 'git', + 'for-each-ref', + '--contains', + 'HEAD', + '--format', + '%(refname:short)', + 'refs/remotes/origin/flutter_release/*', + ], + stdout: 'origin/flutter_release/3.41.4-rc2', + ), + // Falls through to tag-based fallback. + const FakeCommand( + command: [ + 'git', + 'for-each-ref', + '--sort=-v:refname', + '--count=1', + '--format=%(refname:short)', + 'refs/tags/[0-9]*.*.*', + ], + stdout: '3.41.4', + ), + const FakeCommand( + command: ['git', 'merge-base', 'HEAD', '3.41.4'], + stdout: headRevision, + ), + const FakeCommand( + command: ['git', 'rev-list', '--count', '$headRevision..HEAD'], + stdout: '48', + ), + ]); + final platform = FakePlatform(); + + final GitTagVersion gitTagVersion = GitTagVersion.determine( + platform, + git: git, + workingDirectory: '.', + ); + // Should NOT be 0.0.0-unknown; the tag fallback should produce a valid version. + expect(gitTagVersion.frameworkVersionFor(headRevision), '3.41.5-0.0.pre-48'); + expect(processManager, hasNoRemainingExpectations); + }); + + testUsingContext( + 'determine picks stable branch over rc branch from shorebird flutter_release', + () { + processManager.addCommands([ + const FakeCommand( + command: ['git', 'tag', '--points-at', 'HEAD'], + // No tag at HEAD. + ), + const FakeCommand( + command: [ + 'git', + 'for-each-ref', + '--contains', + 'HEAD', + '--format', + '%(refname:short)', + 'refs/remotes/origin/flutter_release/*', + ], + // Both stable and rc branches contain this commit. + stdout: 'origin/flutter_release/3.41.4\norigin/flutter_release/3.41.4-rc2', + ), + ]); + final platform = FakePlatform(); + + final GitTagVersion gitTagVersion = GitTagVersion.determine( + platform, + git: git, + workingDirectory: '.', + ); + expect(gitTagVersion.frameworkVersionFor('abcd1234'), '3.41.4'); + expect(processManager, hasNoRemainingExpectations); + }, + ); + group('$FlutterEngineStampFromFile', () { late FileSystem fs; const flutterRoot = '/path/to/flutter'; diff --git a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart index 195e5c29ef412..ba58f461f5b8d 100644 --- a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart +++ b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:io' hide Directory, File; +import 'dart:typed_data'; import 'package:dwds/dwds.dart'; import 'package:fake_async/fake_async.dart'; @@ -23,6 +24,7 @@ import 'package:flutter_tools/src/isolated/web_asset_server.dart'; import 'package:flutter_tools/src/isolated/web_server_utilities.dart'; import 'package:flutter_tools/src/web/compile.dart'; import 'package:flutter_tools/src/web/devfs_config.dart'; +import 'package:flutter_tools/src/web/module_metadata.dart'; import 'package:flutter_tools/src/web_template.dart'; import 'package:logging/logging.dart' as logging; import 'package:package_config/package_config.dart'; @@ -1803,6 +1805,167 @@ const config = { expect(body, contains('debug: true')); }), ); + + test( + 'DDC library bundle reloaded sources are relative paths', + () => testbed.run(() async { + // `reloadedSourcesUri` should itself be relative. + expect(WebAssetServer.reloadedSourcesUri.host, isEmpty); + + // Set up initial compile. + final File outputFile = globals.fs.file(globals.fs.path.join('lib', 'main.dart')) + ..createSync(recursive: true); + outputFile.parent.childFile('a.lib.js.sources').writeAsStringSync('main() {}'); + outputFile.parent.childFile('a.lib.js.json').writeAsStringSync('{}'); + outputFile.parent.childFile('a.lib.js.map').writeAsStringSync('{}'); + outputFile.parent.childFile('a.lib.js.metadata').writeAsStringSync('{}'); + + final residentCompiler = FakeResidentCompiler() + ..output = const CompilerOutput('a.lib.js', 0, []); + + const webDevServerConfig = WebDevServerConfig(); + final webDevFS = WebDevFS( + packagesFilePath: '.dart_tool/package_config.json', + urlTunneller: null, + useSseForDebugProxy: true, + useSseForDebugBackend: true, + useSseForInjectedClient: true, + nativeNullAssertions: true, + buildInfo: const BuildInfo( + BuildMode.debug, + '', + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', + ), + enableDwds: false, + ddsConfig: const DartDevelopmentServiceConfiguration(enable: false), + entrypoint: Uri.base, + testMode: true, + expressionCompiler: null, + chromiumLauncher: null, + // Use DDC library bundle. + ddcModuleSystem: true, + canaryFeatures: true, + webRenderer: WebRendererMode.canvaskit, + isWasm: false, + useLocalCanvasKit: false, + rootDirectory: globals.fs.currentDirectory, + webDevServerConfig: webDevServerConfig, + fileSystem: globals.fs, + logger: globals.logger, + platform: globals.platform, + webCrossOriginIsolation: false, + ); + webDevFS.ddcModuleLoaderJS.createSync(recursive: true); + webDevFS.flutterJs.createSync(recursive: true); + webDevFS.stackTraceMapper.createSync(recursive: true); + + await webDevFS.create(); + + webDevFS.webAssetServer.entrypointCacheDirectory = globals.fs.currentDirectory; + globals.fs.currentDirectory.childDirectory('lib').childFile('web_entrypoint.dart') + ..createSync(recursive: true) + ..writeAsStringSync('GENERATED'); + final String webPrecompiledCanvaskitSdk = globals.artifacts! + .getHostArtifact(HostArtifact.webPrecompiledDdcLibraryBundleCanvaskitSdk) + .path; + final String webPrecompiledCanvaskitSdkSourcemaps = globals.artifacts! + .getHostArtifact(HostArtifact.webPrecompiledDdcLibraryBundleCanvaskitSdkSourcemaps) + .path; + final String flutterJs = globals.fs.path.join( + globals.artifacts!.getHostArtifact(HostArtifact.flutterJsDirectory).path, + 'flutter.js', + ); + globals.fs.file(webPrecompiledCanvaskitSdk) + ..createSync(recursive: true) + ..writeAsStringSync('HELLO'); + globals.fs.file(webPrecompiledCanvaskitSdkSourcemaps) + ..createSync(recursive: true) + ..writeAsStringSync('THERE'); + globals.fs.file(flutterJs) + ..createSync(recursive: true) + ..writeAsStringSync('(flutter.js content)'); + + await webDevFS.update( + mainUri: outputFile.uri, + generator: residentCompiler, + trackWidgetCreation: true, + bundleFirstUpload: true, + invalidatedFiles: [], + packageConfig: PackageConfig.empty, + pathToReload: '', + dillOutputPath: '', + shaderCompiler: const FakeShaderCompiler(), + ); + + // Recompile with modules in the top-level and in a subdirectory. + const aSource = 'void main() {}'; + const bSource = 'void func() {}'; + final File sources = outputFile.parent.childFile('a.lib.js.sources') + ..writeAsStringSync('$aSource$bSource'); + outputFile.parent.childFile('a.lib.js.map').writeAsStringSync('{}{}'); + final String aMetadata = json.encode( + ModuleMetadata('a.lib.js', 'closure', 'a.map', 'a.lib.js') + ..addLibrary(LibraryMetadata('lib_a', 'dart:lib_a', ['lib_a.dart'])) + ..toJson(), + ); + final String bMetadata = json.encode( + ModuleMetadata('b.lib.js', 'closure', 'b.map', 'b.lib.js') + ..addLibrary(LibraryMetadata('lib_b', 'dart:lib_b', ['lib_b.dart'])) + ..toJson(), + ); + final File metadata = outputFile.parent.childFile('a.lib.js.metadata') + ..writeAsStringSync('$aMetadata$bMetadata'); + outputFile.parent + .childFile('a.lib.js.json') + .writeAsStringSync( + json.encode({ + 'a.lib.js': { + 'code': [0, aSource.length], + 'sourcemap': [0, 2], + 'metadata': [0, aMetadata.length], + }, + 'sub/b.lib.js': { + 'code': [aSource.length, sources.lengthSync()], + 'sourcemap': [2, 4], + 'metadata': [aMetadata.length, metadata.lengthSync()], + }, + }), + ); + + await webDevFS.update( + mainUri: outputFile.uri, + generator: residentCompiler, + trackWidgetCreation: true, + invalidatedFiles: [], + packageConfig: PackageConfig.empty, + pathToReload: '', + dillOutputPath: '', + shaderCompiler: const FakeShaderCompiler(), + ); + + final Uint8List? reloadedSources = webDevFS.webAssetServer.getFile( + WebAssetServer.reloadedSourcesUri.path, + ); + expect(reloadedSources, isNotNull); + expect(json.decode(utf8.decode(reloadedSources!)), [ + { + // The paths within `reloadedSources` should be relative with a root + // prefix. + 'src': '/a.lib.js', + 'module': 'a.lib.js', + 'libraries': ['dart:lib_a'], + }, + { + 'src': '/sub/b.lib.js', + 'module': 'b.lib.js', + 'libraries': ['dart:lib_b'], + }, + ]); + + await webDevFS.destroy(); + }, overrides: {Artifacts: () => Artifacts.test()}), + ); } class FakeHttpServer extends Fake implements HttpServer { diff --git a/packages/flutter_tools/test/general.shard/xcode_project_test.dart b/packages/flutter_tools/test/general.shard/xcode_project_test.dart index 9cb3ca6904e80..ad2e2f12a6ab3 100644 --- a/packages/flutter_tools/test/general.shard/xcode_project_test.dart +++ b/packages/flutter_tools/test/general.shard/xcode_project_test.dart @@ -2,11 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:file/memory.dart'; import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_system/build_system.dart'; @@ -19,6 +22,7 @@ import 'package:test/fake.dart'; import '../src/common.dart'; import '../src/context.dart'; +import '../src/fake_process_manager.dart'; import '../src/fakes.dart'; void main() { @@ -583,6 +587,207 @@ void main() { }, ); }); + + group('prefetchSwiftPackages', () { + testWithoutContext('starts the process and resolves packages successfully', () async { + final fs = MemoryFileSystem.test(); + final testLogger = BufferLogger.test(); + const projectPath = 'path/to/project'; + final Directory buildDirectory = fs.directory('$projectPath/build/ios'); + final fakeProcessManager = FakeProcessManager.empty(); + fakeProcessManager.addCommands([ + FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + '-resolvePackageDependencies', + ], + stdout: ''' +Resolve Package Graph + +Fetching from https://github.com/apple/swift-algorithms.git (cached) + +Fetching from https://github.com/apple/swift-numerics.git (cached) + +Creating working copy of package โ€˜swift-numericsโ€™ + +Creating working copy of package โ€˜swift-algorithmsโ€™ + +Checking out 1.2.2 of package โ€˜swift-algorithmsโ€™ + +Checking out 1.1.1 of package โ€˜swift-numericsโ€™ + + +Resolved source packages: + swift-numerics: https://github.com/apple/swift-numerics.git @ 1.1.1 + swift-algorithms: https://github.com/apple/swift-algorithms.git @ 1.2.2 +''', + ), + ]); + final processUtils = ProcessUtils(logger: testLogger, processManager: fakeProcessManager); + + final iosProject = FakeIosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + await iosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + expect( + testLogger.statusText, + contains(''' +Xcode is fetching Swift Package Manager dependencies. This may take several minutes... + Fetching from https://github.com/apple/swift-algorithms.git (cached)... + Fetching from https://github.com/apple/swift-numerics.git (cached)...'''), + ); + }); + + testWithoutContext('skips running if already completed or in progress', () async { + final fs = MemoryFileSystem.test(); + final testLogger = BufferLogger.test(); + const projectPath = 'path/to/project'; + final Directory buildDirectory = fs.directory('$projectPath/build/ios'); + final fakeProcessManager = FakeProcessManager.empty(); + fakeProcessManager.addCommands([ + FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + '-resolvePackageDependencies', + ], + ), + ]); + final processUtils = ProcessUtils(logger: testLogger, processManager: fakeProcessManager); + + final iosProject = FakeIosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + await iosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + + // Second call should skip starting a new process + await iosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + }); + + testWithoutContext('throws exception when resolving fails', () async { + final fs = MemoryFileSystem.test(); + final testLogger = BufferLogger.test(); + const projectPath = 'path/to/project'; + final Directory buildDirectory = fs.directory('$projectPath/build/ios'); + final fakeProcessManager = FakeProcessManager.empty(); + fakeProcessManager.addCommands([ + FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + '-resolvePackageDependencies', + ], + exitCode: 1, + stderr: 'error: some error', + ), + ]); + final processUtils = ProcessUtils(logger: testLogger, processManager: fakeProcessManager); + + final iosProject = FakeIosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + await expectLater( + iosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${buildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + ), + throwsToolExit(), + ); + }); + + testWithoutContext('prefetchSwiftPackages can run for both platforms', () async { + final fs = MemoryFileSystem.test(); + final testLogger = BufferLogger.test(); + const projectPath = 'path/to/project'; + final Directory iosBuildDirectory = fs.directory('$projectPath/build/ios'); + final Directory macosBuildDirectory = fs.directory('$projectPath/build/macos'); + + final fakeProcessManager = FakeProcessManager.empty(); + fakeProcessManager.addCommands([ + FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${iosBuildDirectory.path}/SourcePackages', + '-resolvePackageDependencies', + ], + ), + FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${macosBuildDirectory.path}/SourcePackages', + '-resolvePackageDependencies', + ], + ), + ]); + final processUtils = ProcessUtils(logger: testLogger, processManager: fakeProcessManager); + + final iosProject = FakeIosProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + await iosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${iosBuildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + ); + + final macosProject = FakeMacOSProject.fromFlutter(FakeFlutterProject(fileSystem: fs)); + await macosProject.prefetchSwiftPackages( + xcodebuildProjectCommandArguments: [ + 'xcrun', + 'xcodebuild', + '-clonedSourcePackagesDirPath', + '/${macosBuildDirectory.path}/SourcePackages', + ], + processUtils: processUtils, + logger: testLogger, + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + }); + }); }); } @@ -618,7 +823,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { @@ -665,3 +870,23 @@ class FakeCache extends Fake implements Cache { return olderThanToolsStamp; } } + +class FakeIosProject extends IosProject { + FakeIosProject.fromFlutter(super.parent) : super.fromFlutter(); + + @override + bool usesSwiftPackageManager = true; + + @override + bool flutterPluginSwiftPackageInProjectSettings = true; +} + +class FakeMacOSProject extends MacOSProject { + FakeMacOSProject.fromFlutter(super.parent) : super.fromFlutter(); + + @override + bool usesSwiftPackageManager = true; + + @override + bool flutterPluginSwiftPackageInProjectSettings = true; +} diff --git a/packages/flutter_tools/test/integration.shard/android_plugin_example_app_build_test.dart b/packages/flutter_tools/test/integration.shard/android_plugin_example_app_build_test.dart index f6e9643cd5924..44620b6e7431a 100644 --- a/packages/flutter_tools/test/integration.shard/android_plugin_example_app_build_test.dart +++ b/packages/flutter_tools/test/integration.shard/android_plugin_example_app_build_test.dart @@ -97,8 +97,7 @@ void main() { gradleProperties.writeAsStringSync(''' org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -android.useAndroidX=true -android.enableR8=true'''); +android.useAndroidX=true'''); // Run flutter build apk using AGP 3.3.0 result = processManager.runSync([ diff --git a/packages/flutter_tools/test/integration.shard/flutter_build_apk_split_per_abi_test.dart b/packages/flutter_tools/test/integration.shard/flutter_build_apk_split_per_abi_test.dart index 0c2ab881d8a1e..c1a272bef7987 100644 --- a/packages/flutter_tools/test/integration.shard/flutter_build_apk_split_per_abi_test.dart +++ b/packages/flutter_tools/test/integration.shard/flutter_build_apk_split_per_abi_test.dart @@ -21,7 +21,6 @@ import java.io.FileInputStream 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") } @@ -36,10 +35,6 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "com.example.splitperabi" @@ -60,6 +55,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + flutter { source = "../.." } diff --git a/packages/flutter_tools/test/integration.shard/gradle_libapp_so_packaging_test.dart b/packages/flutter_tools/test/integration.shard/gradle_libapp_so_packaging_test.dart new file mode 100644 index 0000000000000..d8119e787518a --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/gradle_libapp_so_packaging_test.dart @@ -0,0 +1,462 @@ +// 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. + +// Regression tests for `libapp.so` being dropped from the APK/app bundle, which +// surfaces either as a runtime "VM snapshot invalid" crash (APK) or as a +// "Release app bundle failed to strip debug symbols from native libraries" +// build failure (app bundle). +// +// Root cause: https://github.com/flutter/flutter/issues/186810 and +// https://github.com/flutter/flutter/issues/187388 (a regression from +// https://github.com/flutter/flutter/pull/181275, which moved `libapp.so` from +// a jar dependency onto a Flutter Gradle Plugin source-set `jniLibs` directory). +// +// These tests cover the two confirmed triggers: +// * Case A: a combined `subprojects { ... evaluationDependsOn(":app") }` block +// in the root `android/build.gradle.kts` together with a plugin whose Gradle +// subproject name sorts alphabetically before `:app`. This evaluates `:app` +// before its build directory is redirected. +// * Case B: a flavored project where a build for a single ABI (e.g. a prior +// `flutter run` on one device) leaves stale incremental state that drops +// `libapp.so` for the other ABIs on the next multi-ABI build. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:archive/archive.dart'; +import 'package:file/file.dart'; +import 'package:file_testing/file_testing.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; + +import '../src/common.dart'; +import 'test_utils.dart'; + +void main() { + late Directory tempDir; + + setUp(() { + tempDir = createResolvedTempDirectorySync('flutter_libapp_so_test.'); + }); + + tearDown(() { + tryToDelete(tempDir); + }); + + String getTestFileString(String probe) { + return ''' + import 'package:flutter/material.dart'; + + void main() { + print('$probe'); + runApp(const MyApp()); + } + + class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: Scaffold( + body: Center( + child: Text('Hello World'), + ), + ), + ); + } + } + '''; + } + + ArchiveFile? getLibAppFile(File apkFile) { + final List apkBytes = apkFile.readAsBytesSync(); + final Archive archive = ZipDecoder().decodeBytes(apkBytes); + return archive.findFile('lib/arm64-v8a/libapp.so') ?? + archive.findFile('lib/armeabi-v7a/libapp.so') ?? + archive.findFile('lib/x86_64/libapp.so'); + } + + test('libapp.so is packaged in an app bundle with a combined subprojects block ' + 'and a plugin sorted before ":app"', () async { + final Directory appDir = _createApp(tempDir); + + // A plugin whose Gradle subproject (":aaa_plugin") sorts before ":app", so + // the combined subprojects loop reaches it (and triggers + // evaluationDependsOn(":app")) before ":app"'s build dir is redirected. + _createPlugin(tempDir, 'aaa_plugin'); + _addPathDependency(appDir, 'aaa_plugin', tempDir.childDirectory('aaa_plugin')); + + _useCombinedSubprojectsBlock(appDir); + _disableR8(appDir); + + final ProcessResult result = processManager.runSync([ + flutterBin, + 'build', + 'appbundle', + '--release', + ], workingDirectory: appDir.path); + expect( + result.exitCode, + 0, + reason: 'flutter build appbundle --release failed:\n${result.stdout}\n${result.stderr}', + ); + + final List files = _appBundleFileList(appDir, _releaseBundle(appDir)); + for (final arch in ['arm64-v8a', 'armeabi-v7a', 'x86_64']) { + expect( + files, + contains('base/lib/$arch/libapp.so'), + reason: 'libapp.so missing for $arch in the app bundle', + ); + } + }); + + test('libapp.so is packaged for every ABI after a single-ABI build of a flavored app', () async { + final Directory appDir = _createApp(tempDir); + _addFlavors(appDir, ['prod']); + _disableR8(appDir); + + // Simulate a prior `flutter run` on a single-architecture device, which only + // builds `app.so` for that ABI. + final ProcessResult singleAbiBuild = processManager.runSync([ + flutterBin, + 'build', + 'apk', + '--release', + '--flavor', + 'prod', + '--target-platform', + 'android-arm64', + ], workingDirectory: appDir.path); + expect( + singleAbiBuild.exitCode, + 0, + reason: 'single-ABI build failed:\n${singleAbiBuild.stdout}\n${singleAbiBuild.stderr}', + ); + + // Now build for all ABIs. `libapp.so` must be present for all of them, not + // just the one built above. + final ProcessResult allAbiBuild = processManager.runSync([ + flutterBin, + 'build', + 'appbundle', + '--release', + '--flavor', + 'prod', + ], workingDirectory: appDir.path); + expect( + allAbiBuild.exitCode, + 0, + reason: 'multi-ABI build failed:\n${allAbiBuild.stdout}\n${allAbiBuild.stderr}', + ); + + final List files = _appBundleFileList( + appDir, + appDir + .childDirectory('build') + .childDirectory('app') + .childDirectory('outputs') + .childDirectory('bundle') + .childDirectory('prodRelease') + .childFile('app-prod-release.aab'), + ); + for (final arch in ['arm64-v8a', 'armeabi-v7a', 'x86_64']) { + expect( + files, + contains('base/lib/$arch/libapp.so'), + reason: 'libapp.so missing for $arch after a single-ABI build preceded the multi-ABI build', + ); + } + }); + + // Reproduction test for https://github.com/flutter/flutter/issues/187553 + testWithoutContext('mergeJniLibFolders runs when Dart AOT source changes with flavors', () async { + final Directory projectDir = tempDir.childDirectory('app'); + + // 1. Create a Flutter app template. + final ProcessResult createResult = processManager.runSync([ + flutterBin, + 'create', + '--template=app', + '--platforms=android', + 'app', + ], workingDirectory: tempDir.path); + expect(createResult, const ProcessResultMatcher()); + + // 2. Add product flavors to build.gradle.kts. + final File buildGradleFile = projectDir + .childDirectory('android/app') + .childFile('build.gradle.kts'); + expect(buildGradleFile, exists); + String buildGradleContents = buildGradleFile.readAsStringSync(); + buildGradleContents = buildGradleContents.replaceFirst('android {', ''' +android { + flavorDimensions += "default" + productFlavors { + create("prod") { + dimension = "default" + } + create("dev") { + dimension = "default" + } + }'''); + buildGradleFile.writeAsStringSync(buildGradleContents); + + const PROBE_A = 'probe-AAAA'; + const PROBE_B = 'probe-BBBB'; + // 3. Configure main.dart with probe-AAAA. + final File mainDartFile = projectDir.childDirectory('lib').childFile('main.dart'); + expect(mainDartFile, exists); + mainDartFile.writeAsStringSync(getTestFileString(PROBE_A)); + + // 4. Build APK with flavor prod (Build 1). + final ProcessResult build1Result = processManager.runSync([ + flutterBin, + 'build', + 'apk', + '--release', + '--flavor', + 'prod', + ], workingDirectory: projectDir.path); + expect(build1Result, const ProcessResultMatcher()); + + // Verify APK contains probe-AAAA. + final File apkFile = projectDir + .childDirectory('build/app/outputs/flutter-apk') + .childFile('app-prod-release.apk'); + expect(apkFile, exists); + + ArchiveFile? libappFile = getLibAppFile(apkFile); + expect(libappFile, isNotNull); + String libappContent = latin1.decode(libappFile!.content as List, allowInvalid: true); + expect(libappContent.contains(PROBE_A), isTrue); + expect(libappContent.contains(PROBE_B), isFalse); + + // 5. Change main.dart to probe-BBBB. + mainDartFile.writeAsStringSync(getTestFileString(PROBE_B)); + + // 6. Build APK with flavor prod (Build 2 - incremental). + final ProcessResult build2Result = processManager.runSync([ + flutterBin, + 'build', + 'apk', + '--release', + '--flavor', + 'prod', + ], workingDirectory: projectDir.path); + expect(build2Result, const ProcessResultMatcher()); + + // Verify APK contains probe-BBBB and NOT probe-AAAA. + libappFile = getLibAppFile(apkFile); + expect(libappFile, isNotNull); + libappContent = latin1.decode(libappFile!.content as List, allowInvalid: true); + expect(libappContent.contains('probe-BBBB'), isTrue); + expect(libappContent.contains('probe-AAAA'), isFalse); + }); + + // Regression test for https://github.com/flutter/flutter/issues/188785. + // + // Building the androidTest variant directly (as flutter/packages' Firebase Test + // Lab tooling does) configures Gradle without a Flutter compile task: the single + // `assembleAndroidTest` CLI task makes `shouldConfigureFlutterTask` return false, + // so `compileFlutterBuild` is never registered. The jniLibs copy task + // wired via the variant API must tolerate that (absent) compile task rather than + // failing with "Task with name 'compileFlutterBuildDebug' not found". + testWithoutContext( + 'app:assembleAndroidTest builds when no Flutter compile task is configured', + () async { + final Directory appDir = _createApp(tempDir); + + // Configure Gradle without running the Flutter build (no `flutter assemble`). + final ProcessResult configResult = processManager.runSync([ + flutterBin, + 'build', + 'apk', + '--debug', + '--config-only', + ], workingDirectory: appDir.path); + expect(configResult, const ProcessResultMatcher()); + + final Directory androidDir = appDir.childDirectory('android'); + final File gradlew = androidDir.childFile(Platform.isWindows ? 'gradlew.bat' : 'gradlew'); + final ProcessResult assembleResult = processManager.runSync([ + gradlew.path, + 'app:assembleAndroidTest', + '-Pverbose=true', + ], workingDirectory: androidDir.path); + expect( + assembleResult.exitCode, + 0, + reason: + 'gradlew app:assembleAndroidTest failed:\n${assembleResult.stdout}\n${assembleResult.stderr}', + ); + }, + ); +} + +Directory _createApp(Directory workingDir) { + final ProcessResult result = processManager.runSync([ + flutterBin, + 'create', + '--template=app', + '--platforms=android', + 'app', + ], workingDirectory: workingDir.path); + if (result.exitCode != 0) { + throw StateError('flutter create app failed:\n${result.stdout}\n${result.stderr}'); + } + return workingDir.childDirectory('app'); +} + +void _createPlugin(Directory workingDir, String name) { + final ProcessResult result = processManager.runSync([ + flutterBin, + 'create', + '--template=plugin', + '--platforms=android', + name, + ], workingDirectory: workingDir.path); + if (result.exitCode != 0) { + throw StateError('flutter create plugin failed:\n${result.stdout}\n${result.stderr}'); + } +} + +void _addPathDependency(Directory appDir, String name, Directory packageDir) { + final File pubspec = appDir.childFile('pubspec.yaml'); + final String contents = pubspec.readAsStringSync(); + // Insert the path dependency immediately under the `dependencies:` key. + final String updated = contents.replaceFirst( + RegExp(r'^dependencies:\s*$', multiLine: true), + 'dependencies:\n $name:\n path: ${packageDir.path.replaceAll(r'\', '/')}', + ); + if (updated == contents) { + throw StateError('Failed to add path dependency to ${pubspec.path}'); + } + pubspec.writeAsStringSync(updated); +} + +/// Rewrites the two separate `subprojects { ... }` blocks generated by the app +/// template into the single combined block that triggers the regression. +void _useCombinedSubprojectsBlock(Directory appDir) { + final File buildGradle = appDir.childDirectory('android').childFile('build.gradle.kts'); + final String contents = buildGradle.readAsStringSync().replaceAll('\r\n', '\n'); + const separateBlocks = ''' +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +}'''; + const combinedBlock = ''' +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) + project.evaluationDependsOn(":app") +}'''; + if (!contents.contains(separateBlocks)) { + throw StateError( + 'Could not find the expected separate subprojects blocks in ${buildGradle.path}.\n' + 'Template may have changed; update this test.', + ); + } + buildGradle.writeAsStringSync(contents.replaceFirst(separateBlocks, combinedBlock)); +} + +void _addFlavors(Directory appDir, List flavors) { + final File buildGradle = appDir + .childDirectory('android') + .childDirectory('app') + .childFile('build.gradle.kts'); + final String contents = buildGradle.readAsStringSync(); + final buffer = StringBuffer() + ..writeln(' flavorDimensions += "env"') + ..writeln(' productFlavors {'); + for (final flavor in flavors) { + buffer + ..writeln(' create("$flavor") {') + ..writeln(' dimension = "env"') + ..writeln(' }'); + } + buffer.writeln(' }'); + // Insert the product flavors at the start of the `android { ... }` block. + final String updated = contents.replaceFirst( + RegExp(r'^android\s*\{', multiLine: true), + 'android {\n$buffer', + ); + if (updated == contents) { + throw StateError('Failed to add product flavors to ${buildGradle.path}'); + } + buildGradle.writeAsStringSync(updated); +} + +void _disableR8(Directory appDir) { + final File buildGradle = appDir + .childDirectory('android') + .childDirectory('app') + .childFile('build.gradle.kts'); + final String contents = buildGradle.readAsStringSync(); + final String updated = contents.replaceFirst( + 'signingConfig = signingConfigs.getByName("debug")', + 'signingConfig = signingConfigs.getByName("debug")\n isMinifyEnabled = false\n isShrinkResources = false', + ); + if (updated == contents) { + throw StateError('Failed to disable R8 in ${buildGradle.path}'); + } + buildGradle.writeAsStringSync(updated); +} + +File _releaseBundle(Directory appDir) => appDir + .childDirectory('build') + .childDirectory('app') + .childDirectory('outputs') + .childDirectory('bundle') + .childDirectory('release') + .childFile('app-release.aab'); + +/// Returns the file entries inside [appBundle] using `apkanalyzer files list`. +List _appBundleFileList(Directory appDir, File appBundle) { + if (!appBundle.existsSync()) { + throw StateError('App bundle not found at ${appBundle.path}'); + } + final File localProperties = appDir.childDirectory('android').childFile('local.properties'); + final RegExpMatch? match = RegExp( + r'sdk\.dir=(.+)', + ).firstMatch(localProperties.readAsStringSync()); + final String sdkPath = match?.group(1)?.trim() ?? ''; + if (sdkPath.isEmpty) { + throw StateError('SDK path not found in ${localProperties.path}'); + } + final String apkAnalyzer = fileSystem + .directory(sdkPath) + .childDirectory('cmdline-tools') + .childDirectory('latest') + .childDirectory('bin') + .childFile(Platform.isWindows ? 'apkanalyzer.bat' : 'apkanalyzer') + .path; + + final ProcessResult result = processManager.runSync([ + apkAnalyzer, + 'files', + 'list', + appBundle.path, + ]); + if (result.exitCode != 0) { + throw ProcessException( + apkAnalyzer, + ['files', 'list', appBundle.path], + 'apkanalyzer failed:\n${result.stderr}', + result.exitCode, + ); + } + // apkanalyzer prints entries like `/base/lib/arm64-v8a/libapp.so`; normalize + // by trimming the leading slash. + return result.stdout + .toString() + .split('\n') + .map((String line) => line.trim()) + .where((String line) => line.isNotEmpty) + .map((String line) => line.startsWith('/') ? line.substring(1) : line) + .toList(); +} diff --git a/packages/flutter_tools/test/integration.shard/isolated/native_assets_without_cbuild_assemble_test.dart b/packages/flutter_tools/test/integration.shard/isolated/native_assets_without_cbuild_assemble_test.dart index 37dad8abb516d..098b3e811af60 100644 --- a/packages/flutter_tools/test/integration.shard/isolated/native_assets_without_cbuild_assemble_test.dart +++ b/packages/flutter_tools/test/integration.shard/isolated/native_assets_without_cbuild_assemble_test.dart @@ -54,6 +54,14 @@ void main() { codeSign: buildCommand != 'ios', ); } + + if (platform.isLinux) { + _testBuildBundle( + targetPlatform: 'linux-x64', + processManager: processManager, + hooksVersionConstraint: constraint, + ); + } } void _testBuildCommand({ @@ -150,3 +158,69 @@ String _getPackageFfiTemplatePubspecVersion() { final version = dependencies['hooks']! as String; return version; } + +void _testBuildBundle({ + required String targetPlatform, + required String hooksVersionConstraint, + required ProcessManager processManager, +}) { + testWithoutContext( + 'flutter build bundle --target-platform=$targetPlatform succeeds without libraries', + () async { + await inTempDir((Directory tempDirectory) async { + const packageName = 'uses_package_hooks'; + + // Create a new (plain Dart SDK) project. + await expectLater( + processManager.run([ + flutterBin, + 'create', + '--no-pub', + packageName, + ], workingDirectory: tempDirectory.path), + completion(const ProcessResultMatcher()), + ); + + final Directory packageDirectory = tempDirectory.childDirectory(packageName); + + // Add hooks and resolve implicitly (pub add does pub get). + await expectLater( + processManager.run([ + flutterBin, + 'packages', + 'add', + 'hooks:$hooksVersionConstraint', + ], workingDirectory: packageDirectory.path), + completion(const ProcessResultMatcher()), + ); + + // Add a build hook that does nothing to the package. + packageDirectory.childDirectory('hook').childFile('build.dart') + ..createSync(recursive: true) + ..writeAsStringSync(''' +import 'package:hooks/hooks.dart'; + +void main(List args) async { + await build(args, (config, output) async {}); +} +'''); + + // Try building bundle. + final args = [ + flutterBin, + 'build', + 'bundle', + '--target-platform=$targetPlatform', + '--debug', + ]; + io.stderr.writeln('Running $args...'); + final io.Process process = await processManager.start( + args, + workingDirectory: packageDirectory.path, + mode: ProcessStartMode.inheritStdio, + ); + expect(await process.exitCode, 0); + }); + }, + ); +} diff --git a/packages/flutter_tools/test/integration.shard/swift_package_manager_add2app_test.dart b/packages/flutter_tools/test/integration.shard/swift_package_manager_add2app_test.dart index c2ef7582eb084..a5b3a954ef5cb 100644 --- a/packages/flutter_tools/test/integration.shard/swift_package_manager_add2app_test.dart +++ b/packages/flutter_tools/test/integration.shard/swift_package_manager_add2app_test.dart @@ -741,10 +741,8 @@ Future _verifyCodeSigning({ buildDir.childDirectory(framework).path, ]); final output = 'stdout: ${result.stdout}\nstderr: ${result.stderr}'; - expect( - output, - contains('Signature=adhoc'), - reason: '$framework not codesigned correctly: \n$output', - ); + final bool isCodesigned = + output.contains('Signature=adhoc') || output.contains('Signature size='); + expect(isCodesigned, isTrue, reason: '$framework not codesigned correctly: \n$output'); } } diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart index 4c0d5140f483c..ea16c7fd33e37 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart @@ -160,7 +160,7 @@ void main() { }); testUsingContext("doesn't crash on flutter clean", () async { - // Regression test for https://github.com/flutter/flutter/issues/175058.\ + // Regression test for https://github.com/flutter/flutter/issues/175058. dtdLauncher = DtdLauncher( logger: logger!, artifacts: globals.artifacts!, @@ -171,9 +171,6 @@ void main() { final Uri dtdUri = await dtdLauncher!.launch(); // Connect to it and listen to the WidgetPreviewScaffold stream. - // - // The preview scaffold will send a 'Connected' event on this stream once it has initialized - // and is ready. final DartToolingDaemon dtdConnection = await DartToolingDaemon.connect(dtdUri); final completer = Completer(); var firstConnection = true; @@ -185,26 +182,46 @@ void main() { if (firstConnection) { firstConnection = false; runFlutterClean(tempDir); + // Restore dependencies so the Analysis Server and Dwds can resolve packages during Hot Restart. + // Add --offline flag to prevent attempts to reach the network during CI. + processManager.runSync([ + flutterBin, + 'pub', + 'get', + '--offline', + ], workingDirectory: tempDir.path); dtdConnection.call( WidgetPreviewDtdServices.kWidgetPreviewServiceRoot, WidgetPreviewDtdServices.kHotRestartPreviewer, ); return; } - // The second `Connected` event should come after the previewer is hot restarted after - // we perform the `flutter clean`. This event won't be sent again if the previewer has - // crashed. + // The second `Connected` event should come after the previewer is hot restarted successfully. completer.complete(); }); await dtdConnection.streamListen(WidgetPreviewDtdServices.kWidgetPreviewScaffoldStreamRoot); // Start the widget preview and wait for the 'Connected' event. - await runWidgetPreview( + final Stream stdoutStream = await runWidgetPreview( tempDir: tempDir, expectedMessages: firstLaunchMessagesWeb, dtdUri: dtdUri, ); - await completer.future; + + final processExitCompleter = Completer(); + final StreamSubscription subscription = stdoutStream.listen( + null, + onDone: () => processExitCompleter.complete(), + ); + + // Race the connection completer against process exit for clearer failure messages. + await Future.any(>[completer.future, processExitCompleter.future]); + expect( + processExitCompleter.isCompleted, + isFalse, + reason: 'Widget preview process exited unexpectedly after flutter clean.', + ); + await subscription.cancel(); }); }); } diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart b/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart index fec1b4d124dae..9f505a7660c5a 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart @@ -59,9 +59,23 @@ Future> startWidgetPreview({ if (legacyPreviewDetection) '--legacy-preview-detection', ], workingDirectory: tempDir.path); + addTearDown(() async { + process.kill(); + await process.exitCode; + }); + final controller = StreamController.broadcast(); process.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((String msg) { - controller.add(msg); + // ignore: avoid_print + print('[stdout] $msg'); + if (!controller.isClosed) { + controller.add(msg); + } + }); + + process.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen((String msg) { + // ignore: avoid_print + print('[stderr] $msg'); }); unawaited( diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart index df194d1f6fe1a..5e31080219782 100644 --- a/packages/flutter_tools/test/src/context.dart +++ b/packages/flutter_tools/test/src/context.dart @@ -367,7 +367,7 @@ class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter { @override Future> getBuildSettings( - String projectPath, { + XcodeBasedProject xcodeProject, { XcodeProjectBuildContext? buildContext, Duration timeout = const Duration(minutes: 1), }) async { @@ -384,6 +384,7 @@ class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter { @override Future cleanWorkspace( + XcodeBasedProject xcodeProject, String workspacePath, String scheme, { required Directory buildDirectory, @@ -392,7 +393,7 @@ class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter { @override Future getInfo( - String projectPath, { + XcodeBasedProject xcodeProject, { String? projectFilename, required Directory buildDirectory, }) async { @@ -405,18 +406,16 @@ class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter { List xcrunCommand() => ['xcrun']; @override - Future prefetchSwiftPackages( - String projectPath, { + Future prefetchSwiftPackagesForProject( + XcodeBasedProject xcodeProject, { required Directory buildDirectory, - bool quiet = true, - bool waitForCompletion = true, }) async {} @override - Future> xcodebuildProjectCommand( - String projectPath, + Future> fetchDependenciesAndGenerateXcodebuildArgs( + XcodeBasedProject xcodeProject, Directory buildDirectory, { - bool skipPackageResolution = true, + bool skipPackageUpdatesAndValidation = true, }) async { return ['xcrun', 'xcodebuild']; } diff --git a/packages/integration_test/android/build.gradle.kts b/packages/integration_test/android/build.gradle.kts index f6aae8f374749..289daa5ff1c96 100644 --- a/packages/integration_test/android/build.gradle.kts +++ b/packages/integration_test/android/build.gradle.kts @@ -55,7 +55,7 @@ android { api("androidx.test:runner:1.2+") api("androidx.test:rules:1.2+") - api("androidx.test.espresso:espresso-core:3.2+") + api("androidx.test.espresso:espresso-core:3.3+") implementation("com.google.guava:guava:28.1-android") } diff --git a/packages/integration_test/example/android/project-app.lockfile b/packages/integration_test/example/android/project-app.lockfile index 346c52a71a0a7..7ae565996e1da 100644 --- a/packages/integration_test/example/android/project-app.lockfile +++ b/packages/integration_test/example/android/project-app.lockfile @@ -32,16 +32,16 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugCompileClassp androidx.profileinstaller:profileinstaller:1.3.1=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.test.espresso:espresso-core:3.6.1=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.test.espresso:espresso-idling-resource:3.6.1=androidTestImplementationDependenciesMetadata androidx.test.services:storage:1.5.0=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath androidx.test:core:1.6.1=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath -androidx.test:monitor:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.test:monitor:1.7.2=androidTestImplementationDependenciesMetadata androidx.test:rules:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath androidx.test:runner:1.6.2=androidTestImplementationDependenciesMetadata,debugAndroidTestCompileClasspath androidx.tracing:tracing:1.1.0=androidTestImplementationDependenciesMetadata androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -169,7 +169,7 @@ junit:junit:4.12=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRun junit:junit:4.13.2=androidTestImplementationDependenciesMetadata,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath,testImplementationDependenciesMetadata net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib org.bouncycastle:bcpg-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcpkix-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcprov-jdk18on:1.80=kotlinBouncyCastleConfiguration diff --git a/packages/integration_test/example/android/project-integration_test.lockfile b/packages/integration_test/example/android/project-integration_test.lockfile index aa42451a6a92a..b4fb0d394a0e3 100644 --- a/packages/integration_test/example/android/project-integration_test.lockfile +++ b/packages/integration_test/example/android/project-integration_test.lockfile @@ -30,11 +30,11 @@ androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRu androidx.profileinstaller:profileinstaller:1.3.1=debugAndroidTestRuntimeClasspath,debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.2.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.startup:startup-runtime:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-core:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test.espresso:espresso-idling-resource:3.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:monitor:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:monitor:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.test:rules:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.test:runner:1.2.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:runner:1.3.0=apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugApiDependenciesMetadata,debugCompileClasspath,debugImplementationDependenciesMetadata,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileApiDependenciesMetadata,profileCompileClasspath,profileImplementationDependenciesMetadata,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseApiDependenciesMetadata,releaseCompileClasspath,releaseImplementationDependenciesMetadata,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath @@ -161,7 +161,7 @@ net.bytebuddy:byte-buddy-agent:1.14.10=debugUnitTestCompileClasspath,debugUnitTe net.bytebuddy:byte-buddy:1.14.10=debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath,testImplementationDependenciesMetadata net.java.dev.jna:jna-platform:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle net.java.dev.jna:jna:5.6.0=_internal-unified-test-platform-android-device-provider-ddmlib,_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-result-listener-gradle -net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib,apiDependenciesMetadata,debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,implementationDependenciesMetadata,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.sf.kxml:kxml2:2.3.0=_internal-unified-test-platform-android-device-provider-ddmlib org.bouncycastle:bcpg-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcpkix-jdk18on:1.80=kotlinBouncyCastleConfiguration org.bouncycastle:bcprov-jdk18on:1.80=kotlinBouncyCastleConfiguration diff --git a/packages/shorebird_tests/.gitignore b/packages/shorebird_tests/.gitignore new file mode 100644 index 0000000000000..3a85790408401 --- /dev/null +++ b/packages/shorebird_tests/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/packages/shorebird_tests/README.md b/packages/shorebird_tests/README.md new file mode 100644 index 0000000000000..b87fb24a480da --- /dev/null +++ b/packages/shorebird_tests/README.md @@ -0,0 +1,2 @@ +A dart project that includes tests that perform asserts in the modifications +made on the Flutter framework by the Shorebird team. diff --git a/packages/shorebird_tests/analysis_options.yaml b/packages/shorebird_tests/analysis_options.yaml new file mode 100644 index 0000000000000..a767d79d7f4b1 --- /dev/null +++ b/packages/shorebird_tests/analysis_options.yaml @@ -0,0 +1,2 @@ +# This file configures the static analysis results for your project (errors, +include: package:lints/recommended.yaml diff --git a/packages/shorebird_tests/pubspec.yaml b/packages/shorebird_tests/pubspec.yaml new file mode 100644 index 0000000000000..d0fba0c1fa95d --- /dev/null +++ b/packages/shorebird_tests/pubspec.yaml @@ -0,0 +1,16 @@ +name: shorebird_tests +description: Shorebird's Flutter customizations tests +version: 1.0.0 + +environment: + sdk: ^3.3.4 + +dependencies: + archive: ^3.5.1 + path: ^1.9.0 + yaml: ^3.1.2 + +dev_dependencies: + lints: ^3.0.0 + meta: ^1.15.0 + test: ^1.24.0 diff --git a/packages/shorebird_tests/test/android_test.dart b/packages/shorebird_tests/test/android_test.dart new file mode 100644 index 0000000000000..1e0a6da4f3824 --- /dev/null +++ b/packages/shorebird_tests/test/android_test.dart @@ -0,0 +1,94 @@ +import 'package:test/test.dart'; + +import 'shorebird_tests.dart'; + +void main() { + setUpAll(warmUpTemplateProject); + + group('shorebird android projects', () { + testWithShorebirdProject('can build an apk', (projectDirectory) async { + await projectDirectory.runFlutterBuildApk(); + + expect(projectDirectory.apkFile().existsSync(), isTrue); + expect(projectDirectory.shorebirdFile.existsSync(), isTrue); + expect(projectDirectory.getGeneratedAndroidShorebirdYaml(), completes); + }); + + group('when passing the public key through the environment variable', () { + testWithShorebirdProject( + 'adds the public key on top of the original file', + (projectDirectory) async { + final originalYaml = projectDirectory.shorebirdYaml; + + const base64PublicKey = 'public_123'; + await projectDirectory.runFlutterBuildApk( + environment: { + 'SHOREBIRD_PUBLIC_KEY': base64PublicKey, + }, + ); + + final generatedYaml = + await projectDirectory.getGeneratedAndroidShorebirdYaml(); + + expect( + generatedYaml.keys, + containsAll(originalYaml.keys), + ); + + expect( + generatedYaml['patch_public_key'], + equals(base64PublicKey), + ); + }, + ); + }); + + group('when building with a flavor', () { + testWithShorebirdProject( + 'correctly changes the app id', + (projectDirectory) async { + await projectDirectory.addProjectFlavors(); + projectDirectory.addShorebirdFlavors(); + + await projectDirectory.runFlutterBuildApk(flavor: 'internal'); + + final generatedYaml = + await projectDirectory.getGeneratedAndroidShorebirdYaml( + flavor: 'internal', + ); + + expect(generatedYaml['app_id'], equals('internal_123')); + }, + ); + + group('when public key passed through environment variable', () { + testWithShorebirdProject( + 'correctly changes the app id and adds the public key', + (projectDirectory) async { + const base64PublicKey = 'public_123'; + await projectDirectory.addProjectFlavors(); + projectDirectory.addShorebirdFlavors(); + + await projectDirectory.runFlutterBuildApk( + flavor: 'internal', + environment: { + 'SHOREBIRD_PUBLIC_KEY': base64PublicKey, + }, + ); + + final generatedYaml = + await projectDirectory.getGeneratedAndroidShorebirdYaml( + flavor: 'internal', + ); + + expect(generatedYaml['app_id'], equals('internal_123')); + expect( + generatedYaml['patch_public_key'], + equals(base64PublicKey), + ); + }, + ); + }); + }); + }); +} diff --git a/packages/shorebird_tests/test/base_test.dart b/packages/shorebird_tests/test/base_test.dart new file mode 100644 index 0000000000000..ce9b0564b1396 --- /dev/null +++ b/packages/shorebird_tests/test/base_test.dart @@ -0,0 +1,17 @@ +import 'package:test/test.dart'; + +import 'shorebird_tests.dart'; + +void main() { + setUpAll(warmUpTemplateProject); + + group('shorebird helpers', () { + testWithShorebirdProject('can build a base project', + (projectDirectory) async { + expect(projectDirectory.existsSync(), isTrue); + + expect(projectDirectory.pubspecFile.existsSync(), isTrue); + expect(projectDirectory.shorebirdFile.existsSync(), isTrue); + }); + }); +} diff --git a/packages/shorebird_tests/test/ios_test.dart b/packages/shorebird_tests/test/ios_test.dart new file mode 100644 index 0000000000000..bb694405ae946 --- /dev/null +++ b/packages/shorebird_tests/test/ios_test.dart @@ -0,0 +1,93 @@ +import 'package:test/test.dart'; + +import 'shorebird_tests.dart'; + +void main() { + setUpAll(warmUpTemplateProject); + + group( + 'shorebird ios projects', + () { + testWithShorebirdProject('can build', (projectDirectory) async { + await projectDirectory.runFlutterBuildIos(); + + expect(projectDirectory.iosArchiveFile().existsSync(), isTrue); + expect(projectDirectory.getGeneratedIosShorebirdYaml(), completes); + }); + + group('when passing the public key through the environment variable', () { + testWithShorebirdProject( + 'adds the public key on top of the original file', + (projectDirectory) async { + final originalYaml = projectDirectory.shorebirdYaml; + + const base64PublicKey = 'public_123'; + await projectDirectory.runFlutterBuildIos( + environment: { + 'SHOREBIRD_PUBLIC_KEY': base64PublicKey, + }, + ); + + final generatedYaml = + await projectDirectory.getGeneratedIosShorebirdYaml(); + + expect( + generatedYaml.keys, + containsAll(originalYaml.keys), + ); + + expect( + generatedYaml['patch_public_key'], + equals(base64PublicKey), + ); + }, + ); + }); + + group('when building with a flavor', () { + testWithShorebirdProject( + 'correctly changes the app id', + (projectDirectory) async { + await projectDirectory.addProjectFlavors(); + projectDirectory.addShorebirdFlavors(); + + await projectDirectory.runFlutterBuildIos(flavor: 'internal'); + + final generatedYaml = + await projectDirectory.getGeneratedIosShorebirdYaml(); + + expect(generatedYaml['app_id'], equals('internal_123')); + }, + ); + + group('when public key passed through environment variable', () { + testWithShorebirdProject( + 'correctly changes the app id and adds the public key', + (projectDirectory) async { + const base64PublicKey = 'public_123'; + await projectDirectory.addProjectFlavors(); + projectDirectory.addShorebirdFlavors(); + + await projectDirectory.runFlutterBuildIos( + flavor: 'internal', + environment: { + 'SHOREBIRD_PUBLIC_KEY': base64PublicKey, + }, + ); + + final generatedYaml = + await projectDirectory.getGeneratedIosShorebirdYaml(); + + expect(generatedYaml['app_id'], equals('internal_123')); + expect( + generatedYaml['patch_public_key'], + equals(base64PublicKey), + ); + }, + ); + }); + }); + }, + testOn: 'mac-os', + ); +} diff --git a/packages/shorebird_tests/test/shorebird_tests.dart b/packages/shorebird_tests/test/shorebird_tests.dart new file mode 100644 index 0000000000000..cfcda4c5b4603 --- /dev/null +++ b/packages/shorebird_tests/test/shorebird_tests.dart @@ -0,0 +1,424 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:archive/archive_io.dart'; +import 'package:path/path.dart' as path; + +import 'package:meta/meta.dart'; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; + +/// This will be the path to the flutter binary housed in this flutter repository. +/// +/// Which since we are running the tests from this inner package , we need to go up two directories +/// in order to find the flutter binary in the bin folder. +File get _flutterBinaryFile => File( + path.join( + Directory.current.path, + '..', + '..', + 'bin', + 'flutter${Platform.isWindows ? '.bat' : ''}', + ), + ); + +/// Whether to print line-by-line subprocess output. +/// +/// Set the `VERBOSE` environment variable to enable streaming output, +/// which is useful for debugging timeouts in CI. +final bool _verbose = Platform.environment.containsKey('VERBOSE'); + +/// Runs a flutter command using the correct binary ([_flutterBinaryFile]) with the given arguments. +/// +/// Streams stdout and stderr to the test output in real time when [_verbose] +/// is true, so CI logs show progress even if the process hangs or times out. +Future _runFlutterCommand( + List arguments, { + required Directory workingDirectory, + Map? environment, +}) async { + final String command = 'flutter ${arguments.join(' ')}'; + print('[$command] starting...'); + final stopwatch = Stopwatch()..start(); + + final Process process = await Process.start( + _flutterBinaryFile.absolute.path, + arguments, + workingDirectory: workingDirectory.path, + environment: { + 'FLUTTER_STORAGE_BASE_URL': 'https://download.shorebird.dev', + if (environment != null) ...environment, + }, + ); + + final StringBuffer stdoutBuffer = StringBuffer(); + final StringBuffer stderrBuffer = StringBuffer(); + + process.stdout.transform(utf8.decoder).listen((String data) { + stdoutBuffer.write(data); + if (_verbose) { + for (final String line in data.split('\n')) { + if (line.isNotEmpty) { + print(' [$command] $line'); + } + } + } + }); + + process.stderr.transform(utf8.decoder).listen((String data) { + stderrBuffer.write(data); + if (_verbose) { + for (final String line in data.split('\n')) { + if (line.isNotEmpty) { + print(' [$command] (stderr) $line'); + } + } + } + }); + + final int exitCode = await process.exitCode; + stopwatch.stop(); + print('[$command] completed in ${stopwatch.elapsed} ' + '(exit code $exitCode)'); + if (exitCode != 0 && !_verbose) { + print('[$command] stdout:\n$stdoutBuffer'); + print('[$command] stderr:\n$stderrBuffer'); + } + + return ProcessResult( + process.pid, + exitCode, + stdoutBuffer.toString(), + stderrBuffer.toString(), + ); +} + +Future _createFlutterProject(Directory projectDirectory) async { + final result = await _runFlutterCommand( + ['create', '--empty', '.'], + workingDirectory: projectDirectory, + ); + if (result.exitCode != 0) { + throw Exception('Failed to create Flutter project: ${result.stderr}'); + } +} + +/// Cached template project directory, created once and reused across tests. +/// +/// This avoids running `flutter create` for every test, which saves +/// significant time (especially the first Gradle/SDK download). +Directory? _templateProject; + +/// Creates (or returns the cached) template Flutter project with +/// shorebird.yaml configured. The first call runs `flutter create` and +/// `flutter build apk` to warm up Gradle caches. +/// +/// Call this from `setUpAll` so the expensive setup runs outside per-test +/// timeouts. +Future warmUpTemplateProject() => _getTemplateProject(); + +Future _getTemplateProject() async { + if (_templateProject != null) { + return _templateProject!; + } + + final Directory templateDir = Directory( + path.join(Directory.systemTemp.createTempSync().path, 'shorebird_template'), + )..createSync(); + + await _createFlutterProject(templateDir); + + templateDir.pubspecFile.writeAsStringSync(''' +${templateDir.pubspecFile.readAsStringSync()} + assets: + - shorebird.yaml +'''); + + File( + path.join(templateDir.path, 'shorebird.yaml'), + ).writeAsStringSync(''' +app_id: "123" +'''); + + // Warm up the Gradle cache with a throwaway build so subsequent + // per-test builds are fast and don't hit the per-test timeout. + // Skip if Gradle cache is already populated (e.g., from GHA cache restore). + final Directory gradleCache = Directory( + path.join(Platform.environment['HOME'] ?? '', '.gradle', 'caches'), + ); + final bool hasGradleCache = + gradleCache.existsSync() && gradleCache.listSync().isNotEmpty; + if (hasGradleCache) { + print('[warmup] Gradle cache exists, skipping warm-up build'); + } else { + await _runFlutterCommand( + ['build', 'apk'], + workingDirectory: templateDir, + ); + } + + _templateProject = templateDir; + return templateDir; +} + +/// Copies the template project to a fresh directory for test isolation. +Future _copyTemplateProject() async { + final Directory template = await _getTemplateProject(); + final Directory testDir = Directory( + path.join(Directory.systemTemp.createTempSync().path, 'shorebird_test'), + ); + + // Use platform copy to preserve the full directory tree efficiently. + final ProcessResult result; + if (Platform.isWindows) { + result = await Process.run('xcopy', [ + template.path, + testDir.path, + '/E', + '/I', + '/Q', + ]); + } else { + result = await Process.run('cp', ['-R', template.path, testDir.path]); + } + if (result.exitCode != 0) { + throw Exception('Failed to copy template project: ${result.stderr}'); + } + + return testDir; +} + +@isTest +Future testWithShorebirdProject(String name, + FutureOr Function(Directory projectDirectory) testFn) async { + test( + name, + () async { + final Directory projectDirectory = await _copyTemplateProject(); + + try { + await testFn(projectDirectory); + } finally { + projectDirectory.deleteSync(recursive: true); + } + }, + timeout: Timeout( + // Per-test timeout can be shorter now since the template project + // creation and Gradle warm-up happen outside the test timeout. + Duration(minutes: 6), + ), + ); +} + +extension ShorebirdProjectDirectoryOnDirectory on Directory { + File get pubspecFile => File( + path.join(this.path, 'pubspec.yaml'), + ); + + File get shorebirdFile => File( + path.join(this.path, 'shorebird.yaml'), + ); + + YamlMap get shorebirdYaml => + loadYaml(shorebirdFile.readAsStringSync()) as YamlMap; + + File get appGradleFile => File( + path.join(this.path, 'android', 'app', 'build.gradle'), + ); + + Future addPubDependency(String name, {bool dev = false}) async { + final result = await _runFlutterCommand( + ['pub', 'add', if (dev) '--dev', name], + workingDirectory: this, + ); + if (result.exitCode != 0) { + throw Exception( + 'Failed to run `flutter pub add $name`: ${result.stderr}'); + } + } + + Future addProjectFlavors() async { + await addPubDependency( + // Published flutter_flavorizr. We previously pinned a fork for Flutter + // 3.29 support (AngeloAvv/flutter_flavorizr#291, never merged); the + // published package has since adopted 3.29/AGP-8 support (>=2.3.0), so + // the fork is obsolete. + // + // Pinned to 2.4.2 (not the latest 2.5.x): 2.5.0 replaced the Ruby + // xcodeproj gem with dart_xcodeproj, and its generated .pbxproj breaks + // `flutter build ipa --no-codesign --flavor` (the unsigned flavor + // archive demands a Development Team). 2.4.2 is the last Ruby-xcodeproj + // release and matches the generation path the old fork used, while + // still carrying the AGP-8 resValues fix the fork lacked. + 'dev:flutter_flavorizr:2.4.2', + ); + + await File( + path.join( + this.path, + 'flavorizr.yaml', + ), + ).writeAsString(''' +flavors: + playStore: + app: + name: "App" + + android: + applicationId: "com.example.shorebird_test" + ios: + bundleId: "com.example.shorebird_test" + internal: + app: + name: "App (Internal)" + + android: + applicationId: "com.example.shorebird_test.internal" + ios: + bundleId: "com.example.shorebird_test.internal" + global: + app: + name: "App (Global)" + + android: + applicationId: "com.example.shorebird_test.global" + ios: + bundleId: "com.example.shorebird_test.global" +'''); + + final result = await _runFlutterCommand( + // -f/--force skips flutter_flavorizr's interactive "proceed? (Y/n)" + // prompt, which throws "No terminal attached to stdout" under CI. + ['pub', 'run', 'flutter_flavorizr', '-f'], + workingDirectory: this, + ); + if (result.exitCode != 0) { + throw Exception( + 'Failed to run `flutter pub run flutter_flavorizr`: ${result.stderr}'); + } + + // flutter_flavorizr 2.4.2 emits per-flavor `resValue` entries in + // build.gradle. AGP 8 (Flutter 3.44+) gates resValues behind an opt-in + // build feature, so the flavored `flutter build apk` fails with "contains + // custom resource values, but the feature is disabled" unless we enable + // it. (Published flavorizr only enables this on 2.5.x, which we can't use: + // 2.5.0's dart_xcodeproj rewrite breaks `flutter build ipa --no-codesign + // --flavor`. So we stay on the last Ruby-xcodeproj release and toggle the + // build feature ourselves.) + final gradleProperties = + File(path.join(this.path, 'android', 'gradle.properties')); + await gradleProperties.writeAsString( + '\nandroid.defaults.buildfeatures.resvalues=true\n', + mode: FileMode.append, + ); + } + + void addShorebirdFlavors() { + const flavors = ''' +flavors: + global: global_123 + internal: internal_123 + playStore: playStore_123 +'''; + + final currentShorebirdContent = shorebirdFile.readAsStringSync(); + shorebirdFile.writeAsStringSync( + ''' +$currentShorebirdContent +$flavors +''', + ); + } + + Future runFlutterBuildApk({ + String? flavor, + Map? environment, + }) async { + final result = await _runFlutterCommand( + [ + 'build', + 'apk', + if (flavor != null) '--flavor=$flavor', + ], + workingDirectory: this, + environment: environment, + ); + if (result.exitCode != 0) { + throw Exception('Failed to run `flutter build apk`: ${result.stderr}'); + } + } + + Future runFlutterBuildIos({ + Map? environment, + String? flavor, + }) async { + final result = await _runFlutterCommand( + // The projects used to test are generated on spot, to make it simpler we don't + // configure any apple accounts on it, so we skip code signing here. + ['build', 'ipa', '--no-codesign', if (flavor != null) '--flavor=$flavor'], + workingDirectory: this, + environment: environment, + ); + + if (result.exitCode != 0) { + throw Exception('Failed to run `flutter build ios`: ${result.stderr}'); + } + } + + File apkFile({String? flavor}) => File( + path.join( + this.path, + 'build', + 'app', + 'outputs', + 'flutter-apk', + 'app-${flavor != null ? '$flavor-' : ''}release.apk', + ), + ); + + Directory iosArchiveFile() => Directory( + path.join( + this.path, + 'build', + 'ios', + 'archive', + 'Runner.xcarchive', + ), + ); + + Future getGeneratedAndroidShorebirdYaml({String? flavor}) async { + final decodedBytes = + ZipDecoder().decodeBytes(apkFile(flavor: flavor).readAsBytesSync()); + + await extractArchiveToDisk( + decodedBytes, path.join(this.path, 'apk-extracted')); + + final yamlString = File( + path.join( + this.path, + 'apk-extracted', + 'assets', + 'flutter_assets', + 'shorebird.yaml', + ), + ).readAsStringSync(); + return loadYaml(yamlString) as YamlMap; + } + + Future getGeneratedIosShorebirdYaml() async { + final yamlString = File( + path.join( + iosArchiveFile().path, + 'Products', + 'Applications', + 'Runner.app', + 'Frameworks', + 'App.framework', + 'flutter_assets', + 'shorebird.yaml', + ), + ).readAsStringSync(); + return loadYaml(yamlString) as YamlMap; + } +} diff --git a/shorebird/ci/artifacts_manifest.template.yaml b/shorebird/ci/artifacts_manifest.template.yaml new file mode 100644 index 0000000000000..33223fced2de8 --- /dev/null +++ b/shorebird/ci/artifacts_manifest.template.yaml @@ -0,0 +1,65 @@ +# Template for artifacts_manifest.yaml +# This file is processed by shard_runner:finalize +# Variable substitution: {{flutter_engine_revision}} is replaced at generation time +# The $engine placeholder is kept as-is for Shorebird's artifact proxy + +flutter_engine_revision: {{flutter_engine_revision}} +storage_bucket: download.shorebird.dev +artifact_overrides: + # Android release artifacts + - flutter_infra_release/flutter/$engine/android-arm64-release/artifacts.zip + - flutter_infra_release/flutter/$engine/android-arm64-release/darwin-x64.zip + - flutter_infra_release/flutter/$engine/android-arm64-release/linux-x64.zip + - flutter_infra_release/flutter/$engine/android-arm64-release/symbols.zip + - flutter_infra_release/flutter/$engine/android-arm64-release/windows-x64.zip + + - flutter_infra_release/flutter/$engine/android-arm-release/artifacts.zip + - flutter_infra_release/flutter/$engine/android-arm-release/darwin-x64.zip + - flutter_infra_release/flutter/$engine/android-arm-release/linux-x64.zip + - flutter_infra_release/flutter/$engine/android-arm-release/symbols.zip + - flutter_infra_release/flutter/$engine/android-arm-release/windows-x64.zip + + - flutter_infra_release/flutter/$engine/android-x64-release/artifacts.zip + - flutter_infra_release/flutter/$engine/android-x64-release/darwin-x64.zip + - flutter_infra_release/flutter/$engine/android-x64-release/linux-x64.zip + - flutter_infra_release/flutter/$engine/android-x64-release/symbols.zip + - flutter_infra_release/flutter/$engine/android-x64-release/windows-x64.zip + + # engine_stamp.json + - flutter_infra_release/flutter/$engine/engine_stamp.json + + # Dart SDK + - flutter_infra_release/flutter/$engine/dart-sdk-darwin-arm64.zip + - flutter_infra_release/flutter/$engine/dart-sdk-darwin-x64.zip + - flutter_infra_release/flutter/$engine/dart-sdk-linux-x64.zip + - flutter_infra_release/flutter/$engine/dart-sdk-windows-x64.zip + + # Maven artifacts + - download.flutter.io/io/flutter/flutter_embedding_release/1.0.0-$engine/flutter_embedding_release-1.0.0-$engine.pom + - download.flutter.io/io/flutter/flutter_embedding_release/1.0.0-$engine/flutter_embedding_release-1.0.0-$engine.jar + - download.flutter.io/io/flutter/arm64_v8a_release/1.0.0-$engine/arm64_v8a_release-1.0.0-$engine.pom + - download.flutter.io/io/flutter/arm64_v8a_release/1.0.0-$engine/arm64_v8a_release-1.0.0-$engine.jar + - download.flutter.io/io/flutter/armeabi_v7a_release/1.0.0-$engine/armeabi_v7a_release-1.0.0-$engine.pom + - download.flutter.io/io/flutter/armeabi_v7a_release/1.0.0-$engine/armeabi_v7a_release-1.0.0-$engine.jar + - download.flutter.io/io/flutter/x86_64_release/1.0.0-$engine/x86_64_release-1.0.0-$engine.pom + - download.flutter.io/io/flutter/x86_64_release/1.0.0-$engine/x86_64_release-1.0.0-$engine.jar + + # Common release artifacts + - flutter_infra_release/flutter/$engine/flutter_patched_sdk_product.zip + + # iOS release artifacts + - flutter_infra_release/flutter/$engine/ios-release/artifacts.zip + - flutter_infra_release/flutter/$engine/ios-release/Flutter.framework.dSYM.zip + + # Linux release artifacts + - flutter_infra_release/flutter/$engine/linux-x64/artifacts.zip + - flutter_infra_release/flutter/$engine/linux-x64-release/linux-x64-flutter-gtk.zip + + # macOS release artifacts + - flutter_infra_release/flutter/$engine/darwin-x64-release/artifacts.zip + - flutter_infra_release/flutter/$engine/darwin-x64-release/framework.zip + - flutter_infra_release/flutter/$engine/darwin-x64-release/gen_snapshot.zip + + # Windows release artifacts + - flutter_infra_release/flutter/$engine/windows-x64/artifacts.zip + - flutter_infra_release/flutter/$engine/windows-x64-release/windows-x64-flutter.zip diff --git a/shorebird/ci/compose.json b/shorebird/ci/compose.json new file mode 100644 index 0000000000000..494ad46cc0f23 --- /dev/null +++ b/shorebird/ci/compose.json @@ -0,0 +1,42 @@ +{ + "ios-framework": { + "requires": ["ios-release", "ios-sim-x64", "ios-sim-arm64"], + "script": "flutter/sky/tools/create_ios_framework.py", + "flags": ["--dsym", "--strip", "--no-extension-safe-frameworks"], + "path_args": { + "--arm64-out-dir": "ios_release", + "--simulator-x64-out-dir": "ios_debug_sim", + "--simulator-arm64-out-dir": "ios_debug_sim_arm64" + }, + "artifacts": [ + {"src": "artifacts.zip", "dst": "flutter_infra_release/flutter/$engine/ios-release/artifacts.zip"}, + {"src": "Flutter.framework.dSYM.zip", "dst": "flutter_infra_release/flutter/$engine/ios-release/Flutter.framework.dSYM.zip"} + ] + }, + "macos-framework": { + "requires": ["mac-arm64", "mac-x64"], + "script": "flutter/sky/tools/create_macos_framework.py", + "flags": ["--dsym", "--strip", "--zip"], + "path_args": { + "--arm64-out-dir": "mac_release_arm64", + "--x64-out-dir": "mac_release" + }, + "artifacts": [ + {"src": "framework.zip", "dst": "flutter_infra_release/flutter/$engine/darwin-x64-release/framework.zip"}, + {"src": "FlutterMacOS.framework.dSYM.zip", "dst": "flutter_infra_release/flutter/$engine/darwin-x64/FlutterMacOS.framework.dSYM.zip"} + ] + }, + "macos-gen-snapshot": { + "requires": ["mac-arm64", "mac-x64"], + "script": "flutter/sky/tools/create_macos_gen_snapshots.py", + "flags": ["--zip"], + "path_args": { + "--arm64-path": "mac_release_arm64/universal/gen_snapshot_arm64", + "--x64-path": "mac_release/universal/gen_snapshot_x64" + }, + "dst": "release/snapshot", + "artifacts": [ + {"src": "snapshot/gen_snapshot.zip", "dst": "flutter_infra_release/flutter/$engine/darwin-x64-release/gen_snapshot.zip"} + ] + } +} diff --git a/shorebird/ci/internal/generate_manifest.sh b/shorebird/ci/internal/generate_manifest.sh new file mode 100755 index 0000000000000..ee9ef7ddfdf67 --- /dev/null +++ b/shorebird/ci/internal/generate_manifest.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +# This script outputs an artifact_manifest.yaml mapping +# a shorebird engine revision to a flutter engine revision. +# Usage: +# ./generate_manifest.sh > artifact_manifest.yaml + +set -e + +# NOTE: If you edit this file you also may need to edit the global list +# of all known artifacts in the artifact_proxy's config.dart + +if [ "$#" -ne 1 ]; then + echo "Usage: ./generate_manifest.sh " + exit 1 +fi + +FLUTTER_ENGINE_REVISION=$1 + +cat < $MANIFEST_FILE + +# FIXME: This should not be in shell, it's too complicated/repetitive. +# Only need the libflutter.so (and flutter.jar) artifacts +# Artifact list: https://github.com/shorebirdtech/shorebird/blob/main/packages/artifact_proxy/lib/config.dart + +HOST_ARCH='darwin-x64' +ARM64_HOST_ARCH='darwin-arm64' + +INFRA_ROOT="gs://$STORAGE_BUCKET/flutter_infra_release/flutter/$ENGINE_HASH" + +# engine_stamp.json +ENGINE_STAMP_FILE=$ENGINE_OUT/engine_stamp.json +gsutil cp $ENGINE_STAMP_FILE $INFRA_ROOT/engine_stamp.json + +# Dart SDK +# This gets uploaded to flutter_infra_release/flutter/\$engine/dart-sdk-$HOST_ARCH.zip +# We also upload to the content-aware hash path to support local development branches. +CONTENT_INFRA_ROOT="gs://$STORAGE_BUCKET/flutter_infra_release/flutter/$CONTENT_HASH" + +# x64 Dart SDK +HOST_RELEASE=$ENGINE_OUT/host_release +DART_ZIP_FILE=dart-sdk-$HOST_ARCH.zip +( + cd $HOST_RELEASE; + zip -r $DART_ZIP_FILE dart-sdk +) +ZIPS_DEST=$INFRA_ROOT/$DART_ZIP_FILE +gsutil cp $HOST_RELEASE/$DART_ZIP_FILE $ZIPS_DEST +# Also upload to content-aware hash path +gsutil cp $HOST_RELEASE/$DART_ZIP_FILE $CONTENT_INFRA_ROOT/$DART_ZIP_FILE + +# arm64 Dart SDK +HOST_RELEASE_ARM64=$ENGINE_OUT/host_release_arm64 +DART_ZIP_FILE=dart-sdk-$ARM64_HOST_ARCH.zip +( + cd $HOST_RELEASE_ARM64; + zip -r $DART_ZIP_FILE dart-sdk +) +ZIPS_DEST=$INFRA_ROOT/$DART_ZIP_FILE +gsutil cp $HOST_RELEASE_ARM64/$DART_ZIP_FILE $ZIPS_DEST +# Also upload to content-aware hash path +gsutil cp $HOST_RELEASE_ARM64/$DART_ZIP_FILE $CONTENT_INFRA_ROOT/$DART_ZIP_FILE + +# We want to build flutter/tools/font_subset, but that doesn't work with +# --no-prebuilt-dart-sdk. +# https://github.com/flutter/flutter/issues/164531 +# # mac x64 host_release font_subset (ConstFinder) +# ARCH_OUT=$ENGINE_OUT/host_release +# ZIPS_OUT=$ARCH_OUT/zip_archives/$HOST_ARCH +# ZIPS_DEST=$INFRA_ROOT/darwin-x64-release +# gsutil cp $ZIPS_OUT/font-subset.zip $ZIPS_DEST/font-subset.zip + +# # mac arm64 host_release font_subset (ConstFinder) +# ARCH_OUT=$ENGINE_OUT/host_release_arm64 +# ZIPS_OUT=$ARCH_OUT/zip_archives/$HOST_ARCH +# ZIPS_DEST=$INFRA_ROOT/darwin-arm64-release +# gsutil cp $ZIPS_OUT/font-subset.zip $ZIPS_DEST/font-subset.zip + +# Android Arm64 release gen_snapshot +ARCH_OUT=$ENGINE_OUT/android_release_arm64 +ZIPS_OUT=$ARCH_OUT/zip_archives/android-arm64-release +ZIPS_DEST=$INFRA_ROOT/android-arm64-release +gsutil cp $ZIPS_OUT/$HOST_ARCH.zip $ZIPS_DEST/$HOST_ARCH.zip + +# Android Arm32 release gen_snapshot +ARCH_OUT=$ENGINE_OUT/android_release +ZIPS_OUT=$ARCH_OUT/zip_archives/android-arm-release +ZIPS_DEST=$INFRA_ROOT/android-arm-release +gsutil cp $ZIPS_OUT/$HOST_ARCH.zip $ZIPS_DEST/$HOST_ARCH.zip + +# Android x64 release gen_snapshot +ARCH_OUT=$ENGINE_OUT/android_release_x64 +ZIPS_OUT=$ARCH_OUT/zip_archives/android-x64-release +ZIPS_DEST=$INFRA_ROOT/android-x64-release +gsutil cp $ZIPS_OUT/$HOST_ARCH.zip $ZIPS_DEST/$HOST_ARCH.zip + +# Match the upload pattern from iOS: +# https://github.com/flutter/engine/commit/1d7f0c66c316a37105601b13136f890f6595aebc + +# iOS release Flutter artifacts +ARCH_OUT=$ENGINE_OUT/release +ZIPS_OUT=$ARCH_OUT +ZIPS_DEST=$INFRA_ROOT/ios-release +gsutil cp $ZIPS_OUT/artifacts.zip $ZIPS_DEST/artifacts.zip + +# iOS dSYM +gsutil cp $ZIPS_OUT/Flutter.framework.dSYM.zip $ZIPS_DEST/Flutter.framework.dSYM.zip + +# macOS framework +ARCH_OUT=$ENGINE_OUT/release +ZIPS_OUT=$ARCH_OUT/framework +ZIPS_DEST=$INFRA_ROOT/darwin-x64-release +gsutil cp $ZIPS_OUT/framework.zip $ZIPS_DEST/framework.zip + +# macOS gen_snapshot +ARCH_OUT=$ENGINE_OUT/release +ZIPS_OUT=$ARCH_OUT/snapshot +ZIPS_DEST=$INFRA_ROOT/darwin-x64-release +gsutil cp $ZIPS_OUT/gen_snapshot.zip $ZIPS_DEST/gen_snapshot.zip + +# FIXME: these should go where we're putting the arm64 macOS artifacts +# (darwin-x64-release), however, arm macs use darwin-x64-release and we +# currently only support those. We need to find a way to support both. +# macOS x64 release artifacts +# ARCH_OUT=$ENGINE_OUT/mac_release +# ZIPS_OUT=$ARCH_OUT/zip_archives/darwin-x64-release +# ZIPS_DEST=$INFRA_ROOT/darwin-x64-release +# gsutil cp $ZIPS_OUT/artifacts.zip $ZIPS_DEST/artifacts.zip + +# macOS arm64 release artifacts +ARCH_OUT=$ENGINE_OUT/mac_release_arm64 +ZIPS_OUT=$ARCH_OUT/zip_archives/darwin-arm64-release +# This looks wrong - why are we putting arm64 artifacts in darwin-x64-release +# instead of darwin-arm64-release? This is because arm macs use darwin-x64-release +# and we need to use the artifacts we've built for arm64 macs. +ZIPS_DEST=$INFRA_ROOT/darwin-x64-release +gsutil cp $ZIPS_OUT/artifacts.zip $ZIPS_DEST/artifacts.zip + +# macOS dSYM (used for symbolication, not by Flutter) +ARCH_OUT=$ENGINE_OUT/release +ZIPS_OUT=$ARCH_OUT/framework +ZIPS_DEST=$INFRA_ROOT/darwin-x64 +gsutil cp $ZIPS_OUT/FlutterMacOS.framework.dSYM.zip $ZIPS_DEST/FlutterMacOS.framework.dSYM.zip + +TMP_DIR=$(mktemp -d) + +PATCH_VERSION=0.3.0 +GH_RELEASE=https://github.com/shorebirdtech/updater/releases/download/patch-v$PATCH_VERSION/ +cd $TMP_DIR +curl -L $GH_RELEASE/patch-x86_64-apple-darwin.zip -o patch-x86_64-apple-darwin.zip +curl -L $GH_RELEASE/patch-aarch64-apple-darwin.zip -o patch-aarch64-apple-darwin.zip +curl -L $GH_RELEASE/patch-x86_64-pc-windows-msvc.zip -o patch-x86_64-pc-windows-msvc.zip +curl -L $GH_RELEASE/patch-x86_64-unknown-linux-musl.zip -o patch-x86_64-unknown-linux-musl.zip + +gsutil cp patch-x86_64-apple-darwin.zip $SHOREBIRD_ROOT/patch-darwin-x64.zip +gsutil cp patch-aarch64-apple-darwin.zip $SHOREBIRD_ROOT/patch-darwin-arm64.zip +gsutil cp patch-x86_64-pc-windows-msvc.zip $SHOREBIRD_ROOT/patch-windows-x64.zip +gsutil cp patch-x86_64-unknown-linux-musl.zip $SHOREBIRD_ROOT/patch-linux-x64.zip + +gsutil cp $MANIFEST_FILE $SHOREBIRD_ROOT/artifacts_manifest.yaml diff --git a/shorebird/ci/internal/win_build.sh b/shorebird/ci/internal/win_build.sh new file mode 100755 index 0000000000000..9c47369707d34 --- /dev/null +++ b/shorebird/ci/internal/win_build.sh @@ -0,0 +1,63 @@ +#!/bin/bash -e + +# Usage: +# ./win_build.sh engine_path + +ENGINE_ROOT=$1 + +ENGINE_SRC=$ENGINE_ROOT/src +ENGINE_OUT=$ENGINE_SRC/out +UPDATER_SRC=$ENGINE_SRC/flutter/third_party/updater +HOST_ARCH='windows-x64' + +# The Rust updater library is now built as part of the GN/Ninja engine +# build โ€” see //flutter/shell/common/shorebird/BUILD.gn's +# build_rust_updater action. Any engine target that depends (transitively) +# on //flutter/shell/common/shorebird:updater pulls in updater.lib +# automatically. + +# Compile the engine using the steps here: +# https://github.com/flutter/flutter/wiki/Compiling-the-engine#compiling-for-android-from-macos-or-linux +cd $ENGINE_SRC + +NINJA="ninja" +GN=./flutter/tools/gn +# We could probably use our own prebuilt dart SDK, by modifying the gn files. +GN_ARGS="--no-rbe --no-enable-unittests" + +# Windows only needs gen_snapshot for each Android CPU type. +# See https://github.com/flutter/engine/blob/e590b24f3962fda3ec9144dcee3f7565b195839a/ci/builders/windows_android_aot_engine.json + +TARGETS="archive_win_gen_snapshot" + +# Build host_release +$GN $GN_ARGS --runtime-mode=release --no-prebuilt-dart-sdk +$NINJA -C out/host_release dart_sdk +# We want to build flutter/tools/font_subset, but that doesn't work with +# --no-prebuilt-dart-sdk. +# https://github.com/flutter/flutter/issues/164531 + +# Build windows desktop targets +$GN $GN_ARGS --runtime-mode=release --no-prebuilt-dart-sdk +$NINJA -C ./out/host_release flutter/build/archives:windows_flutter gen_snapshot windows flutter/build/archives:artifacts + +# Build debug Windows artifacts +# These are output to the `windows-x64` directory in host_debug, and are used +# by `flutter build windows --release`. +$GN $GN_ARGS --no-prebuilt-dart-sdk +$NINJA -C ./out/host_debug flutter/build/archives:artifacts + +# If this gives you trouble, try using VS2019 instead. I had trouble with 2022. +# Android arm64 release +$GN $GN_ARGS --android --android-cpu=arm64 --runtime-mode=release +$NINJA -C ./out/android_release_arm64 $TARGETS + +# Android arm32 release +$GN $GN_ARGS --runtime-mode=release --android +$NINJA -C out/android_release $TARGETS + +# Android x64 release +$GN $GN_ARGS --android --android-cpu=x64 --runtime-mode=release +$NINJA -C ./out/android_release_x64 $TARGETS + +# We could also build the `patch` tool for Windows here. diff --git a/shorebird/ci/internal/win_setup.sh b/shorebird/ci/internal/win_setup.sh new file mode 100755 index 0000000000000..971135644f66b --- /dev/null +++ b/shorebird/ci/internal/win_setup.sh @@ -0,0 +1,8 @@ +#!/bin/bash -e + +# Usage: +# ./windows_setup.sh + +# Add the MSVC toolchain to Rust. +rustup target add \ + x86_64-pc-windows-msvc diff --git a/shorebird/ci/internal/win_upload.sh b/shorebird/ci/internal/win_upload.sh new file mode 100755 index 0000000000000..952457109e40c --- /dev/null +++ b/shorebird/ci/internal/win_upload.sh @@ -0,0 +1,90 @@ +#!/bin/bash -e + +# Usage: +# ./win_upload.sh engine_path git_hash +ENGINE_ROOT=$1 +ENGINE_HASH=$2 + +STORAGE_BUCKET="download.shorebird.dev" +SHOREBIRD_ROOT=gs://$STORAGE_BUCKET/shorebird/$ENGINE_HASH + +ENGINE_SRC=$ENGINE_ROOT/src +ENGINE_OUT=$ENGINE_SRC/out +ENGINE_FLUTTER=$ENGINE_SRC/flutter +# FLUTTER_ROOT is the Flutter monorepo root (parent of engine/) +FLUTTER_ROOT=$(dirname $ENGINE_ROOT) + +cd $FLUTTER_ROOT + +# Compute the content-aware hash for the Dart SDK. +# This allows Flutter checkouts that haven't changed engine content to share +# the same pre-built Dart SDK, even if they have different git commit SHAs. +CONTENT_HASH=$($FLUTTER_ROOT/bin/internal/content_aware_hash.sh) + +# We do not generate a manifest file, we assume another builder did that. + +# TODO(eseidel): This should not be in shell, it's too complicated/repetitive. + +HOST_ARCH='windows-x64' + +INFRA_ROOT="gs://$STORAGE_BUCKET/flutter_infra_release/flutter/$ENGINE_HASH" + +# Dart SDK +# This gets uploaded to flutter_infra_release/flutter/\$engine/dart-sdk-$HOST_ARCH.zip +# We also upload to the content-aware hash path to support local development branches. +CONTENT_INFRA_ROOT="gs://$STORAGE_BUCKET/flutter_infra_release/flutter/$CONTENT_HASH" + +DART_SDK_DIR=$ENGINE_OUT/host_release/dart-sdk +DART_ZIP_FILE=dart-sdk-$HOST_ARCH.zip + +# Use 7zip to compress the Dart SDK, as zip isn't available on Windows and +# Powershell, which we would normally use in the form of +# `powershell Compress-Archive dart-sdk dart-sdk.zip`, doesn't play nicely +# with git bash paths (e.g. /c/Users/... instead of C:/Users/...) +/c/Program\ Files/7-Zip/7z a $DART_ZIP_FILE $DART_SDK_DIR +ZIPS_DEST=$INFRA_ROOT/$DART_ZIP_FILE +gsutil cp $DART_ZIP_FILE $ZIPS_DEST +# Also upload to content-aware hash path +gsutil cp $DART_ZIP_FILE $CONTENT_INFRA_ROOT/$DART_ZIP_FILE + +# Android Arm64 release gen_snapshot +ARCH_OUT=$ENGINE_OUT/android_release_arm64 +ZIPS_OUT=$ARCH_OUT/zip_archives/android-arm64-release +ZIPS_DEST=$INFRA_ROOT/android-arm64-release +gsutil cp $ZIPS_OUT/$HOST_ARCH.zip $ZIPS_DEST/$HOST_ARCH.zip + +# Android Arm32 release gen_snapshot +ARCH_OUT=$ENGINE_OUT/android_release +ZIPS_OUT=$ARCH_OUT/zip_archives/android-arm-release +ZIPS_DEST=$INFRA_ROOT/android-arm-release +gsutil cp $ZIPS_OUT/$HOST_ARCH.zip $ZIPS_DEST/$HOST_ARCH.zip + +# Android x64 release gen_snapshot +ARCH_OUT=$ENGINE_OUT/android_release_x64 +ZIPS_OUT=$ARCH_OUT/zip_archives/android-x64-release +ZIPS_DEST=$INFRA_ROOT/android-x64-release +gsutil cp $ZIPS_OUT/$HOST_ARCH.zip $ZIPS_DEST/$HOST_ARCH.zip + +# We could upload patch if we built it here. +# gsutil cp $ENGINE_OUT/host_release/patch.zip $SHOREBIRD_ROOT/patch-win-x64.zip + +# Engine release artifacts +ARCH_OUT=$ENGINE_OUT/host_release +ZIPS_OUT=$ARCH_OUT/zip_archives/$HOST_ARCH-release +ZIPS_DEST=$INFRA_ROOT/$HOST_ARCH-release +gsutil cp $ZIPS_OUT/$HOST_ARCH-flutter.zip $ZIPS_DEST/$HOST_ARCH-flutter.zip + +# We want to build flutter/tools/font_subset, but that doesn't work with +# --no-prebuilt-dart-sdk. +# https://github.com/flutter/flutter/issues/164531 +# # Windows x64 host_release font_subset (ConstFinder) +# ARCH_OUT=$ENGINE_OUT/host_release +# ZIPS_OUT=$ARCH_OUT/zip_archives/$HOST_ARCH +# ZIPS_DEST=$INFRA_ROOT/windows-x64-release +# gsutil cp $ZIPS_OUT/font-subset.zip $ZIPS_DEST/font-subset.zip + +# Engine debug artifacts (not sure why this is needed?) +ARCH_OUT=$ENGINE_OUT/host_debug +ZIPS_OUT=$ARCH_OUT/zip_archives/$HOST_ARCH +ZIPS_DEST=$INFRA_ROOT/$HOST_ARCH +gsutil cp $ZIPS_OUT/artifacts.zip $ZIPS_DEST/artifacts.zip diff --git a/shorebird/ci/linux_build_and_upload.sh b/shorebird/ci/linux_build_and_upload.sh new file mode 100755 index 0000000000000..9657e14178695 --- /dev/null +++ b/shorebird/ci/linux_build_and_upload.sh @@ -0,0 +1,32 @@ +#!/bin/bash -e + +# Usage: +# ./linux_build_and_upload.sh flutter_root engine_hash +# +# This is the main entrypoint for building and uploading Linux engine artifacts. +# It is called from the _build_engine repository's CI scripts. + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 flutter_root engine_hash" + exit 1 +fi + +FLUTTER_ROOT=$1 +ENGINE_HASH=$2 +ENGINE_ROOT=$FLUTTER_ROOT/engine + +# Get the absolute path to the directory of this script. +SCRIPT_DIR=$(cd $(dirname $0) && pwd) + +echo "Building engine at $ENGINE_ROOT and uploading to gs://download.shorebird.dev" + +cd $SCRIPT_DIR + +# Run the setup script. +./internal/linux_setup.sh + +# Then run the build. +./internal/linux_build.sh $ENGINE_ROOT + +# Copy Shorebird engine artifacts to Google Cloud Storage. +./internal/linux_upload.sh $ENGINE_ROOT $ENGINE_HASH diff --git a/shorebird/ci/mac_build_and_upload.sh b/shorebird/ci/mac_build_and_upload.sh new file mode 100755 index 0000000000000..9ee302620d207 --- /dev/null +++ b/shorebird/ci/mac_build_and_upload.sh @@ -0,0 +1,32 @@ +#!/bin/bash -e + +# Usage: +# ./mac_build_and_upload.sh flutter_root engine_hash +# +# This is the main entrypoint for building and uploading macOS engine artifacts. +# It is called from the _build_engine repository's CI scripts. + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 flutter_root engine_hash" + exit 1 +fi + +FLUTTER_ROOT=$1 +ENGINE_HASH=$2 +ENGINE_ROOT=$FLUTTER_ROOT/engine + +# Get the absolute path to the directory of this script. +SCRIPT_DIR=$(cd $(dirname $0) && pwd) + +echo "Building engine at $ENGINE_ROOT and uploading to gs://download.shorebird.dev" + +cd $SCRIPT_DIR + +# Run the setup script. +./internal/mac_setup.sh + +# Then run the build. +./internal/mac_build.sh $ENGINE_ROOT + +# Copy Shorebird engine artifacts to Google Cloud Storage. +./internal/mac_upload.sh $ENGINE_ROOT $ENGINE_HASH diff --git a/shorebird/ci/shards/linux.json b/shorebird/ci/shards/linux.json new file mode 100644 index 0000000000000..ffd1b4982dbda --- /dev/null +++ b/shorebird/ci/shards/linux.json @@ -0,0 +1,108 @@ +{ + "android-arm64": { + "steps": [ + { + "type": "rust", + "targets": ["aarch64-linux-android"] + }, + { + "type": "gn_ninja", + "gn_args": ["--android", "--android-cpu=arm64", "--runtime-mode=release"], + "ninja_targets": ["default", "gen_snapshot"], + "out_dir": "android_release_arm64" + } + ], + "artifacts": [ + {"src": "android_release_arm64/zip_archives/android-arm64-release/artifacts.zip", "dst": "flutter_infra_release/flutter/$engine/android-arm64-release/artifacts.zip"}, + {"src": "android_release_arm64/zip_archives/android-arm64-release/linux-x64.zip", "dst": "flutter_infra_release/flutter/$engine/android-arm64-release/linux-x64.zip"}, + {"src": "android_release_arm64/zip_archives/android-arm64-release/symbols.zip", "dst": "flutter_infra_release/flutter/$engine/android-arm64-release/symbols.zip"}, + {"src": "android_release_arm64/arm64_v8a_release.pom", "dst": "download.flutter.io/io/flutter/arm64_v8a_release/1.0.0-$engine/arm64_v8a_release-1.0.0-$engine.pom"}, + {"src": "android_release_arm64/arm64_v8a_release.jar", "dst": "download.flutter.io/io/flutter/arm64_v8a_release/1.0.0-$engine/arm64_v8a_release-1.0.0-$engine.jar"}, + {"src": "android_release_arm64/arm64_v8a_release.maven-metadata.xml", "dst": "download.flutter.io/io/flutter/arm64_v8a_release/1.0.0-$engine/arm64_v8a_release-1.0.0-$engine.maven-metadata.xml"} + ] + }, + "android-arm32": { + "steps": [ + { + "type": "rust", + "targets": ["armv7-linux-androideabi"] + }, + { + "type": "gn_ninja", + "gn_args": ["--android", "--runtime-mode=release"], + "ninja_targets": ["default", "gen_snapshot"], + "out_dir": "android_release" + } + ], + "artifacts": [ + {"src": "android_release/zip_archives/android-arm-release/artifacts.zip", "dst": "flutter_infra_release/flutter/$engine/android-arm-release/artifacts.zip"}, + {"src": "android_release/zip_archives/android-arm-release/linux-x64.zip", "dst": "flutter_infra_release/flutter/$engine/android-arm-release/linux-x64.zip"}, + {"src": "android_release/zip_archives/android-arm-release/symbols.zip", "dst": "flutter_infra_release/flutter/$engine/android-arm-release/symbols.zip"}, + {"src": "android_release/armeabi_v7a_release.pom", "dst": "download.flutter.io/io/flutter/armeabi_v7a_release/1.0.0-$engine/armeabi_v7a_release-1.0.0-$engine.pom"}, + {"src": "android_release/armeabi_v7a_release.jar", "dst": "download.flutter.io/io/flutter/armeabi_v7a_release/1.0.0-$engine/armeabi_v7a_release-1.0.0-$engine.jar"}, + {"src": "android_release/armeabi_v7a_release.maven-metadata.xml", "dst": "download.flutter.io/io/flutter/armeabi_v7a_release/1.0.0-$engine/armeabi_v7a_release-1.0.0-$engine.maven-metadata.xml"}, + {"src": "android_release/flutter_embedding_release.pom", "dst": "download.flutter.io/io/flutter/flutter_embedding_release/1.0.0-$engine/flutter_embedding_release-1.0.0-$engine.pom"}, + {"src": "android_release/flutter_embedding_release.jar", "dst": "download.flutter.io/io/flutter/flutter_embedding_release/1.0.0-$engine/flutter_embedding_release-1.0.0-$engine.jar"}, + {"src": "android_release/flutter_embedding_release.maven-metadata.xml", "dst": "download.flutter.io/io/flutter/flutter_embedding_release/1.0.0-$engine/flutter_embedding_release-1.0.0-$engine.maven-metadata.xml"} + ] + }, + "android-x64": { + "steps": [ + { + "type": "rust", + "targets": ["x86_64-linux-android"] + }, + { + "type": "gn_ninja", + "gn_args": ["--android", "--android-cpu=x64", "--runtime-mode=release"], + "ninja_targets": ["default", "gen_snapshot"], + "out_dir": "android_release_x64" + } + ], + "artifacts": [ + {"src": "android_release_x64/zip_archives/android-x64-release/artifacts.zip", "dst": "flutter_infra_release/flutter/$engine/android-x64-release/artifacts.zip"}, + {"src": "android_release_x64/zip_archives/android-x64-release/linux-x64.zip", "dst": "flutter_infra_release/flutter/$engine/android-x64-release/linux-x64.zip"}, + {"src": "android_release_x64/zip_archives/android-x64-release/symbols.zip", "dst": "flutter_infra_release/flutter/$engine/android-x64-release/symbols.zip"}, + {"src": "android_release_x64/x86_64_release.pom", "dst": "download.flutter.io/io/flutter/x86_64_release/1.0.0-$engine/x86_64_release-1.0.0-$engine.pom"}, + {"src": "android_release_x64/x86_64_release.jar", "dst": "download.flutter.io/io/flutter/x86_64_release/1.0.0-$engine/x86_64_release-1.0.0-$engine.jar"}, + {"src": "android_release_x64/x86_64_release.maven-metadata.xml", "dst": "download.flutter.io/io/flutter/x86_64_release/1.0.0-$engine/x86_64_release-1.0.0-$engine.maven-metadata.xml"} + ] + }, + "host": { + "steps": [ + { + "type": "rust", + "targets": [ + "armv7-linux-androideabi", + "aarch64-linux-android", + "i686-linux-android", + "x86_64-linux-android", + "x86_64-unknown-linux-gnu" + ] + }, + { + "type": "gn_ninja", + "gn_args": ["--runtime-mode=release", "--no-prebuilt-dart-sdk"], + "ninja_targets": ["dart_sdk", "flutter/shell/platform/linux:flutter_gtk", "flutter/build/archives:flutter_patched_sdk"], + "out_dir": "host_release" + }, + { + "type": "gn_ninja", + "gn_args": ["--no-prebuilt-dart-sdk"], + "ninja_targets": ["flutter/build/archives:artifacts"], + "out_dir": "host_debug" + }, + { + "type": "shell", + "command": "mkdir -p out/host_release/aot_tools && out/host_release/dart-sdk/bin/dart pub get --offline --directory=flutter/third_party/dart/pkg/aot_tools && out/host_release/dart-sdk/bin/dart compile kernel flutter/third_party/dart/pkg/aot_tools/bin/aot_tools.dart -o out/host_release/aot_tools/aot-tools.dill" + } + ], + "artifacts": [ + {"src": "host_release/dart-sdk", "dst": "flutter_infra_release/flutter/$engine/dart-sdk-linux-x64.zip", "zip": true, "content_hash": true}, + {"src": "host_release/zip_archives/flutter_patched_sdk_product.zip", "dst": "flutter_infra_release/flutter/$engine/flutter_patched_sdk_product.zip"}, + {"src": "host_release/zip_archives/linux-x64-release/linux-x64-flutter-gtk.zip", "dst": "flutter_infra_release/flutter/$engine/linux-x64-release/linux-x64-flutter-gtk.zip"}, + {"src": "host_debug/zip_archives/linux-x64/artifacts.zip", "dst": "flutter_infra_release/flutter/$engine/linux-x64/artifacts.zip"}, + {"src": "host_release/aot_tools/aot-tools.dill", "dst": "shorebird/$engine/aot-tools.dill"} + ] + } +} diff --git a/shorebird/ci/shards/macos.json b/shorebird/ci/shards/macos.json new file mode 100644 index 0000000000000..0d1336c2be69f --- /dev/null +++ b/shorebird/ci/shards/macos.json @@ -0,0 +1,131 @@ +{ + "android": { + "steps": [ + { + "type": "rust", + "targets": [ + "aarch64-apple-darwin", + "x86_64-apple-darwin" + ] + }, + { + "type": "gn_ninja", + "gn_args": ["--android", "--android-cpu=arm64", "--runtime-mode=release", "--gn-args=host_cpu=\"x64\""], + "ninja_targets": ["flutter/shell/platform/android:gen_snapshot"], + "out_dir": "android_release_arm64" + }, + { + "type": "gn_ninja", + "gn_args": ["--android", "--runtime-mode=release", "--gn-args=host_cpu=\"x64\""], + "ninja_targets": ["flutter/shell/platform/android:gen_snapshot"], + "out_dir": "android_release" + }, + { + "type": "gn_ninja", + "gn_args": ["--android", "--android-cpu=x64", "--runtime-mode=release", "--gn-args=host_cpu=\"x64\""], + "ninja_targets": ["flutter/shell/platform/android:gen_snapshot"], + "out_dir": "android_release_x64" + } + ], + "artifacts": [ + {"src": "android_release_arm64/zip_archives/android-arm64-release/darwin-x64.zip", "dst": "flutter_infra_release/flutter/$engine/android-arm64-release/darwin-x64.zip"}, + {"src": "android_release/zip_archives/android-arm-release/darwin-x64.zip", "dst": "flutter_infra_release/flutter/$engine/android-arm-release/darwin-x64.zip"}, + {"src": "android_release_x64/zip_archives/android-x64-release/darwin-x64.zip", "dst": "flutter_infra_release/flutter/$engine/android-x64-release/darwin-x64.zip"} + ] + }, + "ios-release": { + "steps": [ + { + "type": "rust", + "targets": ["aarch64-apple-ios"] + }, + { + "type": "gn_ninja", + "gn_args": ["--ios", "--runtime-mode=release", "--gn-arg=shorebird_runtime=true"], + "ninja_targets": ["flutter/shell/platform/darwin/ios:flutter_framework", "flutter/lib/snapshot:generate_snapshot_bins"], + "out_dir": "ios_release" + } + ], + "compose_input": "ios-framework" + }, + "ios-sim-x64": { + "steps": [ + { + "type": "rust", + "targets": ["x86_64-apple-ios"] + }, + { + "type": "gn_ninja", + "gn_args": ["--ios", "--runtime-mode=debug", "--simulator"], + "ninja_targets": ["flutter/shell/platform/darwin/ios:flutter_framework", "flutter/lib/snapshot:generate_snapshot_bins"], + "out_dir": "ios_debug_sim" + } + ], + "compose_input": "ios-framework" + }, + "ios-sim-arm64": { + "steps": [ + { + "type": "gn_ninja", + "gn_args": ["--ios", "--runtime-mode=debug", "--simulator", "--simulator-cpu=arm64"], + "ninja_targets": ["flutter/shell/platform/darwin/ios:flutter_framework", "flutter/lib/snapshot:generate_snapshot_bins"], + "out_dir": "ios_debug_sim_arm64" + } + ], + "compose_input": "ios-framework" + }, + "mac-arm64": { + "steps": [ + { + "type": "rust", + "targets": ["aarch64-apple-darwin"] + }, + { + "type": "gn_ninja", + "gn_args": ["--runtime-mode=release", "--mac-cpu=arm64", "--no-prebuilt-dart-sdk"], + "ninja_targets": ["dart_sdk"], + "out_dir": "host_release_arm64" + }, + { + "type": "gn_ninja", + "gn_args": ["--runtime-mode=release", "--mac", "--mac-cpu=arm64"], + "ninja_targets": ["flutter/shell/platform/darwin/macos:zip_macos_flutter_framework", "flutter/lib/snapshot:generate_snapshot_bins", "flutter/build/archives:artifacts"], + "out_dir": "mac_release_arm64" + } + ], + "compose_input": "macos-framework", + "artifacts": [ + {"src": "host_release_arm64/dart-sdk", "dst": "flutter_infra_release/flutter/$engine/dart-sdk-darwin-arm64.zip", "zip": true, "content_hash": true}, + {"src": "mac_release_arm64/zip_archives/darwin-arm64-release/artifacts.zip", "dst": "flutter_infra_release/flutter/$engine/darwin-x64-release/artifacts.zip"} + ] + }, + "mac-x64": { + "steps": [ + { + "type": "rust", + "targets": ["x86_64-apple-darwin"] + }, + { + "type": "gn_ninja", + "gn_args": ["--runtime-mode=release", "--mac-cpu=x64", "--no-prebuilt-dart-sdk"], + "ninja_targets": ["dart_sdk"], + "out_dir": "host_release" + }, + { + "type": "gn_ninja", + "gn_args": ["--runtime-mode=release", "--mac", "--mac-cpu=x64"], + "ninja_targets": ["flutter/shell/platform/darwin/macos:zip_macos_flutter_framework", "flutter/lib/snapshot:generate_snapshot_bins", "flutter/build/archives:artifacts"], + "out_dir": "mac_release" + }, + { + "type": "shell", + "command": "flutter/bin/et stamp && cp out/engine_stamp.json out/mac_release/engine_stamp.json" + } + ], + "compose_input": "macos-framework", + "artifacts": [ + {"src": "host_release/dart-sdk", "dst": "flutter_infra_release/flutter/$engine/dart-sdk-darwin-x64.zip", "zip": true, "content_hash": true}, + {"src": "mac_release/engine_stamp.json", "dst": "flutter_infra_release/flutter/$engine/engine_stamp.json"} + ] + } +} diff --git a/shorebird/ci/shards/windows.json b/shorebird/ci/shards/windows.json new file mode 100644 index 0000000000000..4e5c47c71f0b5 --- /dev/null +++ b/shorebird/ci/shards/windows.json @@ -0,0 +1,76 @@ +{ + "android-arm64": { + "steps": [ + { + "type": "gn_ninja", + "gn_args": ["--android", "--android-cpu=arm64", "--runtime-mode=release"], + "ninja_targets": ["archive_win_gen_snapshot"], + "out_dir": "android_release_arm64" + } + ], + "artifacts": [ + {"src": "android_release_arm64/zip_archives/android-arm64-release/windows-x64.zip", "dst": "flutter_infra_release/flutter/$engine/android-arm64-release/windows-x64.zip"} + ] + }, + "android-arm32": { + "steps": [ + { + "type": "gn_ninja", + "gn_args": ["--android", "--runtime-mode=release"], + "ninja_targets": ["archive_win_gen_snapshot"], + "out_dir": "android_release" + } + ], + "artifacts": [ + {"src": "android_release/zip_archives/android-arm-release/windows-x64.zip", "dst": "flutter_infra_release/flutter/$engine/android-arm-release/windows-x64.zip"} + ] + }, + "android-x64": { + "steps": [ + { + "type": "gn_ninja", + "gn_args": ["--android", "--android-cpu=x64", "--runtime-mode=release"], + "ninja_targets": ["archive_win_gen_snapshot"], + "out_dir": "android_release_x64" + } + ], + "artifacts": [ + {"src": "android_release_x64/zip_archives/android-x64-release/windows-x64.zip", "dst": "flutter_infra_release/flutter/$engine/android-x64-release/windows-x64.zip"} + ] + }, + "host-release": { + "steps": [ + { + "type": "rust", + "targets": ["x86_64-pc-windows-msvc"] + }, + { + "type": "gn_ninja", + "gn_args": ["--runtime-mode=release", "--no-prebuilt-dart-sdk"], + "ninja_targets": ["dart_sdk", "flutter/build/archives:windows_flutter", "gen_snapshot", "windows", "flutter/build/archives:artifacts"], + "out_dir": "host_release" + } + ], + "artifacts": [ + {"src": "host_release/dart-sdk", "dst": "flutter_infra_release/flutter/$engine/dart-sdk-windows-x64.zip", "zip": true, "content_hash": true}, + {"src": "host_release/zip_archives/windows-x64-release/windows-x64-flutter.zip", "dst": "flutter_infra_release/flutter/$engine/windows-x64-release/windows-x64-flutter.zip"} + ] + }, + "host-debug": { + "steps": [ + { + "type": "rust", + "targets": ["x86_64-pc-windows-msvc"] + }, + { + "type": "gn_ninja", + "gn_args": ["--no-prebuilt-dart-sdk"], + "ninja_targets": ["flutter/build/archives:artifacts"], + "out_dir": "host_debug" + } + ], + "artifacts": [ + {"src": "host_debug/zip_archives/windows-x64/artifacts.zip", "dst": "flutter_infra_release/flutter/$engine/windows-x64/artifacts.zip"} + ] + } +} diff --git a/shorebird/ci/win_build_and_upload.sh b/shorebird/ci/win_build_and_upload.sh new file mode 100755 index 0000000000000..bbda403ff2c67 --- /dev/null +++ b/shorebird/ci/win_build_and_upload.sh @@ -0,0 +1,32 @@ +#!/bin/bash -e + +# Usage: +# ./win_build_and_upload.sh flutter_root engine_hash +# +# This is the main entrypoint for building and uploading Windows engine artifacts. +# It is called from the _build_engine repository's CI scripts. + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 flutter_root engine_hash" + exit 1 +fi + +FLUTTER_ROOT=$1 +ENGINE_HASH=$2 +ENGINE_ROOT=$FLUTTER_ROOT/engine + +# Get the absolute path to the directory of this script. +SCRIPT_DIR=$(cd $(dirname $0) && pwd) + +echo "Building engine at $ENGINE_ROOT and uploading to gs://download.shorebird.dev" + +cd $SCRIPT_DIR + +# Run the setup script. +./internal/win_setup.sh + +# Then run the build. +./internal/win_build.sh $ENGINE_ROOT + +# Copy Shorebird engine artifacts to Google Cloud Storage. +./internal/win_upload.sh $ENGINE_ROOT $ENGINE_HASH diff --git a/shorebird/docs/BUILDING.md b/shorebird/docs/BUILDING.md new file mode 100644 index 0000000000000..dfd3c655a1324 --- /dev/null +++ b/shorebird/docs/BUILDING.md @@ -0,0 +1,44 @@ +# Building the Shorebird Engine Locally + +This document explains how to build the Shorebird engine locally for different platforms. + +All commands assume you are running from the `engine/src` directory. + +## Prerequisites + +- Follow the standard Flutter engine setup instructions +- Ensure you have the necessary toolchains installed for your target platform + +## macOS + +### iOS (arm64) + +```bash +./flutter/tools/gn --no-rbe --no-enable-unittests --runtime-mode=release --ios --gn-arg='shorebird_runtime=true' +ninja -C out/ios_release flutter/shell/platform/darwin/ios:flutter_framework flutter/lib/snapshot:generate_snapshot_bins +``` + +### Android arm64 + +```bash +./flutter/tools/gn --no-rbe --no-enable-unittests --android --android-cpu=arm64 --runtime-mode=release --gn-args='host_cpu="x64"' +ninja -C out/android_release_arm64 flutter/shell/platform/android:gen_snapshot +``` + +## Windows + +### Android arm64 + +```bash +./flutter/tools/gn --no-rbe --no-enable-unittests --android --android-cpu=arm64 --runtime-mode=release +ninja -C out/android_release_arm64 archive_win_gen_snapshot +``` + +## Linux + +### Android arm64 + +```bash +./flutter/tools/gn --no-rbe --no-enable-unittests --android --android-cpu=arm64 --runtime-mode=release +ninja -C out/android_release_arm64 default gen_snapshot +```