flutter/dev/tools/bin/engine_hash.dart
John McDole c60e69040d
Add dart command line tool for calculating engine hash (#157212)
Useful in tooling, releases, etc - where Dart is available

Example usages:

```bash
# Monorepo world
❯ dart ./dev/tools/bin/engine_hash.dart -s head
226e13826c7253c968d798666f323b1f207979f8

# Non-monorepo world
❯ dart ./dev/tools/bin/engine_hash.dart -s head
Error calculating engine hash: Not in a monorepo
```
2024-10-21 16:26:28 -07:00

153 lines
4.1 KiB
Dart

// 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: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: <String>['head', 'mergeBase'],
defaultsTo: 'head',
allowedHelp: <String, String>{
'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<int> main(List<String> 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<String> 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<String> engineHash(
Future<ProcessResult> Function(List<String> 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(
<String>[
'git',
'merge-base',
'upstream/main',
'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(
<String>['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<String> 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) | <only newlines> | sha1sum
final StreamController<Digest> output = StreamController<Digest>();
final ByteConversionSink sink = sha1.startChunkedConversion(output);
for (final String line in treeLines) {
sink.add(utf8.encode(line));
sink.add(<int>[0x0a]);
}
sink.close();
final Digest digest = await output.stream.first;
return '$digest';
}