diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index 9a6494dfab..8bcdb32269 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -22,7 +22,7 @@ jobs: run: | git fetch --no-tags --prune --depth=1 origin ${{ github.event.pull_request.base.sha }} git fetch --no-tags --prune --depth=1 origin master - echo "FLUTTER_ENGINE_VERSION=${{ github.event.pull_request.base.sha }}" >> "$GITHUB_ENV" + echo "FLUTTER_PREBUILT_ENGINE_VERSION=${{ github.event.pull_request.base.sha }}" >> "$GITHUB_ENV" - name: Initialize Dart SDK # This downloads the version of the Dart SDK for the current platform. diff --git a/bin/internal/update_engine_version.ps1 b/bin/internal/update_engine_version.ps1 index 77bedc81f9..846ffbe190 100644 --- a/bin/internal/update_engine_version.ps1 +++ b/bin/internal/update_engine_version.ps1 @@ -2,6 +2,9 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +# Want to test this script? +# $ cd dev/tools +# $ dart test test/update_engine_version_test.dart # ---------------------------------- NOTE ---------------------------------- # # @@ -11,13 +14,28 @@ # # -------------------------------------------------------------------------- # -$ErrorActionPreference = "Stop" - $progName = Split-Path -parent $MyInvocation.MyCommand.Definition $flutterRoot = (Get-Item $progName).parent.parent.FullName +# Allow overriding the intended engine version via FLUTTER_PREBUILT_ENGINE_VERSION. +# +# This is for systems, such as Github Actions, where we know ahead of time the +# base-ref we want to use (to download the engine binaries and avoid trying +# to compute one below), or for the Dart HH bot, which wants to try the current +# Flutter framework/engine with a different Dart SDK. +# +# This environment variable is EXPERIMENTAL. If you are not on the Flutter infra +# or Dart infra teams, this code path might be removed at anytime and cease +# functioning. Please file an issue if you have workflow needs. +if (![string]::IsNullOrEmpty($env:FLUTTER_PREBUILT_ENGINE_VERSION)) { + $engineVersion = $env:FLUTTER_PREBUILT_ENGINE_VERSION + Write-Error "[Unstable] Override: Setting engine SHA to $engineVersion" +} + +$ErrorActionPreference = "Stop" + # Test for fusion repository -if ((Test-Path "$flutterRoot\DEPS" -PathType Leaf) -and (Test-Path "$flutterRoot\engine\src\.gn" -PathType Leaf)) { +if ([string]::IsNullOrEmpty($engineVersion) -and (Test-Path "$flutterRoot\DEPS" -PathType Leaf) -and (Test-Path "$flutterRoot\engine\src\.gn" -PathType Leaf)) { # Calculate the engine hash from tracked git files. $branch = (git -C "$flutterRoot" rev-parse --abbrev-ref HEAD) if ($null -eq $Env:LUCI_CONTEXT) { @@ -34,15 +52,15 @@ if ((Test-Path "$flutterRoot\DEPS" -PathType Leaf) -and (Test-Path "$flutterRoot else { $engineVersion = (git -C "$flutterRoot" rev-parse HEAD) } +} - if (($branch -ne "stable" -and $branch -ne "beta")) { - # Write the engine version out so downstream tools know what to look for. - $utf8NoBom = New-Object System.Text.UTF8Encoding($false) - [System.IO.File]::WriteAllText("$flutterRoot\bin\internal\engine.version", $engineVersion, $utf8NoBom) +if (($branch -ne "stable" -and $branch -ne "beta")) { + # Write the engine version out so downstream tools know what to look for. + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText("$flutterRoot\bin\internal\engine.version", $engineVersion, $utf8NoBom) - # The realm on CI is passed in. - if ($Env:FLUTTER_REALM) { - [System.IO.File]::WriteAllText("$flutterRoot\bin\internal\engine.realm", $Env:FLUTTER_REALM, $utf8NoBom) - } + # The realm on CI is passed in. + if ($Env:FLUTTER_REALM) { + [System.IO.File]::WriteAllText("$flutterRoot\bin\internal\engine.realm", $Env:FLUTTER_REALM, $utf8NoBom) } -} \ No newline at end of file +} diff --git a/bin/internal/update_engine_version.sh b/bin/internal/update_engine_version.sh index 67f748000b..a35fa190aa 100755 --- a/bin/internal/update_engine_version.sh +++ b/bin/internal/update_engine_version.sh @@ -3,6 +3,9 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +# Want to test this script? +# $ cd dev/tools +# $ dart test test/update_engine_version_test.dart # ---------------------------------- NOTE ---------------------------------- # # @@ -14,17 +17,18 @@ set -e -# Allow overriding the intended engine version via FLUTTER_ENGINE_VERSION. +# Allow overriding the intended engine version via FLUTTER_PREBUILT_ENGINE_VERSION. # # This is for systems, such as Github Actions, where we know ahead of time the # base-ref we want to use (to download the engine binaries and avoid trying -# to compute one below). +# to compute one below), or for the Dart HH bot, which wants to try the current +# Flutter framework/engine with a different Dart SDK. # # This environment variable is EXPERIMENTAL. If you are not on the Flutter infra -# team, this code path might be removed at anytime and cease functioning. Please -# file an issue if you have workflow needs. -if [ -n "${FLUTTER_ENGINE_VERSION}" ]; then - ENGINE_VERSION="${FLUTTER_ENGINE_VERSION}" +# or Dart infra teams, this code path might be removed at anytime and cease +# functioning. Please file an issue if you have workflow needs. +if [ -n "${FLUTTER_PREBUILT_ENGINE_VERSION}" ]; then + ENGINE_VERSION="${FLUTTER_PREBUILT_ENGINE_VERSION}" echo "[Unstable] Override: Setting engine SHA to $ENGINE_VERSION" 1>&2 fi diff --git a/dev/tools/pubspec.yaml b/dev/tools/pubspec.yaml index 0e36bf1324..4a7ff05e16 100644 --- a/dev/tools/pubspec.yaml +++ b/dev/tools/pubspec.yaml @@ -33,6 +33,8 @@ dev_dependencies: test: 1.25.14 test_api: 0.7.4 + file_testing: 3.0.2 + _fe_analyzer_shared: 76.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" analyzer: 6.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -64,4 +66,4 @@ dev_dependencies: web_socket_channel: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: 1d50 +# PUBSPEC CHECKSUM: 7e9e diff --git a/dev/tools/test/update_engine_version_test.dart b/dev/tools/test/update_engine_version_test.dart new file mode 100644 index 0000000000..b5de06509b --- /dev/null +++ b/dev/tools/test/update_engine_version_test.dart @@ -0,0 +1,307 @@ +// 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. + +@TestOn('vm') +library; + +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:file_testing/file_testing.dart'; +import 'package:platform/platform.dart'; +import 'package:process_runner/process_runner.dart'; +import 'package:test/test.dart'; + +void main() { + const FileSystem localFs = LocalFileSystem(); + final _FlutterRootUnderTest flutterRoot = _FlutterRootUnderTest.findWithin(); + + late Directory tmpDir; + late _FlutterRootUnderTest testRoot; + late Map environment; + late ProcessRunner processRunner; + + setUp(() async { + tmpDir = localFs.systemTempDirectory.createTempSync('update_engine_version_test.'); + testRoot = _FlutterRootUnderTest.fromPath(tmpDir.childDirectory('flutter').path); + + environment = {}; + processRunner = ProcessRunner( + defaultWorkingDirectory: testRoot.root, + environment: environment, + printOutputDefault: true, + ); + + // Copy the update_engine_version script and create a rough directory structure. + flutterRoot.binInternalUpdateEngineVersion.copySyncRecursive( + testRoot.binInternalUpdateEngineVersion.path, + ); + + // On some systems, copying the file means losing the executable bit. + if (const LocalPlatform().isWindows) { + await processRunner.runProcess([ + 'attrib', + '+x', + testRoot.binInternalUpdateEngineVersion.path, + ]); + } + }); + + tearDown(() { + tmpDir.deleteSync(recursive: true); + }); + + Future runUpdateEngineVersion() async { + if (const LocalPlatform().isWindows) { + await processRunner.runProcess([ + 'powershell', + testRoot.binInternalUpdateEngineVersion.path, + ]); + } else { + await processRunner.runProcess([testRoot.binInternalUpdateEngineVersion.path]); + } + } + + group('if FLUTTER_PREBUILT_ENGINE_VERSION is set', () { + setUp(() { + environment['FLUTTER_PREBUILT_ENGINE_VERSION'] = '123abc'; + }); + + test('writes it to engine.version with no git interaction', () async { + await runUpdateEngineVersion(); + + expect(testRoot.binInternalEngineVersion, exists); + expect( + testRoot.binInternalEngineVersion.readAsStringSync(), + equalsIgnoringWhitespace('123abc'), + ); + }); + }); + + Future setupRepo({required String branch}) async { + for (final File f in [testRoot.deps, testRoot.engineSrcGn]) { + f.createSync(recursive: true); + } + + await processRunner.runProcess(['git', 'init', '--initial-branch', 'master']); + await processRunner.runProcess(['git', 'add', '.']); + await processRunner.runProcess(['git', 'commit', '-m', 'Initial commit']); + if (branch != 'master') { + await processRunner.runProcess(['git', 'checkout', '-b', branch]); + } + } + + Future setupRemote({required String remote}) async { + await processRunner.runProcess(['git', 'remote', 'add', remote, testRoot.root.path]); + await processRunner.runProcess(['git', 'fetch', remote]); + } + + test('writes nothing, even if files are set, if we are on "stable"', () async { + await setupRepo(branch: 'stable'); + await setupRemote(remote: 'upstream'); + + await runUpdateEngineVersion(); + + expect(testRoot.binInternalEngineVersion, isNot(exists)); + }); + + test('writes nothing, even if files are set, if we are on "beta"', () async { + await setupRepo(branch: 'beta'); + await setupRemote(remote: 'upstream'); + + await runUpdateEngineVersion(); + + expect(testRoot.binInternalEngineVersion, isNot(exists)); + }); + + group('if DEPS and engine/src/.gn are present, engine.version is derived from', () { + setUp(() async { + await setupRepo(branch: 'master'); + }); + + test('merge-base HEAD upstream/master on non-LUCI when upstream is set', () async { + await setupRemote(remote: 'upstream'); + + final ProcessRunnerResult mergeBaseHeadUpstream = await processRunner.runProcess([ + 'git', + 'merge-base', + 'HEAD', + 'upstream/master', + ]); + await runUpdateEngineVersion(); + + expect(testRoot.binInternalEngineVersion, exists); + expect( + testRoot.binInternalEngineVersion.readAsStringSync(), + equalsIgnoringWhitespace(mergeBaseHeadUpstream.stdout), + ); + }); + + test('merge-base HEAD origin/master on non-LUCI when upstream is not set', () async { + await setupRemote(remote: 'origin'); + + final ProcessRunnerResult mergeBaseHeadOrigin = await processRunner.runProcess([ + 'git', + 'merge-base', + 'HEAD', + 'origin/master', + ]); + await runUpdateEngineVersion(); + + expect(testRoot.binInternalEngineVersion, exists); + expect( + testRoot.binInternalEngineVersion.readAsStringSync(), + equalsIgnoringWhitespace(mergeBaseHeadOrigin.stdout), + ); + }); + + test('rev-parse HEAD when running on LUCI', () async { + environment['LUCI_CONTEXT'] = '_NON_NULL_AND_NON_EMPTY_STRING'; + await runUpdateEngineVersion(); + + final ProcessRunnerResult revParseHead = await processRunner.runProcess([ + 'git', + 'rev-parse', + 'HEAD', + ]); + expect(testRoot.binInternalEngineVersion, exists); + expect( + testRoot.binInternalEngineVersion.readAsStringSync(), + equalsIgnoringWhitespace(revParseHead.stdout), + ); + }); + }); + + group('if DEPS or engine/src/.gn are omitted', () { + setUp(() { + for (final File f in [testRoot.deps, testRoot.engineSrcGn]) { + f.createSync(recursive: true); + } + }); + + test('[DEPS] engine.version is blank', () async { + testRoot.deps.deleteSync(); + + await runUpdateEngineVersion(); + + expect(testRoot.binInternalEngineVersion, exists); + expect(testRoot.binInternalEngineVersion.readAsStringSync(), equalsIgnoringWhitespace('')); + }); + + test('[engine/src/.gn] engine.version is blank', () async { + testRoot.engineSrcGn.deleteSync(); + + await runUpdateEngineVersion(); + + expect(testRoot.binInternalEngineVersion, exists); + expect(testRoot.binInternalEngineVersion.readAsStringSync(), equalsIgnoringWhitespace('')); + }); + }); +} + +/// A FrUT, or "Flutter Root"-Under Test (parallel to a SUT, System Under Test). +/// +/// For the intent of this test case, the "Flutter Root" is a directory +/// structure with the following elements: +/// +/// ```txt +/// ├── bin +/// │ ├── internal +/// │ │ ├── engine.version +/// │ │ ├── engine.realm +/// │ │ └── update_engine_version.{sh|ps1} +/// │ └── engine +/// │ └── src +/// │ └── .gn +/// └── DEPS +/// ``` +final class _FlutterRootUnderTest { + /// Creates a root-under test using [path] as the root directory. + /// + /// It is assumed the files already exist or will be created if needed. + factory _FlutterRootUnderTest.fromPath( + String path, { + FileSystem fileSystem = const LocalFileSystem(), + Platform platform = const LocalPlatform(), + }) { + final Directory root = fileSystem.directory(path); + return _FlutterRootUnderTest._( + root, + deps: root.childFile('DEPS'), + engineSrcGn: root.childFile(fileSystem.path.join('engine', 'src', '.gn')), + binInternalEngineVersion: root.childFile( + fileSystem.path.join('bin', 'internal', 'engine.version'), + ), + binInternalEngineRealm: root.childFile( + fileSystem.path.join('bin', 'internal', 'engine.realm'), + ), + binInternalUpdateEngineVersion: root.childFile( + fileSystem.path.join( + 'bin', + 'internal', + 'update_engine_version.${platform.isWindows ? 'ps1' : 'sh'}', + ), + ), + ); + } + + factory _FlutterRootUnderTest.findWithin([ + String? path, + FileSystem fileSystem = const LocalFileSystem(), + ]) { + path ??= fileSystem.currentDirectory.path; + Directory current = fileSystem.directory(path); + while (!current.childFile('DEPS').existsSync()) { + if (current.path == current.parent.path) { + throw ArgumentError.value(path, 'path', 'Could not resolve flutter root'); + } + current = current.parent; + } + return _FlutterRootUnderTest.fromPath(current.path); + } + + const _FlutterRootUnderTest._( + this.root, { + required this.deps, + required this.engineSrcGn, + required this.binInternalEngineVersion, + required this.binInternalEngineRealm, + required this.binInternalUpdateEngineVersion, + }); + + final Directory root; + + /// `DEPS`. + /// + /// The presenence of this file is an indicator we are in a fused (mono) repo. + final File deps; + + /// `engine/src/.gn`. + /// + /// The presenence of this file is an indicator we are in a fused (mono) repo. + final File engineSrcGn; + + /// `bin/internal/engine.version`. + /// + /// This file contains a SHA of which engine binaries to download. + final File binInternalEngineVersion; + + /// `bin/internal/engine.realm`. + /// + /// It is a mystery what this file contains, but it's set by `FLUTTER_REALM`. + final File binInternalEngineRealm; + + /// `bin/internal/update_engine_version.{sh|ps1}`. + /// + /// This file contains a shell script that conditionally writes, on execution: + /// - [binInternalEngineVersion] + /// - [binInternalEngineRealm] + final File binInternalUpdateEngineVersion; +} + +extension on File { + void copySyncRecursive(String newPath) { + fileSystem.directory(fileSystem.path.dirname(newPath)).createSync(recursive: true); + copySync(newPath); + } +} diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index e0ab8373e2..642eecc8ed 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: archive: 3.6.1 args: 2.6.0 dds: 5.0.0 - dwds: 24.3.2 + dwds: 24.3.3 completion: 1.0.1 coverage: 1.11.1 crypto: 3.0.6 @@ -121,4 +121,4 @@ dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: b23b +# PUBSPEC CHECKSUM: cf3c