From 6e02e13c6626bfa9810311fb90824fb021989935 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Tue, 6 May 2025 10:00:40 -0700 Subject: [PATCH] Add bin/internal/last_engine_commit.sh and tests. --- bin/internal/last_engine_commit.ps1 | 28 +++ bin/internal/last_engine_commit.sh | 29 +++ dev/bots/analyze.dart | 1 + dev/tools/test/last_engine_commit_test.dart | 259 ++++++++++++++++++++ docs/tool/Engine-artifacts.md | 17 +- 5 files changed, 331 insertions(+), 3 deletions(-) create mode 100644 bin/internal/last_engine_commit.ps1 create mode 100755 bin/internal/last_engine_commit.sh create mode 100644 dev/tools/test/last_engine_commit_test.dart diff --git a/bin/internal/last_engine_commit.ps1 b/bin/internal/last_engine_commit.ps1 new file mode 100644 index 0000000000000..b652c158ab2cb --- /dev/null +++ b/bin/internal/last_engine_commit.ps1 @@ -0,0 +1,28 @@ +# 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. + +# Based on the current repository state, writes on stdout the last commit in the +# git tree that edited either `DEPS` or any file in the `engine/` sub-folder, +# which is used to ensure `bin/internal/engine.version` is set correctly. + +# ---------------------------------- NOTE ---------------------------------- # +# +# Please keep the logic in this file consistent with the logic in the +# `last_engine_commit.sh` script in the same directory to ensure that Flutter +# continues to work across all platforms! +# +# https://github.com/flutter/flutter/blob/main/docs/tool/Engine-artifacts.md. +# +# Want to test this script? +# $ cd dev/tools +# $ dart test test/last_engine_commit_test.dart +# +# -------------------------------------------------------------------------- # + +$ErrorActionPreference = "Stop" + +$progName = Split-Path -parent $MyInvocation.MyCommand.Definition +$flutterRoot = (Get-Item $progName).parent.parent.FullName + +cmd /c "git log -1 --pretty=format:%H -- ""$(git rev-parse --show-toplevel)/DEPS"" ""$(git rev-parse --show-toplevel)/engine""" diff --git a/bin/internal/last_engine_commit.sh b/bin/internal/last_engine_commit.sh new file mode 100755 index 0000000000000..47ed1b3e91113 --- /dev/null +++ b/bin/internal/last_engine_commit.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# 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. + +# Based on the current repository state, writes on stdout the last commit in the +# git tree that edited either `DEPS` or any file in the `engine/` sub-folder, +# which is used to ensure `bin/internal/engine.version` is set correctly. +# + +# ---------------------------------- NOTE ---------------------------------- # +# +# Please keep the logic in this file consistent with the logic in the +# `last_engine_commit.ps1` script in the same directory to ensure that Flutter +# continues to work across all platforms! +# +# https://github.com/flutter/flutter/blob/main/docs/tool/Engine-artifacts.md. +# +# Want to test this script? +# $ cd dev/tools +# $ dart test test/last_engine_commit_test.dart +# +# -------------------------------------------------------------------------- # + +set -e + +FLUTTER_ROOT="$(dirname "$(dirname "$(dirname "${BASH_SOURCE[0]}")")")" + +git log -1 --pretty=format:%H -- "$(git rev-parse --show-toplevel)/DEPS" "$(git rev-parse --show-toplevel)/engine" diff --git a/dev/bots/analyze.dart b/dev/bots/analyze.dart index 2d01a9e7cde41..38703a7a36637 100644 --- a/dev/bots/analyze.dart +++ b/dev/bots/analyze.dart @@ -2660,6 +2660,7 @@ const Set kExecutableAllowlist = { 'bin/dart', 'bin/flutter', 'bin/flutter-dev', + 'bin/internal/last_engine_commit.sh', 'bin/internal/update_dart_sdk.sh', 'bin/internal/update_engine_version.sh', 'bin/internal/content_aware_hash.sh', diff --git a/dev/tools/test/last_engine_commit_test.dart b/dev/tools/test/last_engine_commit_test.dart new file mode 100644 index 0000000000000..1d62ad5712aed --- /dev/null +++ b/dev/tools/test/last_engine_commit_test.dart @@ -0,0 +1,259 @@ +// 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 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +////////////////////////////////////////////////////////////////////// +// // +// ✨ THINKING OF MOVING/REFACTORING THIS FILE? READ ME FIRST! ✨ // +// // +// There is a link to this file in //docs/tool/Engine-artfiacts.md // +// and it would be very kind of you to update the link, if needed. // +// // +////////////////////////////////////////////////////////////////////// + +void main() { + // Want to test the powershell (update_engine_version.ps1) file, but running + // a macOS or Linux machine? You can install powershell and then opt-in to + // running `pwsh bin/internal/update_engine_version.ps1`. + // + // macOS: https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-macos + // linux: https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-linux + // + // Then, set this variable to true: + final bool usePowershellOnPosix = () { + // Intentionally not a const so that linting doesn't go wild across the test. + return false; + }(); + + const FileSystem localFs = LocalFileSystem(); + final _FlutterRootUnderTest flutterRoot = _FlutterRootUnderTest.findWithin( + forcePowershell: usePowershellOnPosix, + ); + + late Directory tmpDir; + late _FlutterRootUnderTest testRoot; + late Map environment; + + void printIfNotEmpty(String prefix, String string) { + if (string.isNotEmpty) { + string.split(io.Platform.lineTerminator).forEach((String s) { + print('$prefix:>$s<'); + }); + } + } + + io.ProcessResult run(String executable, List args) { + print('Running "$executable ${args.join(" ")}'); + final io.ProcessResult result = io.Process.runSync( + executable, + args, + environment: environment, + workingDirectory: testRoot.root.absolute.path, + includeParentEnvironment: false, + ); + if (result.exitCode != 0) { + fail( + 'Failed running "$executable $args" (exit code = ${result.exitCode}),' + '\nstdout: ${result.stdout}' + '\nstderr: ${result.stderr}', + ); + } + printIfNotEmpty('stdout', (result.stdout as String).trim()); + printIfNotEmpty('stderr', (result.stderr as String).trim()); + return result; + } + + setUpAll(() async { + if (usePowershellOnPosix) { + final io.ProcessResult result = io.Process.runSync('pwsh', ['--version']); + print('Using Powershell (${result.stdout}) on POSIX for local debugging and testing'); + } + }); + + /// Initializes a blank git repo in [testRoot.root]. + void initGitRepoWithBlankInitialCommit() { + run('git', ['init', '--initial-branch', 'master']); + run('git', ['config', '--local', 'user.email', 'test@example.com']); + run('git', ['config', '--local', 'user.name', 'Test User']); + run('git', ['add', '.']); + run('git', ['commit', '--allow-empty', '-m', 'Initial commit']); + } + + late int commitCount; + + setUp(() async { + commitCount = 0; + + tmpDir = localFs.systemTempDirectory.createTempSync('last_engine_commit_test.'); + testRoot = _FlutterRootUnderTest.fromPath( + tmpDir.childDirectory('flutter').path, + forcePowershell: usePowershellOnPosix, + ); + + environment = {}; + + if (const LocalPlatform().isWindows || usePowershellOnPosix) { + // Copy a minimal set of environment variables needed to run the update_engine_version script in PowerShell. + const List powerShellVariables = ['SystemRoot', 'Path', 'PATHEXT']; + for (final String key in powerShellVariables) { + final String? value = io.Platform.environment[key]; + if (value != null) { + environment[key] = value; + } + } + } + + // Copy the update_engine_version script and create a rough directory structure. + flutterRoot.binInternalLastEngineCommit.copySyncRecursive( + testRoot.binInternalLastEngineCommit.path, + ); + + initGitRepoWithBlankInitialCommit(); + }); + + tearDown(() { + tmpDir.deleteSync(recursive: true); + }); + + /// Runs `bin/internal/last_engine_commit.{sh|ps1}` and returns the stdout. + /// + /// - On Windows, `powershell` is used (to run `last_engine_commit.ps1`); + /// - On POSIX, if [usePowershellOnPosix] is set, `pwsh` is used (to run `last_engine_commit.ps1`); + /// - Otherwise, `last_engine_commit.sh` is used. + String getLastEngineCommit() { + final String executable; + final List args; + if (const LocalPlatform().isWindows) { + executable = 'powershell'; + args = [testRoot.binInternalLastEngineCommit.path]; + } else if (usePowershellOnPosix) { + executable = 'pwsh'; + args = [testRoot.binInternalLastEngineCommit.path]; + } else { + executable = testRoot.binInternalLastEngineCommit.path; + args = []; + } + return run(executable, args).stdout as String; + } + + void writeCommit(Iterable files) { + commitCount++; + for (final String relativePath in files) { + localFs.file(localFs.path.join(testRoot.root.path, relativePath)) + ..createSync(recursive: true) + ..writeAsStringSync('$commitCount'); + } + run('git', ['add', '.']); + run('git', ['commit', '-m', 'Wrote ${files.length} files']); + } + + test('returns the last engine commit', () { + writeCommit(['DEPS', 'engine/README.md']); + + final String lastEngine = getLastEngineCommit(); + expect(lastEngine, isNotEmpty); + + writeCommit(['CHANGELOG.md', 'dev/folder/called/engine/README.md']); + expect(getLastEngineCommit(), lastEngine); + }); + + test('considers DEPS an engine change', () { + writeCommit(['DEPS', 'engine/README.md']); + + final String lastEngineA = getLastEngineCommit(); + expect(lastEngineA, isNotEmpty); + + writeCommit(['DEPS']); + final String lastEngineB = getLastEngineCommit(); + expect(lastEngineB, allOf(isNotEmpty, isNot(equals(lastEngineA)))); + }); +} + +extension on File { + void copySyncRecursive(String newPath) { + fileSystem.directory(fileSystem.path.dirname(newPath)).createSync(recursive: true); + copySync(newPath); + } +} + +/// 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 +/// ├── DEPS +/// ├── engine/ +/// ├── bin/ +/// │ ├── internal/ +/// │ │ └── last_engine_commit.{sh|ps1} +/// ``` +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(), + bool forcePowershell = false, + }) { + final Directory root = fileSystem.directory(path); + return _FlutterRootUnderTest._( + root, + depsFile: root.childFile('DEPS'), + engineDirectory: root.childDirectory('engine'), + binInternalLastEngineCommit: root.childFile( + fileSystem.path.join( + 'bin', + 'internal', + 'last_engine_commit.${platform.isWindows || forcePowershell ? 'ps1' : 'sh'}', + ), + ), + ); + } + + factory _FlutterRootUnderTest.findWithin({ + String? path, + FileSystem fileSystem = const LocalFileSystem(), + bool forcePowershell = false, + }) { + 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, forcePowershell: forcePowershell); + } + + const _FlutterRootUnderTest._( + this.root, { + required this.binInternalLastEngineCommit, + required this.depsFile, + required this.engineDirectory, + }); + + final Directory root; + + /// `DEPS`. + final File depsFile; + + /// The `engine/` directory. + final Directory engineDirectory; + + /// `bin/internal/last_engine_commit.{sh|ps1}`. + final File binInternalLastEngineCommit; +} diff --git a/docs/tool/Engine-artifacts.md b/docs/tool/Engine-artifacts.md index b5d34e65a1c97..24efe32fb20a8 100644 --- a/docs/tool/Engine-artifacts.md +++ b/docs/tool/Engine-artifacts.md @@ -61,9 +61,17 @@ On Cocoon (Flutter's internal CI/CD) we _often_ set | `main` | `commit.sha` | _Uses normal flow_ | _Uses normal flow_ | | `flutter-x.x-candidate.x` | `commit.sha` | N/A[^1] | `commit.sha` | -> NOTE: `engine.version` is intentionally ignored in release candidate -> post-submit builds. See -> [#167010](https://github.com/flutter/flutter/issues/167010). +> IMPORTANT: `engine.version` is intentionally ignored in release candidate +> post-submit builds. +> +> To generate a new `engine.version`: +> +> ```sh +> ./bin/internal/last_engine_commit.sh > ./bin/internal/engine.version +> ``` +> +> At the moment this needs to be manually done, and manually verified, before +> making a release. See [#168273](https://github.com/flutter/flutter/issues/168273). [^1]: Release candidates do not use a merge queue. @@ -74,6 +82,9 @@ The script(s) that compute (and test the computation of) the engine version: - [`bin/internal/update_engine_version.sh`](../../bin/internal/update_engine_version.sh) - [`bin/internal/update_engine_version.ps1`](../../bin/internal/update_engine_version.ps1) - [`dev/tools/test/update_engine_version_test.dart`](../../dev/tools/test/update_engine_version_test.dart) +- [`bin/internal/last_engine_commit.sh`](../../bin/internal/last_engine_commit.sh) +- [`bin/internal/last_engine_commit.ps1`](../../bin/internal/last_engine_commit.ps1) +- [`dev/tools/test/last_engine_commit_test.dart`](../../dev/tools/test/last_engine_commit_test.dart) - [`bin/internal/content_aware_hash.sh`](../../bin/internal/content_aware_hash.sh) - [`bin/internal/content_aware_hash.ps1`](../../bin/internal/content_aware_hash.ps1) - [`dev/tools/test/content_aware_hash_test.dart`](../../dev/tools/test/content_aware_hash_test.dart)