diff --git a/dev/bots/analyze.dart b/dev/bots/analyze.dart index f6e08e8646..8edafdff23 100644 --- a/dev/bots/analyze.dart +++ b/dev/bots/analyze.dart @@ -2579,6 +2579,7 @@ const Set kExecutableAllowlist = { 'dev/tools/repackage_gradle_wrapper.sh', 'dev/tools/bin/engine_hash.sh', 'dev/tools/format.sh', + 'dev/tools/test/mock_git.sh', 'packages/flutter_tools/bin/macos_assemble.sh', 'packages/flutter_tools/bin/tool_backend.sh', diff --git a/dev/tools/bin/engine_hash.dart b/dev/tools/bin/engine_hash.dart deleted file mode 100644 index a8d7bb454c..0000000000 --- a/dev/tools/bin/engine_hash.dart +++ /dev/null @@ -1,154 +0,0 @@ -// 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. - -// ---------------------------------- NOTE ---------------------------------- -// -// We must keep the logic in this file consistent with the logic in the -// `engine_hash.sh` script in the same directory to ensure that Flutter -// continues to work across all platforms! -// -// -------------------------------------------------------------------------- - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:args/args.dart'; -import 'package:crypto/crypto.dart'; - -enum GitRevisionStrategy { mergeBase, head } - -final RegExp _hashRegex = RegExp(r'^([a-fA-F0-9]+)'); - -final ArgParser parser = - ArgParser() - ..addOption( - 'strategy', - abbr: 's', - allowed: ['head', 'mergeBase'], - defaultsTo: 'head', - allowedHelp: { - 'head': 'hash from git HEAD', - 'mergeBase': 'hash from the merge-base of HEAD and upstream/master', - }, - ) - ..addFlag('help', abbr: 'h', negatable: false); - -Never printHelp({String? error}) { - final Stdout out = error != null ? stderr : stdout; - if (error != null) { - out.writeln(error); - out.writeln(); - } - out.writeln(''' -Calculate the hash signature for the Flutter Engine -${parser.usage} -'''); - exit(error != null ? 1 : 0); -} - -Future main(List args) async { - final ArgResults arguments; - try { - arguments = parser.parse(args); - } catch (e) { - printHelp(error: '$e'); - } - - if (arguments.wasParsed('help')) { - printHelp(); - } - - final String result; - try { - result = await engineHash( - (List command) => - Process.run(command.first, command.sublist(1), stdoutEncoding: utf8), - revisionStrategy: GitRevisionStrategy.values.byName(arguments.option('strategy')!), - ); - } catch (e) { - stderr.writeln('Error calculating engine hash: $e'); - return 1; - } - - stdout.writeln(result); - - return 0; -} - -/// Returns the hash signature for the engine source code. -Future engineHash( - Future Function(List command) runProcess, { - GitRevisionStrategy revisionStrategy = GitRevisionStrategy.mergeBase, -}) async { - // First figure out the hash we're working with - final String base; - switch (revisionStrategy) { - case GitRevisionStrategy.head: - base = 'HEAD'; - case GitRevisionStrategy.mergeBase: - final ProcessResult processResult = await runProcess([ - 'git', - 'merge-base', - 'upstream/master', - 'HEAD', - ]); - - if (processResult.exitCode != 0) { - throw ''' -Unable to find merge-base hash of the repository: -${processResult.stderr}'''; - } - - final Match? baseHash = _hashRegex.matchAsPrefix(processResult.stdout as String); - if (baseHash?.groupCount != 1) { - throw ''' -Unable to parse merge-base hash of the repository -${processResult.stdout}'''; - } - base = baseHash![1]!; - } - - // List the tree (not the working tree) recursively for the merge-base. - // This is important for future filtering of files, but also do not include - // the developer's changes / in flight PRs. - // The presence `engine` and `DEPS` are signals that you live in a monorepo world. - final ProcessResult processResult = await runProcess([ - 'git', - 'ls-tree', - '-r', - base, - 'engine', - 'DEPS', - ]); - - if (processResult.exitCode != 0) { - throw ''' -Unable to list tree -${processResult.stderr}'''; - } - - // Ensure stable line endings so our hash calculation is stable - final String lsTree = processResult.stdout as String; - if (lsTree.trim().isEmpty) { - throw 'Not in a monorepo'; - } - - final Iterable treeLines = LineSplitter.split(processResult.stdout as String); - - // We could call `git hash-object --stdin` which would just take the input, calculate the size, - // and then sha1sum it like: `blob $size\0$string'. However, that can have different line endings. - // Instead this is equivalent to: - // git ls-tree -r $(git merge-base upstream/main HEAD) | | sha1sum - final StreamController output = StreamController(); - final ByteConversionSink sink = sha1.startChunkedConversion(output); - for (final String line in treeLines) { - sink.add(utf8.encode(line)); - sink.add([0x0a]); - } - sink.close(); - final Digest digest = await output.stream.first; - - return '$digest'; -} diff --git a/dev/tools/bin/engine_hash.sh b/dev/tools/bin/engine_hash.sh index c3e71bee6a..348ad39c67 100755 --- a/dev/tools/bin/engine_hash.sh +++ b/dev/tools/bin/engine_hash.sh @@ -5,70 +5,65 @@ # ---------------------------------- NOTE ---------------------------------- # # -# We must keep the logic in this file consistent with the logic in the -# `engine_hash.dart` script in the same directory to ensure that Flutter -# continues to work across all platforms! +# This file will appear unused within the monorepo. It is used internally +# (in google3) as part of the roll process, and care should be put before +# making changes. +# +# See cl/688973229. # # -------------------------------------------------------------------------- # -# TODO(codefu): Add a test that this always outputs the same hash as -# `engine_hash.dart` when the repositories are merged +# Needed because if it is set, cd may print the path it changed to. +unset CDPATH -STRATEGY=head - -HELP=$( - cat <\n -\t\tthead: hash from git HEAD\n -\t\tmergeBase: hash from the merge-base of HEAD and upstream/master\n -EOF +# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one +# link at a time, and then cds into the link destination and find out where it +# ends up. +# +# The returned filesystem path must be a format usable by Dart's URI parser, +# since the Dart command line tool treats its argument as a file URI, not a +# filename. For instance, multiple consecutive slashes should be reduced to a +# single slash, since double-slashes indicate a URI "authority", and these are +# supposed to be filenames. There is an edge case where this will return +# multiple slashes: when the input resolves to the root directory. However, if +# that were the case, we wouldn't be running this shell, so we don't do anything +# about it. +# +# The function is enclosed in a subshell to avoid changing the working directory +# of the caller. +function follow_links() ( + cd -P "$(dirname -- "$1")" + file="$PWD/$(basename -- "$1")" + while [[ -h "$file" ]]; do + cd -P "$(dirname -- "$file")" + file="$(readlink -- "$file")" + cd -P "$(dirname -- "$file")" + file="$PWD/$(basename -- "$file")" + done + echo "$file" ) -function print_help() { - if [ "${1:-0}" -eq 0 ]; then - echo -e $HELP - exit 0 - else - echo >&2 -e $HELP - exit $1 - fi -} +PROG_NAME="$(follow_links "${BASH_SOURCE[0]}")" +BIN_DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)" +FLUTTER_ROOT="$(cd "${BIN_DIR}/../../.." ; pwd -P)" -while [[ "$#" -gt 0 ]]; do - case $1 in - -s | --strategy) - STRATEGY="$2" - shift # past argument - shift # past value - ;; - -h | --help) - print_help - ;; - -* | --*) - echo >&2 -e "Unknown option $1\n" - print_help 1 - ;; - esac -done - -BASE=HEAD -case $STRATEGY in -head) ;; -mergeBase) - BASE=$(git merge-base upstream/master HEAD) - ;; -*) - echo >&2 -e "Unknown strategy $1\n" - print_help 1 - ;; -esac - -LSTREE=$(git ls-tree -r $BASE engine DEPS) -if [ ${#LSTREE} -eq 0 ]; then - echo >&2 Error calculating engine hash: Not in a monorepo - exit 1 +# Allow using a mock git for testing. +if [ -z "$GIT" ]; then + # By default, use git on PATH. + GIT_BIN="git" else - HASH=$(echo "$LSTREE" | sha1sum | head -c 40) - echo $HASH + # Use the provide GIT executable. + GIT_BIN="$GIT" fi + +# Test for fusion repository +if [ -f "$FLUTTER_ROOT/DEPS" ]; then + ENGINE_VERSION=$($GIT_BIN -C "$FLUTTER_ROOT" merge-base HEAD origin/master) +elif [ -f "$FLUTTER_ROOT/bin/internal/engine.version" ]; then + ENGINE_VERSION=$(cat "$FLUTTER_ROOT/bin/internal/engine.version") +else + >&2 echo "Not a valid FLUTTER_ROOT: $FLUTTER_ROOT" + exit 1 +fi + +echo $ENGINE_VERSION diff --git a/dev/tools/test/engine_hash_test.dart b/dev/tools/test/engine_hash_test.dart index 623842bc83..aa48f4d6c8 100644 --- a/dev/tools/test/engine_hash_test.dart +++ b/dev/tools/test/engine_hash_test.dart @@ -2,98 +2,71 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +@TestOn('posix') +library; + import 'dart:io' as io; -import 'package:collection/collection.dart'; +import 'package:path/path.dart' as p; import 'package:test/test.dart'; -import '../bin/engine_hash.dart' show GitRevisionStrategy, engineHash; - +/// Tests that `/dev/tools/bin/engine_hash.sh` _appears_ to work. void main() { - test('Produces an engine hash for merge-base', () async { - final Future Function(List) runProcess = _fakeProcesses( - processes: [ - ( - exe: 'git', - command: 'merge-base', - rest: ['upstream/master', 'HEAD'], - exitCode: 0, - stdout: 'abcdef1234', - stderr: null, - ), - ( - exe: 'git', - command: 'ls-tree', - rest: ['-r', 'abcdef1234', 'engine', 'DEPS'], - exitCode: 0, - stdout: 'one\r\ntwo\r\n', - stderr: null, - ), - ], - ); + late final io.File engineHashSh; - final Future result = engineHash(runProcess); - - expect(result, completion('c708d7ef841f7e1748436b8ef5670d0b2de1a227')); + setUpAll(() { + engineHashSh = io.File(p.join(p.current, 'bin', 'engine_hash.sh')); + if (!engineHashSh.existsSync()) { + fail('No engine_hash.sh at "${p.absolute(engineHashSh.path)}".'); + } }); - test('Produces an engine hash for HEAD', () async { - final Future Function(List) runProcess = _fakeProcesses( - processes: [ - ( - exe: 'git', - command: 'ls-tree', - rest: ['-r', 'HEAD', 'engine', 'DEPS'], - exitCode: 0, - stdout: 'one\ntwo\n', - stderr: null, - ), - ], - ); + late io.Directory tmpFlutterRoot; - final Future result = engineHash( - runProcess, - revisionStrategy: GitRevisionStrategy.head, - ); + setUp(() { + tmpFlutterRoot = io.Directory.systemTemp.createTempSync('engine_hash_test.'); - expect(result, completion('c708d7ef841f7e1748436b8ef5670d0b2de1a227')); + // Create engine_hash.sh at the same component it would be in the real root. + io.Directory(p.join(tmpFlutterRoot.path, 'dev', 'tools', 'bin')).createSync(recursive: true); + engineHashSh.copySync(p.join(tmpFlutterRoot.path, 'dev', 'tools', 'bin', 'engine_hash.sh')); + + // Create FLUTTER_ROOT/DEPS. + io.File(p.join(tmpFlutterRoot.path, 'DEPS')).createSync(); }); - test('Returns error in non-monorepo', () async { - final Future Function(List) runProcess = _fakeProcesses( - processes: [ - ( - exe: 'git', - command: 'ls-tree', - rest: ['-r', 'HEAD', 'engine', 'DEPS'], - exitCode: 0, - stdout: '', - stderr: null, - ), - ], - ); + tearDown(() { + tmpFlutterRoot.deleteSync(recursive: true); + }); - final Future result = engineHash( - runProcess, - revisionStrategy: GitRevisionStrategy.head, - ); + test('omission of FLUTTER_ROOT/DEPS falls back to engine.version', () { + io.File(p.join(tmpFlutterRoot.path, 'bin', 'internal', 'engine.version')) + ..createSync(recursive: true) + ..writeAsStringSync('12345'); + io.File(p.join(tmpFlutterRoot.path, 'DEPS')).deleteSync(); - expect(result, throwsA('Not in a monorepo')); + final io.ProcessResult result = io.Process.runSync( + p.join(tmpFlutterRoot.path, 'dev', 'tools', 'bin', 'engine_hash.sh'), + [], + ); + expect(result.exitCode, 0, reason: result.stderr.toString()); + expect(result.stdout, '12345\n'); + }); + + test('uses git -C merge-base HEAD origin/master', () { + final io.ProcessResult result = io.Process.runSync( + p.join(tmpFlutterRoot.path, 'dev', 'tools', 'bin', 'engine_hash.sh'), + [], + environment: {'GIT': p.join(p.current, 'test', 'mock_git.sh')}, + ); + expect(result.exitCode, 0, reason: result.stderr.toString()); + expect( + result.stdout, + stringContainsInOrder([ + 'Mock Git: -C', + 'engine_hash_test', + // This needs to be origin/master if the google3 script is running from a fresh checkout. + 'merge-base HEAD origin/master', + ]), + ); }); } - -typedef FakeProcess = - ({String exe, String command, List rest, dynamic stdout, dynamic stderr, int exitCode}); - -Future Function(List) _fakeProcesses({ - required List processes, -}) => (List cmd) async { - for (final FakeProcess process in processes) { - if (process.exe.endsWith(cmd[0]) && - process.command.endsWith(cmd[1]) && - process.rest.equals(cmd.sublist(2))) { - return io.ProcessResult(1, process.exitCode, process.stdout, process.stderr); - } - } - return io.ProcessResult(1, -42, '', '404 command not found: $cmd'); -}; diff --git a/dev/tools/test/mock_git.sh b/dev/tools/test/mock_git.sh new file mode 100755 index 0000000000..64ad034096 --- /dev/null +++ b/dev/tools/test/mock_git.sh @@ -0,0 +1,6 @@ +#!/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. + +echo "Mock Git: $@"