From 4a7f280687bffb6348caab22532d75ad0261369c Mon Sep 17 00:00:00 2001 From: Christopher Fujino Date: Mon, 19 Apr 2021 15:04:04 -0700 Subject: [PATCH] [flutter_conductor] Add "start", "status", "clean" commands to conductor release tool (#80528) --- analysis_options.yaml | 2 + dev/tools/bin/conductor.dart | 20 +- dev/tools/lib/clean.dart | 75 +++ dev/tools/lib/codesign.dart | 78 ++- dev/tools/lib/git.dart | 12 +- dev/tools/lib/globals.dart | 78 ++- dev/tools/lib/proto/README.md | 8 + dev/tools/lib/proto/compile_proto.sh | 45 ++ dev/tools/lib/proto/conductor_state.pb.dart | 538 ++++++++++++++++++ .../lib/proto/conductor_state.pbenum.dart | 69 +++ .../lib/proto/conductor_state.pbjson.dart | 107 ++++ .../lib/proto/conductor_state.pbserver.dart | 12 + dev/tools/lib/proto/conductor_state.proto | 103 ++++ dev/tools/lib/proto/license_header.txt | 3 + dev/tools/lib/repository.dart | 264 +++++++-- dev/tools/lib/roll_dev.dart | 23 +- dev/tools/lib/start.dart | 348 +++++++++++ dev/tools/lib/state.dart | 167 ++++++ dev/tools/lib/status.dart | 68 +++ dev/tools/lib/stdio.dart | 26 +- dev/tools/pubspec.yaml | 4 +- dev/tools/test/clean_test.dart | 97 ++++ dev/tools/test/codesign_integration_test.dart | 5 +- dev/tools/test/codesign_test.dart | 8 + dev/tools/test/common.dart | 33 +- dev/tools/test/roll_dev_integration_test.dart | 13 +- dev/tools/test/roll_dev_test.dart | 22 +- dev/tools/test/start_test.dart | 255 +++++++++ 28 files changed, 2362 insertions(+), 121 deletions(-) create mode 100644 dev/tools/lib/clean.dart create mode 100644 dev/tools/lib/proto/README.md create mode 100755 dev/tools/lib/proto/compile_proto.sh create mode 100644 dev/tools/lib/proto/conductor_state.pb.dart create mode 100644 dev/tools/lib/proto/conductor_state.pbenum.dart create mode 100644 dev/tools/lib/proto/conductor_state.pbjson.dart create mode 100644 dev/tools/lib/proto/conductor_state.pbserver.dart create mode 100644 dev/tools/lib/proto/conductor_state.proto create mode 100644 dev/tools/lib/proto/license_header.txt create mode 100644 dev/tools/lib/start.dart create mode 100644 dev/tools/lib/state.dart create mode 100644 dev/tools/lib/status.dart create mode 100644 dev/tools/test/clean_test.dart create mode 100644 dev/tools/test/start_test.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 5b0c143fed..98a9111986 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -40,6 +40,8 @@ analyzer: unnecessary_null_comparison: ignore exclude: - "bin/cache/**" + # Ignore protoc generated files + - "dev/tools/lib/proto/*" linter: rules: diff --git a/dev/tools/bin/conductor.dart b/dev/tools/bin/conductor.dart index 03addabc34..40b30ac7a7 100644 --- a/dev/tools/bin/conductor.dart +++ b/dev/tools/bin/conductor.dart @@ -2,18 +2,18 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Rolls the dev channel. -// Only tested on Linux. -// // See: https://github.com/flutter/flutter/wiki/Release-process import 'dart:io' as io; import 'package:args/command_runner.dart'; +import 'package:dev_tools/clean.dart'; import 'package:dev_tools/codesign.dart'; import 'package:dev_tools/globals.dart'; import 'package:dev_tools/roll_dev.dart'; import 'package:dev_tools/repository.dart'; +import 'package:dev_tools/start.dart'; +import 'package:dev_tools/status.dart'; import 'package:dev_tools/stdio.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; @@ -54,6 +54,16 @@ Future main(List args) async { checkouts: checkouts, flutterRoot: localFlutterRoot, ), + StatusCommand( + checkouts: checkouts, + ), + StartCommand( + checkouts: checkouts, + flutterRoot: localFlutterRoot, + ), + CleanCommand( + checkouts: checkouts, + ), ].forEach(runner.addCommand); if (!assertsEnabled()) { @@ -63,8 +73,8 @@ Future main(List args) async { try { await runner.run(args); - } on Exception catch (e) { - stdio.printError(e.toString()); + } on Exception catch (e, stacktrace) { + stdio.printError('$e\n\n$stacktrace'); io.exit(1); } } diff --git a/dev/tools/lib/clean.dart b/dev/tools/lib/clean.dart new file mode 100644 index 0000000000..59e968e5f7 --- /dev/null +++ b/dev/tools/lib/clean.dart @@ -0,0 +1,75 @@ +// 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:meta/meta.dart'; +import 'package:platform/platform.dart'; + +import './globals.dart'; +import './repository.dart'; +import './state.dart'; +import './stdio.dart'; + +const String kYesFlag = 'yes'; +const String kStateOption = 'state-file'; + +/// Command to clean up persistent state file. +/// +/// If the release was not completed, this command will abort the release. +class CleanCommand extends Command { + CleanCommand({ + @required this.checkouts, + }) : platform = checkouts.platform, + fileSystem = checkouts.fileSystem, + stdio = checkouts.stdio { + final String defaultPath = defaultStateFilePath(platform); + argParser.addFlag( + kYesFlag, + help: 'Override confirmation checks.', + ); + argParser.addOption( + kStateOption, + defaultsTo: defaultPath, + help: 'Path to persistent state file. Defaults to $defaultPath', + ); + } + + final Checkouts checkouts; + final FileSystem fileSystem; + final Platform platform; + final Stdio stdio; + + @override + String get name => 'clean'; + + @override + String get description => 'Cleanup persistent state file. ' + 'This will abort a work in progress release.'; + + @override + void run() { + final File stateFile = checkouts.fileSystem.file(argResults[kStateOption]); + if (!stateFile.existsSync()) { + throw ConductorException( + 'No persistent state file found at ${stateFile.path}!'); + } + + if (!(argResults[kYesFlag] as bool)) { + stdio.printStatus( + 'Are you sure you want to clean up the persistent state file at\n' + '${stateFile.path} (y/n)?', + ); + final String response = stdio.readLineSync(); + + // Only proceed if the first character of stdin is 'y' or 'Y' + if (response.isEmpty || response[0].toLowerCase() != 'y') { + stdio.printStatus('Aborting clean operation.'); + return; + } + } + stdio.printStatus('Deleting persistent state file ${stateFile.path}...'); + stateFile.deleteSync(); + } +} diff --git a/dev/tools/lib/codesign.dart b/dev/tools/lib/codesign.dart index 1f135b9ca1..27f9014e2f 100644 --- a/dev/tools/lib/codesign.dart +++ b/dev/tools/lib/codesign.dart @@ -33,11 +33,15 @@ class CodesignCommand extends Command { CodesignCommand({ @required this.checkouts, @required this.flutterRoot, + FrameworkRepository framework, }) : assert(flutterRoot != null), fileSystem = checkouts.fileSystem, platform = checkouts.platform, stdio = checkouts.stdio, processManager = checkouts.processManager { + if (framework != null) { + _framework = framework; + } argParser.addFlag( kVerify, help: @@ -71,13 +75,12 @@ class CodesignCommand extends Command { final Directory flutterRoot; FrameworkRepository _framework; - FrameworkRepository get framework => _framework ??= FrameworkRepository.localRepoAsUpstream( - checkouts, - upstreamPath: flutterRoot.path, - ); - - @visibleForTesting - set framework(FrameworkRepository framework) => _framework = framework; + FrameworkRepository get framework { + return _framework ??= FrameworkRepository.localRepoAsUpstream( + checkouts, + upstreamPath: flutterRoot.path, + ); + } @override String get name => 'codesign'; @@ -102,15 +105,19 @@ class CodesignCommand extends Command { String revision; if (argResults.wasParsed(kRevision)) { - stdio.printError('Warning! When providing an arbitrary revision, the contents of the cache may not'); - stdio.printError('match the expected binaries in the conductor tool. It is preferred to check out'); - stdio.printError('the desired revision and run that version of the conductor.\n'); + stdio.printError( + 'Warning! When providing an arbitrary revision, the contents of the cache may not'); + stdio.printError( + 'match the expected binaries in the conductor tool. It is preferred to check out'); + stdio.printError( + 'the desired revision and run that version of the conductor.\n'); revision = argResults[kRevision] as String; } else { revision = (processManager.runSync( ['git', 'rev-parse', 'HEAD'], workingDirectory: framework.checkoutDirectory.path, - ).stdout as String).trim(); + ).stdout as String) + .trim(); assert(revision.isNotEmpty); } @@ -158,7 +165,10 @@ class CodesignCommand extends Command { 'dart-sdk/bin/dart', 'dart-sdk/bin/dartaotruntime', 'dart-sdk/bin/utils/gen_snapshot', - ].map((String relativePath) => fileSystem.path.join(framework.cacheDirectory, relativePath)).toList(); + ] + .map((String relativePath) => + fileSystem.path.join(framework.cacheDirectory, relativePath)) + .toList(); } /// Binaries that are only expected to be codesigned. @@ -178,7 +188,10 @@ class CodesignCommand extends Command { 'artifacts/engine/ios/Flutter.xcframework/ios-armv7_arm64/Flutter.framework/Flutter', 'artifacts/engine/ios/Flutter.xcframework/ios-x86_64-simulator/Flutter.framework/Flutter', 'artifacts/ios-deploy/ios-deploy', - ].map((String relativePath) => fileSystem.path.join(framework.cacheDirectory, relativePath)).toList(); + ] + .map((String relativePath) => + fileSystem.path.join(framework.cacheDirectory, relativePath)) + .toList(); } /// Verify the existence of all expected binaries in cache. @@ -197,19 +210,27 @@ class CodesignCommand extends Command { } else if (binariesWithoutEntitlements.contains(binaryPath)) { foundFiles.add(binaryPath); } else { - throw ConductorException('Found unexpected binary in cache: $binaryPath'); + throw ConductorException( + 'Found unexpected binary in cache: $binaryPath'); } } - final List allExpectedFiles = binariesWithEntitlements + binariesWithoutEntitlements; + final List allExpectedFiles = + binariesWithEntitlements + binariesWithoutEntitlements; if (foundFiles.length < allExpectedFiles.length) { - final List unfoundFiles = allExpectedFiles.where( - (String file) => !foundFiles.contains(file), - ).toList(); - stdio.printError('Expected binaries not found in cache:\n\n${unfoundFiles.join('\n')}\n'); - stdio.printError('If this commit is removing binaries from the cache, this test should be fixed by'); - stdio.printError('removing the relevant entry from either the `binariesWithEntitlements` or'); - stdio.printError('`binariesWithoutEntitlements` getters in dev/tools/lib/codesign.dart.'); + final List unfoundFiles = allExpectedFiles + .where( + (String file) => !foundFiles.contains(file), + ) + .toList(); + stdio.printError( + 'Expected binaries not found in cache:\n\n${unfoundFiles.join('\n')}\n'); + stdio.printError( + 'If this commit is removing binaries from the cache, this test should be fixed by'); + stdio.printError( + 'removing the relevant entry from either the `binariesWithEntitlements` or'); + stdio.printError( + '`binariesWithoutEntitlements` getters in dev/tools/lib/codesign.dart.'); throw ConductorException('Did not find all expected binaries!'); } @@ -275,13 +296,15 @@ class CodesignCommand extends Command { } if (unexpectedBinaries.isNotEmpty) { - stdio.printError('Found ${unexpectedBinaries.length} unexpected binaries in the cache:'); + stdio.printError( + 'Found ${unexpectedBinaries.length} unexpected binaries in the cache:'); unexpectedBinaries.forEach(print); } // Finally, exit on any invalid state if (unsignedBinaries.isNotEmpty) { - throw ConductorException('Test failed because unsigned binaries detected.'); + throw ConductorException( + 'Test failed because unsigned binaries detected.'); } if (wrongEntitlementBinaries.isNotEmpty) { @@ -291,7 +314,8 @@ class CodesignCommand extends Command { } if (unexpectedBinaries.isNotEmpty) { - throw ConductorException('Test failed because unexpected binaries found in the cache.'); + throw ConductorException( + 'Test failed because unexpected binaries found in the cache.'); } stdio.printStatus( @@ -300,6 +324,7 @@ class CodesignCommand extends Command { } List _allBinaryPaths; + /// Find every binary file in the given [rootDirectory]. List findBinaryPaths(String rootDirectory) { if (_allBinaryPaths != null) { @@ -356,7 +381,8 @@ class CodesignCommand extends Command { bool passes = true; final String output = entitlementResult.stdout as String; for (final String entitlement in expectedEntitlements) { - final bool entitlementExpected = binariesWithEntitlements.contains(binaryPath); + final bool entitlementExpected = + binariesWithEntitlements.contains(binaryPath); if (output.contains(entitlement) != entitlementExpected) { stdio.printError( 'File "$binaryPath" ${entitlementExpected ? 'does not have expected' : 'has unexpected'} ' diff --git a/dev/tools/lib/git.dart b/dev/tools/lib/git.dart index a85a2db0ff..caed631b92 100644 --- a/dev/tools/lib/git.dart +++ b/dev/tools/lib/git.dart @@ -19,6 +19,7 @@ class Git { List args, String explanation, { @required String workingDirectory, + bool allowFailures = false, }) { final ProcessResult result = _run(args, workingDirectory); if (result.exitCode == 0) { @@ -68,6 +69,15 @@ class Git { message.writeln('stdout from git:\n${result.stdout}\n'); if ((result.stderr as String).isNotEmpty) message.writeln('stderr from git:\n${result.stderr}\n'); - throw Exception(message); + throw GitException(message.toString()); } } + +class GitException implements Exception { + GitException(this.message); + + final String message; + + @override + String toString() => 'Exception: $message'; +} diff --git a/dev/tools/lib/globals.dart b/dev/tools/lib/globals.dart index e6abe0e02b..2234b79ca2 100644 --- a/dev/tools/lib/globals.dart +++ b/dev/tools/lib/globals.dart @@ -2,20 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:args/args.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import 'package:platform/platform.dart'; -const String kIncrement = 'increment'; -const String kCommit = 'commit'; -const String kRemoteName = 'remote'; -const String kJustPrint = 'just-print'; -const String kYes = 'yes'; -const String kForce = 'force'; -const String kSkipTagging = 'skip-tagging'; - const String kUpstreamRemote = 'https://github.com/flutter/flutter.git'; +const String gsutilBinary = 'gsutil.py'; + const List kReleaseChannels = [ 'stable', 'beta', @@ -23,6 +18,12 @@ const List kReleaseChannels = [ 'master', ]; +const String kReleaseDocumentationUrl = 'https://github.com/flutter/flutter/wiki/Flutter-Cherrypick-Process'; + +final RegExp releaseCandidateBranchRegex = RegExp( + r'flutter-(\d+)\.(\d+)-candidate\.(\d+)', +); + /// Cast a dynamic to String and trim. String stdoutToString(dynamic input) { final String str = input as String; @@ -86,3 +87,64 @@ bool assertsEnabled() { }()); return assertsEnabled; } + +/// Either return the value from [env] or fall back to [argResults]. +/// +/// If the key does not exist in either the environment or CLI args, throws a +/// [ConductorException]. +/// +/// The environment is favored over CLI args since the latter can have a default +/// value, which the environment should be able to override. +String getValueFromEnvOrArgs( + String name, + ArgResults argResults, + Map env, +) { + final String envName = fromArgToEnvName(name); + if (env[envName] != null ) { + return env[envName]; + } + final String argValue = argResults[name] as String; + if (argValue != null) { + return argValue; + } + + throw ConductorException( + 'Expected either the CLI arg --$name or the environment variable $envName ' + 'to be provided!'); +} + +/// Return multiple values from the environment or fall back to [argResults]. +/// +/// Values read from an environment variable are assumed to be comma-delimited. +/// +/// If the key does not exist in either the CLI args or environment, throws a +/// [ConductorException]. +/// +/// The environment is favored over CLI args since the latter can have a default +/// value, which the environment should be able to override. +List getValuesFromEnvOrArgs( + String name, + ArgResults argResults, + Map env, +) { + final String envName = fromArgToEnvName(name); + if (env[envName] != null && env[envName] != '') { + return env[envName].split(','); + } + final List argValues = argResults[name] as List; + if (argValues != null) { + return argValues; + } + + throw ConductorException( + 'Expected either the CLI arg --$name or the environment variable $envName ' + 'to be provided!'); +} + +/// Translate CLI arg names to env variable names. +/// +/// For example, 'state-file' -> 'STATE_FILE'. +String fromArgToEnvName(String argName) { + return argName.toUpperCase().replaceAll(r'-', r'_'); +} diff --git a/dev/tools/lib/proto/README.md b/dev/tools/lib/proto/README.md new file mode 100644 index 0000000000..b57f8e4a0c --- /dev/null +++ b/dev/tools/lib/proto/README.md @@ -0,0 +1,8 @@ +## Flutter Conductor Protocol Buffers + +This directory contains [conductor_state.proto](./conductor_state.proto), which +defines the persistent state file the conductor creates. After changes to this +file, you must run the [compile_proto.sh](./compile_proto.sh) script in this +directory, which will re-generate the rest of the Dart files in this directory, +format them, and prepend the license comment from +[license_header.txt](./license_header.txt). diff --git a/dev/tools/lib/proto/compile_proto.sh b/dev/tools/lib/proto/compile_proto.sh new file mode 100755 index 0000000000..ff81e0fa10 --- /dev/null +++ b/dev/tools/lib/proto/compile_proto.sh @@ -0,0 +1,45 @@ +#!/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. + +# //flutter/dev/tools/lib/proto +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +DARTFMT="$DIR/../../../../bin/cache/dart-sdk/bin/dartfmt" + +# Ensure dart-sdk is cached +"$DIR/../../../../bin/dart" --version + +if ! type protoc >/dev/null 2>&1; then + PROTOC_LINK='https://grpc.io/docs/protoc-installation/' + echo "Error! \"protoc\" binary required on path." + echo "See $PROTOC_LINK for more information." + exit 1 +fi + +if ! type dart >/dev/null 2>&1; then + echo "Error! \"dart\" binary required on path." + exit 1 +fi + +# Pin protoc-gen-dart to pre-nullsafe version. +dart pub global activate protoc_plugin 19.3.1 + +protoc --dart_out="$DIR" --proto_path="$DIR" "$DIR/conductor_state.proto" + +for SOURCE_FILE in $(ls "$DIR"/*.pb*.dart); do + # Format in place file + "$DARTFMT" --overwrite --line-length 120 "$SOURCE_FILE" + + # Create temp copy with the license header prepended + cp license_header.txt "${SOURCE_FILE}.tmp" + + # Add an extra newline required by analysis (analysis also prevents + # license_header.txt from having the trailing newline) + echo '' >> "${SOURCE_FILE}.tmp" + + cat "$SOURCE_FILE" >> "${SOURCE_FILE}.tmp" + + # Move temp version (with license) over the original + mv "${SOURCE_FILE}.tmp" "$SOURCE_FILE" +done diff --git a/dev/tools/lib/proto/conductor_state.pb.dart b/dev/tools/lib/proto/conductor_state.pb.dart new file mode 100644 index 0000000000..58aee2cb59 --- /dev/null +++ b/dev/tools/lib/proto/conductor_state.pb.dart @@ -0,0 +1,538 @@ +// 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. + +/// +// Generated code. Do not modify. +// source: conductor_state.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'conductor_state.pbenum.dart'; + +export 'conductor_state.pbenum.dart'; + +class Remote extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'Remote', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'conductor_state'), + createEmptyInstance: create) + ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name') + ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'url') + ..hasRequiredFields = false; + + Remote._() : super(); + factory Remote({ + $core.String name, + $core.String url, + }) { + final _result = create(); + if (name != null) { + _result.name = name; + } + if (url != null) { + _result.url = url; + } + return _result; + } + factory Remote.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Remote.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Remote clone() => Remote()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Remote copyWith(void Function(Remote) updates) => + super.copyWith((message) => updates(message as Remote)) as Remote; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Remote create() => Remote._(); + Remote createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Remote getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Remote _defaultInstance; + + @$pb.TagNumber(1) + $core.String get name => $_getSZ(0); + @$pb.TagNumber(1) + set name($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasName() => $_has(0); + @$pb.TagNumber(1) + void clearName() => clearField(1); + + @$pb.TagNumber(2) + $core.String get url => $_getSZ(1); + @$pb.TagNumber(2) + set url($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasUrl() => $_has(1); + @$pb.TagNumber(2) + void clearUrl() => clearField(2); +} + +class Cherrypick extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'Cherrypick', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'conductor_state'), + createEmptyInstance: create) + ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trunkRevision', + protoName: 'trunkRevision') + ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'appliedRevision', + protoName: 'appliedRevision') + ..e( + 3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'state', $pb.PbFieldType.OE, + defaultOrMaker: CherrypickState.PENDING, valueOf: CherrypickState.valueOf, enumValues: CherrypickState.values) + ..hasRequiredFields = false; + + Cherrypick._() : super(); + factory Cherrypick({ + $core.String trunkRevision, + $core.String appliedRevision, + CherrypickState state, + }) { + final _result = create(); + if (trunkRevision != null) { + _result.trunkRevision = trunkRevision; + } + if (appliedRevision != null) { + _result.appliedRevision = appliedRevision; + } + if (state != null) { + _result.state = state; + } + return _result; + } + factory Cherrypick.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Cherrypick.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Cherrypick clone() => Cherrypick()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Cherrypick copyWith(void Function(Cherrypick) updates) => + super.copyWith((message) => updates(message as Cherrypick)) as Cherrypick; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Cherrypick create() => Cherrypick._(); + Cherrypick createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Cherrypick getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Cherrypick _defaultInstance; + + @$pb.TagNumber(1) + $core.String get trunkRevision => $_getSZ(0); + @$pb.TagNumber(1) + set trunkRevision($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasTrunkRevision() => $_has(0); + @$pb.TagNumber(1) + void clearTrunkRevision() => clearField(1); + + @$pb.TagNumber(2) + $core.String get appliedRevision => $_getSZ(1); + @$pb.TagNumber(2) + set appliedRevision($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasAppliedRevision() => $_has(1); + @$pb.TagNumber(2) + void clearAppliedRevision() => clearField(2); + + @$pb.TagNumber(3) + CherrypickState get state => $_getN(2); + @$pb.TagNumber(3) + set state(CherrypickState v) { + setField(3, v); + } + + @$pb.TagNumber(3) + $core.bool hasState() => $_has(2); + @$pb.TagNumber(3) + void clearState() => clearField(3); +} + +class Repository extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'Repository', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'conductor_state'), + createEmptyInstance: create) + ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'candidateBranch', + protoName: 'candidateBranch') + ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'startingGitHead', + protoName: 'startingGitHead') + ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'currentGitHead', + protoName: 'currentGitHead') + ..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'checkoutPath', + protoName: 'checkoutPath') + ..aOM(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'upstream', + subBuilder: Remote.create) + ..aOM(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'mirror', + subBuilder: Remote.create) + ..pc( + 7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'cherrypicks', $pb.PbFieldType.PM, + subBuilder: Cherrypick.create) + ..hasRequiredFields = false; + + Repository._() : super(); + factory Repository({ + $core.String candidateBranch, + $core.String startingGitHead, + $core.String currentGitHead, + $core.String checkoutPath, + Remote upstream, + Remote mirror, + $core.Iterable cherrypicks, + }) { + final _result = create(); + if (candidateBranch != null) { + _result.candidateBranch = candidateBranch; + } + if (startingGitHead != null) { + _result.startingGitHead = startingGitHead; + } + if (currentGitHead != null) { + _result.currentGitHead = currentGitHead; + } + if (checkoutPath != null) { + _result.checkoutPath = checkoutPath; + } + if (upstream != null) { + _result.upstream = upstream; + } + if (mirror != null) { + _result.mirror = mirror; + } + if (cherrypicks != null) { + _result.cherrypicks.addAll(cherrypicks); + } + return _result; + } + factory Repository.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Repository.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Repository clone() => Repository()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Repository copyWith(void Function(Repository) updates) => + super.copyWith((message) => updates(message as Repository)) as Repository; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Repository create() => Repository._(); + Repository createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Repository getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Repository _defaultInstance; + + @$pb.TagNumber(1) + $core.String get candidateBranch => $_getSZ(0); + @$pb.TagNumber(1) + set candidateBranch($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasCandidateBranch() => $_has(0); + @$pb.TagNumber(1) + void clearCandidateBranch() => clearField(1); + + @$pb.TagNumber(2) + $core.String get startingGitHead => $_getSZ(1); + @$pb.TagNumber(2) + set startingGitHead($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasStartingGitHead() => $_has(1); + @$pb.TagNumber(2) + void clearStartingGitHead() => clearField(2); + + @$pb.TagNumber(3) + $core.String get currentGitHead => $_getSZ(2); + @$pb.TagNumber(3) + set currentGitHead($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasCurrentGitHead() => $_has(2); + @$pb.TagNumber(3) + void clearCurrentGitHead() => clearField(3); + + @$pb.TagNumber(4) + $core.String get checkoutPath => $_getSZ(3); + @$pb.TagNumber(4) + set checkoutPath($core.String v) { + $_setString(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasCheckoutPath() => $_has(3); + @$pb.TagNumber(4) + void clearCheckoutPath() => clearField(4); + + @$pb.TagNumber(5) + Remote get upstream => $_getN(4); + @$pb.TagNumber(5) + set upstream(Remote v) { + setField(5, v); + } + + @$pb.TagNumber(5) + $core.bool hasUpstream() => $_has(4); + @$pb.TagNumber(5) + void clearUpstream() => clearField(5); + @$pb.TagNumber(5) + Remote ensureUpstream() => $_ensure(4); + + @$pb.TagNumber(6) + Remote get mirror => $_getN(5); + @$pb.TagNumber(6) + set mirror(Remote v) { + setField(6, v); + } + + @$pb.TagNumber(6) + $core.bool hasMirror() => $_has(5); + @$pb.TagNumber(6) + void clearMirror() => clearField(6); + @$pb.TagNumber(6) + Remote ensureMirror() => $_ensure(5); + + @$pb.TagNumber(7) + $core.List get cherrypicks => $_getList(6); +} + +class ConductorState extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'ConductorState', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'conductor_state'), + createEmptyInstance: create) + ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'releaseChannel', + protoName: 'releaseChannel') + ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'releaseVersion', + protoName: 'releaseVersion') + ..aOM(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'engine', + subBuilder: Repository.create) + ..aOM(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'framework', + subBuilder: Repository.create) + ..aInt64(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'createdDate', + protoName: 'createdDate') + ..aInt64(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'lastUpdatedDate', + protoName: 'lastUpdatedDate') + ..pPS(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'logs') + ..e( + 9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'lastPhase', $pb.PbFieldType.OE, + protoName: 'lastPhase', + defaultOrMaker: ReleasePhase.INITIALIZE, + valueOf: ReleasePhase.valueOf, + enumValues: ReleasePhase.values) + ..aOS(10, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'conductorVersion') + ..hasRequiredFields = false; + + ConductorState._() : super(); + factory ConductorState({ + $core.String releaseChannel, + $core.String releaseVersion, + Repository engine, + Repository framework, + $fixnum.Int64 createdDate, + $fixnum.Int64 lastUpdatedDate, + $core.Iterable<$core.String> logs, + ReleasePhase lastPhase, + $core.String conductorVersion, + }) { + final _result = create(); + if (releaseChannel != null) { + _result.releaseChannel = releaseChannel; + } + if (releaseVersion != null) { + _result.releaseVersion = releaseVersion; + } + if (engine != null) { + _result.engine = engine; + } + if (framework != null) { + _result.framework = framework; + } + if (createdDate != null) { + _result.createdDate = createdDate; + } + if (lastUpdatedDate != null) { + _result.lastUpdatedDate = lastUpdatedDate; + } + if (logs != null) { + _result.logs.addAll(logs); + } + if (lastPhase != null) { + _result.lastPhase = lastPhase; + } + if (conductorVersion != null) { + _result.conductorVersion = conductorVersion; + } + return _result; + } + factory ConductorState.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ConductorState.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ConductorState clone() => ConductorState()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ConductorState copyWith(void Function(ConductorState) updates) => + super.copyWith((message) => updates(message as ConductorState)) + as ConductorState; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ConductorState create() => ConductorState._(); + ConductorState createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ConductorState getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ConductorState _defaultInstance; + + @$pb.TagNumber(1) + $core.String get releaseChannel => $_getSZ(0); + @$pb.TagNumber(1) + set releaseChannel($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasReleaseChannel() => $_has(0); + @$pb.TagNumber(1) + void clearReleaseChannel() => clearField(1); + + @$pb.TagNumber(2) + $core.String get releaseVersion => $_getSZ(1); + @$pb.TagNumber(2) + set releaseVersion($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasReleaseVersion() => $_has(1); + @$pb.TagNumber(2) + void clearReleaseVersion() => clearField(2); + + @$pb.TagNumber(4) + Repository get engine => $_getN(2); + @$pb.TagNumber(4) + set engine(Repository v) { + setField(4, v); + } + + @$pb.TagNumber(4) + $core.bool hasEngine() => $_has(2); + @$pb.TagNumber(4) + void clearEngine() => clearField(4); + @$pb.TagNumber(4) + Repository ensureEngine() => $_ensure(2); + + @$pb.TagNumber(5) + Repository get framework => $_getN(3); + @$pb.TagNumber(5) + set framework(Repository v) { + setField(5, v); + } + + @$pb.TagNumber(5) + $core.bool hasFramework() => $_has(3); + @$pb.TagNumber(5) + void clearFramework() => clearField(5); + @$pb.TagNumber(5) + Repository ensureFramework() => $_ensure(3); + + @$pb.TagNumber(6) + $fixnum.Int64 get createdDate => $_getI64(4); + @$pb.TagNumber(6) + set createdDate($fixnum.Int64 v) { + $_setInt64(4, v); + } + + @$pb.TagNumber(6) + $core.bool hasCreatedDate() => $_has(4); + @$pb.TagNumber(6) + void clearCreatedDate() => clearField(6); + + @$pb.TagNumber(7) + $fixnum.Int64 get lastUpdatedDate => $_getI64(5); + @$pb.TagNumber(7) + set lastUpdatedDate($fixnum.Int64 v) { + $_setInt64(5, v); + } + + @$pb.TagNumber(7) + $core.bool hasLastUpdatedDate() => $_has(5); + @$pb.TagNumber(7) + void clearLastUpdatedDate() => clearField(7); + + @$pb.TagNumber(8) + $core.List<$core.String> get logs => $_getList(6); + + @$pb.TagNumber(9) + ReleasePhase get lastPhase => $_getN(7); + @$pb.TagNumber(9) + set lastPhase(ReleasePhase v) { + setField(9, v); + } + + @$pb.TagNumber(9) + $core.bool hasLastPhase() => $_has(7); + @$pb.TagNumber(9) + void clearLastPhase() => clearField(9); + + @$pb.TagNumber(10) + $core.String get conductorVersion => $_getSZ(8); + @$pb.TagNumber(10) + set conductorVersion($core.String v) { + $_setString(8, v); + } + + @$pb.TagNumber(10) + $core.bool hasConductorVersion() => $_has(8); + @$pb.TagNumber(10) + void clearConductorVersion() => clearField(10); +} diff --git a/dev/tools/lib/proto/conductor_state.pbenum.dart b/dev/tools/lib/proto/conductor_state.pbenum.dart new file mode 100644 index 0000000000..57a84639f6 --- /dev/null +++ b/dev/tools/lib/proto/conductor_state.pbenum.dart @@ -0,0 +1,69 @@ +// 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. + +/// +// Generated code. Do not modify. +// source: conductor_state.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +// ignore_for_file: UNDEFINED_SHOWN_NAME +import 'dart:core' as $core; +import 'package:protobuf/protobuf.dart' as $pb; + +class ReleasePhase extends $pb.ProtobufEnum { + static const ReleasePhase INITIALIZE = + ReleasePhase._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'INITIALIZE'); + static const ReleasePhase APPLY_ENGINE_CHERRYPICKS = + ReleasePhase._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'APPLY_ENGINE_CHERRYPICKS'); + static const ReleasePhase CODESIGN_ENGINE_BINARIES = + ReleasePhase._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CODESIGN_ENGINE_BINARIES'); + static const ReleasePhase APPLY_FRAMEWORK_CHERRYPICKS = ReleasePhase._( + 3, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'APPLY_FRAMEWORK_CHERRYPICKS'); + static const ReleasePhase PUBLISH_VERSION = + ReleasePhase._(4, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PUBLISH_VERSION'); + static const ReleasePhase PUBLISH_CHANNEL = + ReleasePhase._(5, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PUBLISH_CHANNEL'); + static const ReleasePhase VERIFY_RELEASE = + ReleasePhase._(6, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'VERIFY_RELEASE'); + + static const $core.List values = [ + INITIALIZE, + APPLY_ENGINE_CHERRYPICKS, + CODESIGN_ENGINE_BINARIES, + APPLY_FRAMEWORK_CHERRYPICKS, + PUBLISH_VERSION, + PUBLISH_CHANNEL, + VERIFY_RELEASE, + ]; + + static final $core.Map<$core.int, ReleasePhase> _byValue = $pb.ProtobufEnum.initByValue(values); + static ReleasePhase valueOf($core.int value) => _byValue[value]; + + const ReleasePhase._($core.int v, $core.String n) : super(v, n); +} + +class CherrypickState extends $pb.ProtobufEnum { + static const CherrypickState PENDING = + CherrypickState._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PENDING'); + static const CherrypickState PENDING_WITH_CONFLICT = + CherrypickState._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PENDING_WITH_CONFLICT'); + static const CherrypickState COMPLETED = + CherrypickState._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'COMPLETED'); + static const CherrypickState ABANDONED = + CherrypickState._(3, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ABANDONED'); + + static const $core.List values = [ + PENDING, + PENDING_WITH_CONFLICT, + COMPLETED, + ABANDONED, + ]; + + static final $core.Map<$core.int, CherrypickState> _byValue = $pb.ProtobufEnum.initByValue(values); + static CherrypickState valueOf($core.int value) => _byValue[value]; + + const CherrypickState._($core.int v, $core.String n) : super(v, n); +} diff --git a/dev/tools/lib/proto/conductor_state.pbjson.dart b/dev/tools/lib/proto/conductor_state.pbjson.dart new file mode 100644 index 0000000000..003b03ab90 --- /dev/null +++ b/dev/tools/lib/proto/conductor_state.pbjson.dart @@ -0,0 +1,107 @@ +// 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. + +/// +// Generated code. Do not modify. +// source: conductor_state.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package + +import 'dart:core' as $core; +import 'dart:convert' as $convert; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use releasePhaseDescriptor instead') +const ReleasePhase$json = const { + '1': 'ReleasePhase', + '2': const [ + const {'1': 'INITIALIZE', '2': 0}, + const {'1': 'APPLY_ENGINE_CHERRYPICKS', '2': 1}, + const {'1': 'CODESIGN_ENGINE_BINARIES', '2': 2}, + const {'1': 'APPLY_FRAMEWORK_CHERRYPICKS', '2': 3}, + const {'1': 'PUBLISH_VERSION', '2': 4}, + const {'1': 'PUBLISH_CHANNEL', '2': 5}, + const {'1': 'VERIFY_RELEASE', '2': 6}, + ], +}; + +/// Descriptor for `ReleasePhase`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List releasePhaseDescriptor = $convert.base64Decode( + 'CgxSZWxlYXNlUGhhc2USDgoKSU5JVElBTElaRRAAEhwKGEFQUExZX0VOR0lORV9DSEVSUllQSUNLUxABEhwKGENPREVTSUdOX0VOR0lORV9CSU5BUklFUxACEh8KG0FQUExZX0ZSQU1FV09SS19DSEVSUllQSUNLUxADEhMKD1BVQkxJU0hfVkVSU0lPThAEEhMKD1BVQkxJU0hfQ0hBTk5FTBAFEhIKDlZFUklGWV9SRUxFQVNFEAY='); +@$core.Deprecated('Use cherrypickStateDescriptor instead') +const CherrypickState$json = const { + '1': 'CherrypickState', + '2': const [ + const {'1': 'PENDING', '2': 0}, + const {'1': 'PENDING_WITH_CONFLICT', '2': 1}, + const {'1': 'COMPLETED', '2': 2}, + const {'1': 'ABANDONED', '2': 3}, + ], +}; + +/// Descriptor for `CherrypickState`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List cherrypickStateDescriptor = $convert.base64Decode( + 'Cg9DaGVycnlwaWNrU3RhdGUSCwoHUEVORElORxAAEhkKFVBFTkRJTkdfV0lUSF9DT05GTElDVBABEg0KCUNPTVBMRVRFRBACEg0KCUFCQU5ET05FRBAD'); +@$core.Deprecated('Use remoteDescriptor instead') +const Remote$json = const { + '1': 'Remote', + '2': const [ + const {'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'}, + const {'1': 'url', '3': 2, '4': 1, '5': 9, '10': 'url'}, + ], +}; + +/// Descriptor for `Remote`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List remoteDescriptor = + $convert.base64Decode('CgZSZW1vdGUSEgoEbmFtZRgBIAEoCVIEbmFtZRIQCgN1cmwYAiABKAlSA3VybA=='); +@$core.Deprecated('Use cherrypickDescriptor instead') +const Cherrypick$json = const { + '1': 'Cherrypick', + '2': const [ + const {'1': 'trunkRevision', '3': 1, '4': 1, '5': 9, '10': 'trunkRevision'}, + const {'1': 'appliedRevision', '3': 2, '4': 1, '5': 9, '10': 'appliedRevision'}, + const {'1': 'state', '3': 3, '4': 1, '5': 14, '6': '.conductor_state.CherrypickState', '10': 'state'}, + ], +}; + +/// Descriptor for `Cherrypick`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List cherrypickDescriptor = $convert.base64Decode( + 'CgpDaGVycnlwaWNrEiQKDXRydW5rUmV2aXNpb24YASABKAlSDXRydW5rUmV2aXNpb24SKAoPYXBwbGllZFJldmlzaW9uGAIgASgJUg9hcHBsaWVkUmV2aXNpb24SNgoFc3RhdGUYAyABKA4yIC5jb25kdWN0b3Jfc3RhdGUuQ2hlcnJ5cGlja1N0YXRlUgVzdGF0ZQ=='); +@$core.Deprecated('Use repositoryDescriptor instead') +const Repository$json = const { + '1': 'Repository', + '2': const [ + const {'1': 'candidateBranch', '3': 1, '4': 1, '5': 9, '10': 'candidateBranch'}, + const {'1': 'startingGitHead', '3': 2, '4': 1, '5': 9, '10': 'startingGitHead'}, + const {'1': 'currentGitHead', '3': 3, '4': 1, '5': 9, '10': 'currentGitHead'}, + const {'1': 'checkoutPath', '3': 4, '4': 1, '5': 9, '10': 'checkoutPath'}, + const {'1': 'upstream', '3': 5, '4': 1, '5': 11, '6': '.conductor_state.Remote', '10': 'upstream'}, + const {'1': 'mirror', '3': 6, '4': 1, '5': 11, '6': '.conductor_state.Remote', '10': 'mirror'}, + const {'1': 'cherrypicks', '3': 7, '4': 3, '5': 11, '6': '.conductor_state.Cherrypick', '10': 'cherrypicks'}, + ], +}; + +/// Descriptor for `Repository`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List repositoryDescriptor = $convert.base64Decode( + 'CgpSZXBvc2l0b3J5EigKD2NhbmRpZGF0ZUJyYW5jaBgBIAEoCVIPY2FuZGlkYXRlQnJhbmNoEigKD3N0YXJ0aW5nR2l0SGVhZBgCIAEoCVIPc3RhcnRpbmdHaXRIZWFkEiYKDmN1cnJlbnRHaXRIZWFkGAMgASgJUg5jdXJyZW50R2l0SGVhZBIiCgxjaGVja291dFBhdGgYBCABKAlSDGNoZWNrb3V0UGF0aBIzCgh1cHN0cmVhbRgFIAEoCzIXLmNvbmR1Y3Rvcl9zdGF0ZS5SZW1vdGVSCHVwc3RyZWFtEi8KBm1pcnJvchgGIAEoCzIXLmNvbmR1Y3Rvcl9zdGF0ZS5SZW1vdGVSBm1pcnJvchI9CgtjaGVycnlwaWNrcxgHIAMoCzIbLmNvbmR1Y3Rvcl9zdGF0ZS5DaGVycnlwaWNrUgtjaGVycnlwaWNrcw=='); +@$core.Deprecated('Use conductorStateDescriptor instead') +const ConductorState$json = const { + '1': 'ConductorState', + '2': const [ + const {'1': 'releaseChannel', '3': 1, '4': 1, '5': 9, '10': 'releaseChannel'}, + const {'1': 'releaseVersion', '3': 2, '4': 1, '5': 9, '10': 'releaseVersion'}, + const {'1': 'engine', '3': 4, '4': 1, '5': 11, '6': '.conductor_state.Repository', '10': 'engine'}, + const {'1': 'framework', '3': 5, '4': 1, '5': 11, '6': '.conductor_state.Repository', '10': 'framework'}, + const {'1': 'createdDate', '3': 6, '4': 1, '5': 3, '10': 'createdDate'}, + const {'1': 'lastUpdatedDate', '3': 7, '4': 1, '5': 3, '10': 'lastUpdatedDate'}, + const {'1': 'logs', '3': 8, '4': 3, '5': 9, '10': 'logs'}, + const {'1': 'lastPhase', '3': 9, '4': 1, '5': 14, '6': '.conductor_state.ReleasePhase', '10': 'lastPhase'}, + const {'1': 'conductor_version', '3': 10, '4': 1, '5': 9, '10': 'conductorVersion'}, + ], +}; + +/// Descriptor for `ConductorState`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List conductorStateDescriptor = $convert.base64Decode( + 'Cg5Db25kdWN0b3JTdGF0ZRImCg5yZWxlYXNlQ2hhbm5lbBgBIAEoCVIOcmVsZWFzZUNoYW5uZWwSJgoOcmVsZWFzZVZlcnNpb24YAiABKAlSDnJlbGVhc2VWZXJzaW9uEjMKBmVuZ2luZRgEIAEoCzIbLmNvbmR1Y3Rvcl9zdGF0ZS5SZXBvc2l0b3J5UgZlbmdpbmUSOQoJZnJhbWV3b3JrGAUgASgLMhsuY29uZHVjdG9yX3N0YXRlLlJlcG9zaXRvcnlSCWZyYW1ld29yaxIgCgtjcmVhdGVkRGF0ZRgGIAEoA1ILY3JlYXRlZERhdGUSKAoPbGFzdFVwZGF0ZWREYXRlGAcgASgDUg9sYXN0VXBkYXRlZERhdGUSEgoEbG9ncxgIIAMoCVIEbG9ncxI7CglsYXN0UGhhc2UYCSABKA4yHS5jb25kdWN0b3Jfc3RhdGUuUmVsZWFzZVBoYXNlUglsYXN0UGhhc2USKwoRY29uZHVjdG9yX3ZlcnNpb24YCiABKAlSEGNvbmR1Y3RvclZlcnNpb24='); diff --git a/dev/tools/lib/proto/conductor_state.pbserver.dart b/dev/tools/lib/proto/conductor_state.pbserver.dart new file mode 100644 index 0000000000..f97847ad74 --- /dev/null +++ b/dev/tools/lib/proto/conductor_state.pbserver.dart @@ -0,0 +1,12 @@ +// 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. + +/// +// Generated code. Do not modify. +// source: conductor_state.proto +// +// @dart = 2.7 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package + +export 'conductor_state.pb.dart'; diff --git a/dev/tools/lib/proto/conductor_state.proto b/dev/tools/lib/proto/conductor_state.proto new file mode 100644 index 0000000000..5733ae736b --- /dev/null +++ b/dev/tools/lib/proto/conductor_state.proto @@ -0,0 +1,103 @@ +syntax = "proto3"; + +package conductor_state; + +// A git remote +message Remote { + string name = 1; + string url = 2; +} + +enum ReleasePhase { + // Release was started with `conductor start` and repositories cloned. + INITIALIZE = 0; + APPLY_ENGINE_CHERRYPICKS = 1; + CODESIGN_ENGINE_BINARIES = 2; + APPLY_FRAMEWORK_CHERRYPICKS = 3; + + // Git tag applied to framework RC branch HEAD and pushed upstream. + PUBLISH_VERSION = 4; + + // RC branch HEAD pushed to upstream release branch. + // + // For example, flutter-1.2-candidate.3 -> upstream/beta + PUBLISH_CHANNEL = 5; + + // Package artifacts verified to exist on cloud storage. + VERIFY_RELEASE = 6; +} + +enum CherrypickState { + // The cherrypick has not yet been applied. + PENDING = 0; + + // The cherrypick has not been applied and will require manual resolution. + PENDING_WITH_CONFLICT = 1; + + // The cherrypick has been successfully applied to the local checkout. + // + // This state requires Cherrypick.appliedRevision to also be set. + COMPLETED = 2; + + // The cherrypick will NOT be applied in this release. + ABANDONED = 3; +} + +message Cherrypick { + // The revision on trunk to cherrypick. + string trunkRevision = 1; + + // Once applied, the actual commit revision of the cherrypick. + string appliedRevision = 2; + + CherrypickState state = 3; +} + +message Repository { + // The development git branch the release is based on. + // + // Must be of the form /flutter-(\d+)\.(\d+)-candidate\.(\d+)/ + string candidateBranch = 1; + + // The commit hash at the tip of the branch before cherrypicks were applied. + string startingGitHead = 2; + + // The difference in commits between this and [startingGitHead] is the number + // of cherrypicks that have been currently applied. + string currentGitHead = 3; + + // Path to the git checkout on local disk. + string checkoutPath = 4; + + // The remote commits will be fetched from. + Remote upstream = 5; + + // The remote cherrypicks will be pushed to to create a Pull Request. + // + // This should be a mirror owned by the user conducting the release. + Remote mirror = 6; + + // Desired cherrypicks. + repeated Cherrypick cherrypicks = 7; +} + +message ConductorState { + // One of 'stable', 'beta', or 'dev' + string releaseChannel = 1; + + // The name of the release. + string releaseVersion = 2; + + Repository engine = 4; + Repository framework = 5; + int64 createdDate = 6; + int64 lastUpdatedDate = 7; + + repeated string logs = 8; + + // The last [ReleasePhase] that was successfully completed. + ReleasePhase lastPhase = 9; + + // Commit hash of the Conductor tool. + string conductor_version = 10; +} diff --git a/dev/tools/lib/proto/license_header.txt b/dev/tools/lib/proto/license_header.txt new file mode 100644 index 0000000000..854d12e7cb --- /dev/null +++ b/dev/tools/lib/proto/license_header.txt @@ -0,0 +1,3 @@ +// 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. diff --git a/dev/tools/lib/repository.dart b/dev/tools/lib/repository.dart index b1872a49f5..2a8d7a5028 100644 --- a/dev/tools/lib/repository.dart +++ b/dev/tools/lib/repository.dart @@ -11,28 +11,68 @@ import 'package:process/process.dart'; import 'package:platform/platform.dart'; import './git.dart'; -import './globals.dart' as globals; +import './globals.dart'; import './stdio.dart'; import './version.dart'; +/// Allowed git remote names. +enum RemoteName { + upstream, + mirror, +} + +class Remote { + const Remote({ + @required RemoteName name, + @required this.url, + }) : _name = name; + + final RemoteName _name; + + /// The name of the remote. + String get name { + switch (_name) { + case RemoteName.upstream: + return 'upstream'; + case RemoteName.mirror: + return 'mirror'; + } + throw ConductorException('Invalid value of _name: $_name'); // For analyzer + } + + /// The URL of the remote. + final String url; +} + /// A source code repository. abstract class Repository { Repository({ @required this.name, - @required this.upstream, + @required this.fetchRemote, @required this.processManager, @required this.stdio, @required this.platform, @required this.fileSystem, @required this.parentDirectory, + this.initialRef, this.localUpstream = false, this.useExistingCheckout = false, + this.pushRemote, }) : git = Git(processManager), assert(localUpstream != null), assert(useExistingCheckout != null); final String name; - final String upstream; + final Remote fetchRemote; + + /// Remote to publish tags and commits to. + /// + /// This value can be null, in which case attempting to publish will lead to + /// a [ConductorException]. + final Remote pushRemote; + + /// The initial ref (branch or commit name) to check out. + final String initialRef; final Git git; final ProcessManager processManager; final Stdio stdio; @@ -55,33 +95,49 @@ abstract class Repository { return _checkoutDirectory; } _checkoutDirectory = parentDirectory.childDirectory(name); + lazilyInitialize(); + return _checkoutDirectory; + } + + /// Ensure the repository is cloned to disk and initialized with proper state. + void lazilyInitialize() { if (!useExistingCheckout && _checkoutDirectory.existsSync()) { stdio.printTrace('Deleting $name from ${_checkoutDirectory.path}...'); _checkoutDirectory.deleteSync(recursive: true); - } else if (useExistingCheckout && _checkoutDirectory.existsSync()) { - git.run( - ['checkout', 'master'], - 'Checkout to master branch', - workingDirectory: _checkoutDirectory.path, - ); - git.run( - ['pull', '--ff-only'], - 'Updating $name repo', - workingDirectory: _checkoutDirectory.path, - ); } + if (!_checkoutDirectory.existsSync()) { stdio.printTrace( - 'Cloning $name from $upstream to ${_checkoutDirectory.path}...'); + 'Cloning $name from ${fetchRemote.url} to ${_checkoutDirectory.path}...', + ); git.run( - ['clone', '--', upstream, _checkoutDirectory.path], + [ + 'clone', + '--origin', + fetchRemote.name, + '--', + fetchRemote.url, + _checkoutDirectory.path + ], 'Cloning $name repo', workingDirectory: parentDirectory.path, ); + if (pushRemote != null) { + git.run( + ['remote', 'add', pushRemote.name, pushRemote.url], + 'Adding remote ${pushRemote.url} as ${pushRemote.name}', + workingDirectory: _checkoutDirectory.path, + ); + git.run( + ['fetch', pushRemote.name], + 'Fetching git remote ${pushRemote.name}', + workingDirectory: _checkoutDirectory.path, + ); + } if (localUpstream) { // These branches must exist locally for the repo that depends on it // to fetch and push to. - for (final String channel in globals.kReleaseChannels) { + for (final String channel in kReleaseChannels) { git.run( ['checkout', channel, '--'], 'check out branch $channel locally', @@ -91,10 +147,17 @@ abstract class Repository { } } + if (initialRef != null) { + git.run( + ['checkout', '${fetchRemote.name}/$initialRef'], + 'Checking out initialRef $initialRef', + workingDirectory: _checkoutDirectory.path, + ); + } final String revision = reverseParse('HEAD'); - stdio - .printTrace('Repository $name is checked out at revision "$revision".'); - return _checkoutDirectory; + stdio.printTrace( + 'Repository $name is checked out at revision "$revision".', + ); } /// The URL of the remote named [remoteName]. @@ -117,6 +180,15 @@ abstract class Repository { return output == ''; } + /// Return the revision for the branch point between two refs. + String branchPoint(String firstRef, String secondRef) { + return git.getOutput( + ['merge-base', firstRef, secondRef], + 'determine the merge base between $firstRef and $secondRef', + workingDirectory: checkoutDirectory.path, + ).trim(); + } + /// Fetch all branches and associated commits and tags from [remoteName]. void fetch(String remoteName) { git.run( @@ -126,10 +198,22 @@ abstract class Repository { ); } - void checkout(String revision) { + /// Create (and checkout) a new branch based on the current HEAD. + /// + /// Runs `git checkout -b $branchName`. + void newBranch(String branchName) { git.run( - ['checkout', revision], - 'checkout $revision', + ['checkout', '-b', branchName], + 'create & checkout new branch $branchName', + workingDirectory: checkoutDirectory.path, + ); + } + + /// Check out the given ref. + void checkout(String ref) { + git.run( + ['checkout', ref], + 'checkout ref', workingDirectory: checkoutDirectory.path, ); } @@ -146,13 +230,25 @@ abstract class Repository { ); } + /// List commits in reverse chronological order. + List revList(List args) { + return git + .getOutput( + ['rev-list', ...args], + 'rev-list with args ${args.join(' ')}', + workingDirectory: checkoutDirectory.path, + ) + .trim() + .split('\n'); + } + /// Look up the commit for [ref]. String reverseParse(String ref) { final String revisionHash = git.getOutput( ['rev-parse', ref], 'look up the commit for the ref $ref', workingDirectory: checkoutDirectory.path, - ).trim(); + ); assert(revisionHash.isNotEmpty); return revisionHash; } @@ -184,11 +280,55 @@ abstract class Repository { return exitcode == 0; } - /// Resets repository HEAD to [commit]. - void reset(String commit) { + /// Determines if a commit will cherry-pick to current HEAD without conflict. + bool canCherryPick(String commit) { + assert( + gitCheckoutClean(), + 'cannot cherry-pick because git checkout ${checkoutDirectory.path} is not clean', + ); + + final int exitcode = git.run( + ['cherry-pick', '--no-commit', commit], + 'attempt to cherry-pick $commit without committing', + allowNonZeroExitCode: true, + workingDirectory: checkoutDirectory.path, + ); + + final bool result = exitcode == 0; + + if (result == false) { + stdio.printError(git.getOutput( + ['diff'], + 'get diff of failed cherry-pick', + workingDirectory: checkoutDirectory.path, + )); + } + + reset('HEAD'); + return result; + } + + /// Cherry-pick a [commit] to the current HEAD. + /// + /// This method will throw a [GitException] if the command fails. + void cherryPick(String commit) { + assert( + gitCheckoutClean(), + 'cannot cherry-pick because git checkout ${checkoutDirectory.path} is not clean', + ); + git.run( - ['reset', commit, '--hard'], - 'reset to the release commit', + ['cherry-pick', '--no-commit', commit], + 'attempt to cherry-pick $commit without committing', + workingDirectory: checkoutDirectory.path, + ); + } + + /// Resets repository HEAD to [ref]. + void reset(String ref) { + git.run( + ['reset', ref, '--hard'], + 'reset to $ref', workingDirectory: checkoutDirectory.path, ); } @@ -260,12 +400,17 @@ class FrameworkRepository extends Repository { FrameworkRepository( this.checkouts, { String name = 'framework', - String upstream = FrameworkRepository.defaultUpstream, + Remote fetchRemote = const Remote( + name: RemoteName.upstream, url: FrameworkRepository.defaultUpstream), bool localUpstream = false, bool useExistingCheckout = false, + String initialRef, + Remote pushRemote, }) : super( name: name, - upstream: upstream, + fetchRemote: fetchRemote, + pushRemote: pushRemote, + initialRef: initialRef, fileSystem: checkouts.fileSystem, localUpstream: localUpstream, parentDirectory: checkouts.directory, @@ -288,7 +433,10 @@ class FrameworkRepository extends Repository { return FrameworkRepository( checkouts, name: name, - upstream: 'file://$upstreamPath/', + fetchRemote: Remote( + name: RemoteName.upstream, + url: 'file://$upstreamPath/', + ), localUpstream: false, useExistingCheckout: useExistingCheckout, ); @@ -298,6 +446,8 @@ class FrameworkRepository extends Repository { static const String defaultUpstream = 'https://github.com/flutter/flutter.git'; + static const String defaultBranch = 'master'; + String get cacheDirectory => fileSystem.path.join( checkoutDirectory.path, 'bin', @@ -311,7 +461,8 @@ class FrameworkRepository extends Repository { return FrameworkRepository( checkouts, name: cloneName, - upstream: 'file://${checkoutDirectory.path}/', + fetchRemote: Remote( + name: RemoteName.upstream, url: 'file://${checkoutDirectory.path}/'), useExistingCheckout: useExistingCheckout, ); } @@ -345,8 +496,8 @@ class FrameworkRepository extends Repository { } @override - void checkout(String revision) { - super.checkout(revision); + void checkout(String ref) { + super.checkout(ref); // The tool will overwrite old cached artifacts, but not delete unused // artifacts from a previous version. Thus, delete the entire cache and // re-populate. @@ -363,12 +514,55 @@ class FrameworkRepository extends Repository { final io.ProcessResult result = runFlutter(['--version', '--machine']); final Map versionJson = jsonDecode( - globals.stdoutToString(result.stdout), + stdoutToString(result.stdout), ) as Map; return Version.fromString(versionJson['frameworkVersion'] as String); } } +class EngineRepository extends Repository { + EngineRepository( + this.checkouts, { + String name = 'engine', + String initialRef = EngineRepository.defaultBranch, + Remote fetchRemote = const Remote( + name: RemoteName.upstream, url: EngineRepository.defaultUpstream), + bool localUpstream = false, + bool useExistingCheckout = false, + Remote pushRemote, + }) : super( + name: name, + fetchRemote: fetchRemote, + pushRemote: pushRemote, + initialRef: initialRef, + fileSystem: checkouts.fileSystem, + localUpstream: localUpstream, + parentDirectory: checkouts.directory, + platform: checkouts.platform, + processManager: checkouts.processManager, + stdio: checkouts.stdio, + useExistingCheckout: useExistingCheckout, + ); + + final Checkouts checkouts; + + static const String defaultUpstream = 'https://github.com/flutter/engine.git'; + static const String defaultBranch = 'master'; + + @override + Repository cloneRepository(String cloneName) { + assert(localUpstream); + cloneName ??= 'clone-of-$name'; + return EngineRepository( + checkouts, + name: cloneName, + fetchRemote: Remote( + name: RemoteName.upstream, url: 'file://${checkoutDirectory.path}/'), + useExistingCheckout: useExistingCheckout, + ); + } +} + /// An enum of all the repositories that the Conductor supports. enum RepositoryType { framework, diff --git a/dev/tools/lib/roll_dev.dart b/dev/tools/lib/roll_dev.dart index 3fa1da1ff3..29b5c582eb 100644 --- a/dev/tools/lib/roll_dev.dart +++ b/dev/tools/lib/roll_dev.dart @@ -8,11 +8,18 @@ import 'package:file/file.dart'; import 'package:meta/meta.dart'; import 'package:platform/platform.dart'; -import './globals.dart'; import './repository.dart'; import './stdio.dart'; import './version.dart'; +const String kIncrement = 'increment'; +const String kCommit = 'commit'; +const String kRemoteName = 'remote'; +const String kJustPrint = 'just-print'; +const String kYes = 'yes'; +const String kForce = 'force'; +const String kSkipTagging = 'skip-tagging'; + /// Create a new dev release without cherry picks. class RollDevCommand extends Command { RollDevCommand({ @@ -57,7 +64,17 @@ class RollDevCommand extends Command { help: 'Do not create tag and push to remote, only update release branch. ' 'For recovering when the script fails trying to git push to the release branch.' ); - argParser.addFlag(kYes, negatable: false, abbr: 'y', help: 'Skip the confirmation prompt.'); + argParser.addFlag( + kYes, + negatable: false, + abbr: 'y', + help: 'Skip the confirmation prompt.', + ); + argParser.addOption( + kRemoteName, + help: 'Specifies which git remote to fetch from.', + defaultsTo: 'upstream', + ); } final Checkouts checkouts; @@ -92,8 +109,8 @@ bool rollDev({ @required ArgResults argResults, @required Stdio stdio, @required FrameworkRepository repository, - String remoteName = 'origin', }) { + final String remoteName = argResults[kRemoteName] as String; final String level = argResults[kIncrement] as String; final String commit = argResults[kCommit] as String; final bool justPrint = argResults[kJustPrint] as bool; diff --git a/dev/tools/lib/start.dart b/dev/tools/lib/start.dart new file mode 100644 index 0000000000..cffa6a164e --- /dev/null +++ b/dev/tools/lib/start.dart @@ -0,0 +1,348 @@ +// 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:convert' show jsonEncode; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:meta/meta.dart'; +import 'package:platform/platform.dart'; +import 'package:process/process.dart'; + +import './git.dart'; +import './globals.dart'; +import './proto/conductor_state.pb.dart' as pb; +import './proto/conductor_state.pbenum.dart' show ReleasePhase; +import './repository.dart'; +import './state.dart'; +import './stdio.dart'; + +const String kCandidateOption = 'candidate-branch'; +const String kReleaseOption = 'release-channel'; +const String kStateOption = 'state-file'; +const String kFrameworkMirrorOption = 'framework-mirror'; +const String kEngineMirrorOption = 'engine-mirror'; +const String kFrameworkUpstreamOption = 'framework-upstream'; +const String kEngineUpstreamOption = 'engine-upstream'; +const String kFrameworkCherrypicksOption = 'framework-cherrypicks'; +const String kEngineCherrypicksOption = 'engine-cherrypicks'; + +/// Command to print the status of the current Flutter release. +class StartCommand extends Command { + StartCommand({ + @required this.checkouts, + @required this.flutterRoot, + }) : platform = checkouts.platform, + processManager = checkouts.processManager, + fileSystem = checkouts.fileSystem, + stdio = checkouts.stdio { + final String defaultPath = defaultStateFilePath(platform); + argParser.addOption( + kCandidateOption, + help: 'The candidate branch the release will be based on.', + ); + argParser.addOption( + kReleaseOption, + help: 'The target release channel for the release.', + allowed: ['stable', 'beta', 'dev'], + ); + argParser.addOption( + kFrameworkUpstreamOption, + defaultsTo: FrameworkRepository.defaultUpstream, + help: + 'Configurable Framework repo upstream remote. Primarily for testing.', + hide: true, + ); + argParser.addOption( + kEngineUpstreamOption, + defaultsTo: EngineRepository.defaultUpstream, + help: 'Configurable Engine repo upstream remote. Primarily for testing.', + hide: true, + ); + argParser.addOption( + kFrameworkMirrorOption, + help: 'Framework repo mirror remote.', + ); + argParser.addOption( + kEngineMirrorOption, + help: 'Engine repo mirror remote.', + ); + argParser.addOption( + kStateOption, + defaultsTo: defaultPath, + help: 'Path to persistent state file. Defaults to $defaultPath', + ); + argParser.addMultiOption( + kEngineCherrypicksOption, + help: 'Engine cherrypick hashes to be applied.', + defaultsTo: [], + ); + argParser.addMultiOption( + kFrameworkCherrypicksOption, + help: 'Framework cherrypick hashes to be applied.', + defaultsTo: [], + ); + final Git git = Git(processManager); + conductorVersion = git.getOutput( + ['rev-parse', 'HEAD'], + 'look up the current revision.', + workingDirectory: flutterRoot.path, + ).trim(); + + assert(conductorVersion.isNotEmpty); + } + + final Checkouts checkouts; + + /// The root directory of the Flutter repository that houses the Conductor. + /// + /// This directory is used to check the git revision of the Conductor. + final Directory flutterRoot; + final FileSystem fileSystem; + final Platform platform; + final ProcessManager processManager; + final Stdio stdio; + + /// Git revision for the currently running Conductor. + String conductorVersion; + + @override + String get name => 'start'; + + @override + String get description => 'Initialize a new Flutter release.'; + + @override + void run() { + if (!platform.isMacOS && !platform.isLinux) { + throw ConductorException( + 'Error! This tool is only supported on macOS and Linux', + ); + } + + final File stateFile = checkouts.fileSystem.file( + getValueFromEnvOrArgs(kStateOption, argResults, platform.environment), + ); + if (stateFile.existsSync()) { + throw ConductorException( + 'Error! A persistent state file already found at ${argResults[kStateOption]}.\n\n' + 'Run `conductor clean` to cancel a previous release.'); + } + final String frameworkUpstream = getValueFromEnvOrArgs( + kFrameworkUpstreamOption, + argResults, + platform.environment, + ); + final String frameworkMirror = getValueFromEnvOrArgs( + kFrameworkMirrorOption, + argResults, + platform.environment, + ); + final String engineUpstream = getValueFromEnvOrArgs( + kEngineUpstreamOption, + argResults, + platform.environment, + ); + final String engineMirror = getValueFromEnvOrArgs( + kEngineMirrorOption, + argResults, + platform.environment, + ); + final String candidateBranch = getValueFromEnvOrArgs( + kCandidateOption, + argResults, + platform.environment, + ); + final String releaseChannel = getValueFromEnvOrArgs( + kReleaseOption, + argResults, + platform.environment, + ); + final List frameworkCherrypickRevisions = getValuesFromEnvOrArgs( + kFrameworkCherrypicksOption, + argResults, + platform.environment, + ); + final List engineCherrypickRevisions = getValuesFromEnvOrArgs( + kEngineCherrypicksOption, + argResults, + platform.environment, + ); + if (!releaseCandidateBranchRegex.hasMatch(candidateBranch)) { + throw ConductorException( + 'Invalid release candidate branch "$candidateBranch". ' + 'Text should match the regex pattern /${releaseCandidateBranchRegex.pattern}/.'); + } + + final Int64 unixDate = Int64(DateTime.now().millisecondsSinceEpoch); + final pb.ConductorState state = pb.ConductorState(); + + state.releaseChannel = releaseChannel; + state.createdDate = unixDate; + state.lastUpdatedDate = unixDate; + + final EngineRepository engine = EngineRepository( + checkouts, + initialRef: candidateBranch, + fetchRemote: Remote( + name: RemoteName.upstream, + url: engineUpstream, + ), + pushRemote: Remote( + name: RemoteName.mirror, + url: engineMirror, + ), + ); + // Create a new branch so that we don't accidentally push to upstream + // candidateBranch. + engine.newBranch('cherrypicks-$candidateBranch'); + final List engineCherrypicks = _sortCherrypicks( + repository: engine, + cherrypicks: engineCherrypickRevisions, + upstreamRef: EngineRepository.defaultBranch, + releaseRef: candidateBranch, + ).map((String revision) => pb.Cherrypick( + trunkRevision: revision, + state: pb.CherrypickState.PENDING, + )).toList(); + + for (final pb.Cherrypick cherrypick in engineCherrypicks) { + final String revision = cherrypick.trunkRevision; + final bool success = engine.canCherryPick(revision); + stdio.printTrace( + 'Attempt to cherrypick $revision ${success ? 'succeeded' : 'failed'}', + ); + if (!success) { + cherrypick.state = pb.CherrypickState.PENDING_WITH_CONFLICT; + } + } + final String engineHead = engine.reverseParse('HEAD'); + state.engine = pb.Repository( + candidateBranch: candidateBranch, + startingGitHead: engineHead, + currentGitHead: engineHead, + checkoutPath: engine.checkoutDirectory.path, + cherrypicks: engineCherrypicks, + ); + final FrameworkRepository framework = FrameworkRepository( + checkouts, + initialRef: candidateBranch, + fetchRemote: Remote( + name: RemoteName.upstream, + url: frameworkUpstream, + ), + pushRemote: Remote( + name: RemoteName.mirror, + url: frameworkMirror, + ), + ); + framework.newBranch('cherrypicks-$candidateBranch'); + final List frameworkCherrypicks = _sortCherrypicks( + repository: framework, + cherrypicks: frameworkCherrypickRevisions, + upstreamRef: FrameworkRepository.defaultBranch, + releaseRef: candidateBranch, + ).map((String revision) => pb.Cherrypick( + trunkRevision: revision, + state: pb.CherrypickState.PENDING, + )).toList(); + + for (final pb.Cherrypick cherrypick in frameworkCherrypicks) { + final String revision = cherrypick.trunkRevision; + final bool result = framework.canCherryPick(revision); + stdio.printTrace( + 'Attempt to cherrypick $cherrypick ${result ? 'succeeded' : 'failed'}', + ); + } + + final String frameworkHead = framework.reverseParse('HEAD'); + state.framework = pb.Repository( + candidateBranch: candidateBranch, + startingGitHead: frameworkHead, + currentGitHead: frameworkHead, + checkoutPath: framework.checkoutDirectory.path, + cherrypicks: frameworkCherrypicks, + ); + + state.lastPhase = ReleasePhase.INITIALIZE; + + state.conductorVersion = conductorVersion; + + stdio.printTrace('Writing state to file ${stateFile.path}...'); + + state.logs.addAll(stdio.logs); + + stateFile.writeAsStringSync( + jsonEncode(state.toProto3Json()), + flush: true, + ); + + stdio.printStatus(presentState(state)); + } + + // To minimize merge conflicts, sort the commits by rev-list order. + List _sortCherrypicks({ + @required Repository repository, + @required List cherrypicks, + @required String upstreamRef, + @required String releaseRef, + }) { + if (cherrypicks.isEmpty) { + return cherrypicks; + } + + // Input cherrypick hashes that failed to be parsed by git. + final List unknownCherrypicks = []; + // Full 40-char hashes parsed by git. + final List validatedCherrypicks = []; + // Final, validated, sorted list of cherrypicks to be applied. + final List sortedCherrypicks = []; + for (final String cherrypick in cherrypicks) { + try { + final String fullRef = repository.reverseParse(cherrypick); + validatedCherrypicks.add(fullRef); + } on GitException { + // Catch this exception so that we can validate the rest. + unknownCherrypicks.add(cherrypick); + } + } + + final String branchPoint = repository.branchPoint( + '${repository.fetchRemote.name}/$upstreamRef', + '${repository.fetchRemote.name}/$releaseRef', + ); + + // `git rev-list` returns newest first, so reverse this list + final List upstreamRevlist = repository.revList([ + '--ancestry-path', + '$branchPoint..$upstreamRef', + ]).reversed.toList(); + + stdio.printStatus('upstreamRevList:\n${upstreamRevlist.join('\n')}\n'); + stdio.printStatus('validatedCherrypicks:\n${validatedCherrypicks.join('\n')}\n'); + for (final String upstreamRevision in upstreamRevlist) { + if (validatedCherrypicks.contains(upstreamRevision)) { + validatedCherrypicks.remove(upstreamRevision); + sortedCherrypicks.add(upstreamRevision); + if (unknownCherrypicks.isEmpty && validatedCherrypicks.isEmpty) { + return sortedCherrypicks; + } + } + } + + // We were given input cherrypicks that were not present in the upstream + // rev-list + stdio.printError( + 'The following ${repository.name} cherrypicks were not found in the ' + 'upstream $upstreamRef branch:', + ); + for (final String cp in [...validatedCherrypicks, ...unknownCherrypicks]) { + stdio.printError('\t$cp'); + } + throw ConductorException( + '${validatedCherrypicks.length + unknownCherrypicks.length} unknown cherrypicks provided!', + ); + } +} diff --git a/dev/tools/lib/state.dart b/dev/tools/lib/state.dart new file mode 100644 index 0000000000..76f52a43da --- /dev/null +++ b/dev/tools/lib/state.dart @@ -0,0 +1,167 @@ +// 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:platform/platform.dart'; + +import './globals.dart'; +import './proto/conductor_state.pb.dart' as pb; +import './proto/conductor_state.pbenum.dart' show ReleasePhase; + +const String kStateFileName = '.flutter_conductor_state.json'; + +String luciConsoleLink(String channel, String groupName) { + assert( + ['stable', 'beta', 'dev', 'master'].contains(channel), + 'channel $channel not recognized', + ); + assert( + ['framework', 'engine', 'devicelab'].contains(groupName), + 'group named $groupName not recognized', + ); + final String consoleName = channel == 'master' ? groupName : '${channel}_$groupName'; + return 'https://ci.chromium.org/p/flutter/g/$consoleName/console'; +} + +String defaultStateFilePath(Platform platform) { + assert(platform.environment['HOME'] != null); + return [ + platform.environment['HOME'], + kStateFileName, + ].join(platform.pathSeparator); +} + +String presentState(pb.ConductorState state) { + final StringBuffer buffer = StringBuffer(); + buffer.writeln('Conductor version: ${state.conductorVersion}'); + buffer.writeln('Release channel: ${state.releaseChannel}'); + buffer.writeln(''); + buffer.writeln( + 'Release started at: ${DateTime.fromMillisecondsSinceEpoch(state.createdDate.toInt())}'); + buffer.writeln( + 'Last updated at: ${DateTime.fromMillisecondsSinceEpoch(state.lastUpdatedDate.toInt())}'); + buffer.writeln(''); + buffer.writeln('Engine Repo'); + buffer.writeln('\tCandidate branch: ${state.engine.candidateBranch}'); + buffer.writeln('\tStarting git HEAD: ${state.engine.startingGitHead}'); + buffer.writeln('\tCurrent git HEAD: ${state.engine.currentGitHead}'); + buffer.writeln('\tPath to checkout: ${state.engine.checkoutPath}'); + buffer.writeln('\tPost-submit LUCI dashboard: ${luciConsoleLink(state.releaseChannel, 'engine')}'); + if (state.engine.cherrypicks.isNotEmpty) { + buffer.writeln('${state.engine.cherrypicks.length} Engine Cherrypicks:'); + for (final pb.Cherrypick cherrypick in state.engine.cherrypicks) { + buffer.writeln('\t${cherrypick.trunkRevision} - ${cherrypick.state}'); + } + } else { + buffer.writeln('0 Engine cherrypicks.'); + } + buffer.writeln('Framework Repo'); + buffer.writeln('\tCandidate branch: ${state.framework.candidateBranch}'); + buffer.writeln('\tStarting git HEAD: ${state.framework.startingGitHead}'); + buffer.writeln('\tCurrent git HEAD: ${state.framework.currentGitHead}'); + buffer.writeln('\tPath to checkout: ${state.framework.checkoutPath}'); + buffer.writeln('\tPost-submit LUCI dashboard: ${luciConsoleLink(state.releaseChannel, 'framework')}'); + buffer.writeln('\tDevicelab LUCI dashboard: ${luciConsoleLink(state.releaseChannel, 'devicelab')}'); + if (state.framework.cherrypicks.isNotEmpty) { + buffer.writeln('${state.framework.cherrypicks.length} Framework Cherrypicks:'); + for (final pb.Cherrypick cherrypick in state.framework.cherrypicks) { + buffer.writeln('\t${cherrypick.trunkRevision} - ${cherrypick.state}'); + } + } else { + buffer.writeln('0 Framework cherrypicks.'); + } + buffer.writeln(''); + if (state.lastPhase == ReleasePhase.VERIFY_RELEASE) { + buffer.writeln( + '${state.releaseChannel} release ${state.releaseVersion} has been published and verified.\n', + ); + return buffer.toString(); + } + buffer.writeln('The next step is:'); + buffer.writeln(presentPhases(state.lastPhase)); + + buffer.writeln(phaseInstructions(state)); + buffer.writeln(''); + buffer.writeln('Issue `conductor next` when you are ready to proceed.'); + return buffer.toString(); +} + +String presentPhases(ReleasePhase lastPhase) { + final ReleasePhase nextPhase = getNextPhase(lastPhase); + final StringBuffer buffer = StringBuffer(); + bool phaseCompleted = true; + + for (final ReleasePhase phase in ReleasePhase.values) { + if (phase == nextPhase) { + // This phase will execute the next time `conductor next` is run. + buffer.writeln('> ${phase.name} (next)'); + phaseCompleted = false; + } else if (phaseCompleted) { + // This phase was already completed. + buffer.writeln('✓ ${phase.name}'); + } else { + // This phase has not been completed yet. + buffer.writeln(' ${phase.name}'); + } + } + return buffer.toString(); +} + +String phaseInstructions(pb.ConductorState state) { + switch (state.lastPhase) { + case ReleasePhase.INITIALIZE: + if (state.engine.cherrypicks.isEmpty) { + return [ + 'There are no engine cherrypicks, so issue `conductor next` to continue', + 'to the next step.', + ].join('\n'); + } + return [ + 'You must now manually apply the following engine cherrypicks to the checkout', + 'at ${state.engine.checkoutPath} in order:', + for (final pb.Cherrypick cherrypick in state.engine.cherrypicks) + '\t${cherrypick.trunkRevision}', + 'See $kReleaseDocumentationUrl for more information.', + ].join('\n'); + case ReleasePhase.APPLY_ENGINE_CHERRYPICKS: + return [ + 'You must verify Engine CI builds are successful and then codesign the', + 'binaries at revision ${state.engine.currentGitHead}.', + ].join('\n'); + case ReleasePhase.CODESIGN_ENGINE_BINARIES: + return [ + 'You must now manually apply the following framework cherrypicks to the checkout', + 'at ${state.framework.checkoutPath} in order:', + for (final pb.Cherrypick cherrypick in state.framework.cherrypicks) + '\t${cherrypick.trunkRevision}', + ].join('\n'); + case ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS: + return [ + 'You must verify Framework CI builds are successful.', + 'See $kReleaseDocumentationUrl for more information.', + ].join('\n'); + case ReleasePhase.PUBLISH_VERSION: + return 'Issue `conductor next` to publish your release to the release branch.'; + case ReleasePhase.PUBLISH_CHANNEL: + return [ + 'Release archive packages must be verified on cloud storage. Issue', + '`conductor next` to check if they are ready.', + ].join('\n'); + case ReleasePhase.VERIFY_RELEASE: + return 'This release has been completed.'; + } + assert(false); + return ''; // For analyzer +} + +/// Returns the next phase in the ReleasePhase enum. +/// +/// Will throw a [ConductorException] if [ReleasePhase.RELEASE_VERIFIED] is +/// passed as an argument, as there is no next phase. +ReleasePhase getNextPhase(ReleasePhase previousPhase) { + assert(previousPhase != null); + if (previousPhase == ReleasePhase.VERIFY_RELEASE) { + throw ConductorException('There is no next ReleasePhase!'); + } + return ReleasePhase.valueOf(previousPhase.value + 1); +} diff --git a/dev/tools/lib/status.dart b/dev/tools/lib/status.dart new file mode 100644 index 0000000000..2b6dc42277 --- /dev/null +++ b/dev/tools/lib/status.dart @@ -0,0 +1,68 @@ +// 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:convert' show jsonDecode; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:meta/meta.dart'; +import 'package:platform/platform.dart'; + +import './proto/conductor_state.pb.dart' as pb; +import './repository.dart'; +import './state.dart'; +import './stdio.dart'; + +const String kVerboseFlag = 'verbose'; +const String kStateOption = 'state-file'; + +/// Command to print the status of the current Flutter release. +class StatusCommand extends Command { + StatusCommand({ + @required this.checkouts, + }) : platform = checkouts.platform, + fileSystem = checkouts.fileSystem, + stdio = checkouts.stdio { + final String defaultPath = defaultStateFilePath(platform); + argParser.addOption( + kStateOption, + defaultsTo: defaultPath, + help: 'Path to persistent state file. Defaults to $defaultPath', + ); + argParser.addFlag( + kVerboseFlag, + abbr: 'v', + defaultsTo: false, + help: 'Also print logs.', + ); + } + + final Checkouts checkouts; + final FileSystem fileSystem; + final Platform platform; + final Stdio stdio; + + @override + String get name => 'status'; + + @override + String get description => 'Print status of current release.'; + + @override + void run() { + final File stateFile = checkouts.fileSystem.file(argResults[kStateOption]); + if (!stateFile.existsSync()) { + stdio.printStatus( + 'No persistent state file found at ${argResults[kStateOption]}.'); + return; + } + final pb.ConductorState state = pb.ConductorState(); + state.mergeFromProto3Json(jsonDecode(stateFile.readAsStringSync())); + stdio.printStatus(presentState(state)); + if (argResults[kVerboseFlag] as bool) { + stdio.printStatus('\nLogs:'); + state.logs.forEach(stdio.printStatus); + } + } +} diff --git a/dev/tools/lib/stdio.dart b/dev/tools/lib/stdio.dart index d5c4477bee..83be05107d 100644 --- a/dev/tools/lib/stdio.dart +++ b/dev/tools/lib/stdio.dart @@ -7,17 +7,31 @@ import 'dart:io' as io; import 'package:meta/meta.dart'; abstract class Stdio { + final List logs = []; + /// Error/warning messages printed to STDERR. - void printError(String message); + @mustCallSuper + void printError(String message) { + logs.add('[error] $message'); + } /// Ordinary STDOUT messages. - void printStatus(String message); + @mustCallSuper + void printStatus(String message) { + logs.add('[status] $message'); + } /// Debug messages that are only printed in verbose mode. - void printTrace(String message); + @mustCallSuper + void printTrace(String message) { + logs.add('[trace] $message'); + } /// Write string to STDOUT without trailing newline. - void write(String message); + @mustCallSuper + void write(String message) { + logs.add('[write] $message'); + } /// Read a line of text from STDIN. String readLineSync(); @@ -43,21 +57,25 @@ class VerboseStdio extends Stdio { @override void printError(String message) { + super.printError(message); stderr.writeln(message); } @override void printStatus(String message) { + super.printStatus(message); stdout.writeln(message); } @override void printTrace(String message) { + super.printTrace(message); stdout.writeln(message); } @override void write(String message) { + super.write(message); stdout.write(message); } diff --git a/dev/tools/pubspec.yaml b/dev/tools/pubspec.yaml index 36b940a3b6..2b44a8fd3c 100644 --- a/dev/tools/pubspec.yaml +++ b/dev/tools/pubspec.yaml @@ -12,12 +12,14 @@ dependencies: meta: 1.3.0 path: 1.8.0 process: 4.2.1 + protobuf: 1.1.3 charcode: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" clock: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" collection: 1.15.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" crypto: 3.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" file: 6.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + fixnum: 0.10.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" http_parser: 4.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" pedantic: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" platform: 3.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -63,4 +65,4 @@ dev_dependencies: webkit_inspection_protocol: 1.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: 8750 +# PUBSPEC CHECKSUM: e555 diff --git a/dev/tools/test/clean_test.dart b/dev/tools/test/clean_test.dart new file mode 100644 index 0000000000..2c84a5767e --- /dev/null +++ b/dev/tools/test/clean_test.dart @@ -0,0 +1,97 @@ +// 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:args/command_runner.dart'; +import 'package:dev_tools/clean.dart'; +import 'package:dev_tools/repository.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:platform/platform.dart'; + +import '../../../packages/flutter_tools/test/src/fake_process_manager.dart'; +import './common.dart'; + +void main() { + group('clean command', () { + const String flutterRoot = '/flutter'; + const String checkoutsParentDirectory = '$flutterRoot/dev/tools/'; + + MemoryFileSystem fileSystem; + FakePlatform platform; + TestStdio stdio; + FakeProcessManager processManager; + + setUp(() { + stdio = TestStdio(); + fileSystem = MemoryFileSystem.test(); + }); + + tearDown(() { + // Ensure these don't get re-used between tests + stdio = null; + fileSystem = null; + processManager = null; + platform = null; + }); + + CommandRunner createRunner({ + List commands, + String operatingSystem, + }) { + operatingSystem ??= const LocalPlatform().operatingSystem; + final String pathSeparator = operatingSystem == 'windows' ? r'\' : '/'; + + processManager = FakeProcessManager.list(commands ?? []); + platform = FakePlatform( + environment: {'HOME': '/path/to/user/home'}, + pathSeparator: pathSeparator, + ); + final Checkouts checkouts = Checkouts( + fileSystem: fileSystem, + parentDirectory: fileSystem.directory(checkoutsParentDirectory), + platform: platform, + processManager: processManager, + stdio: stdio, + ); + final CleanCommand command = CleanCommand( + checkouts: checkouts, + ); + return CommandRunner('clean-test', '')..addCommand(command); + } + + test('throws if no state file found', () async { + final CommandRunner runner = createRunner(); + const String stateFile = '/state-file.json'; + + await expectLater( + () async => runner.run([ + 'clean', + '--$kStateOption', + stateFile, + '--$kYesFlag', + ]), + throwsExceptionWith( + 'No persistent state file found at $stateFile', + ), + ); + }); + + test('deletes state file', () async { + final CommandRunner runner = createRunner(); + final File stateFile = fileSystem.file('/state-file.json'); + stateFile.writeAsStringSync('{}'); + + await runner.run([ + 'clean', + '--$kStateOption', + stateFile.path, + '--$kYesFlag', + ]); + + expect(stateFile.existsSync(), false); + }); + }, onPlatform: { + 'windows': const Skip('Flutter Conductor only supported on macos/linux'), + }); +} diff --git a/dev/tools/test/codesign_integration_test.dart b/dev/tools/test/codesign_integration_test.dart index c11ece3061..e90ba1e9c3 100644 --- a/dev/tools/test/codesign_integration_test.dart +++ b/dev/tools/test/codesign_integration_test.dart @@ -21,11 +21,14 @@ void main() { () async { const Platform platform = LocalPlatform(); const FileSystem fileSystem = LocalFileSystem(); + final Directory tempDir = fileSystem.systemTempDirectory.createTempSync( + 'conductor_integration_test', + ); const ProcessManager processManager = LocalProcessManager(); final TestStdio stdio = TestStdio(verbose: true); final Checkouts checkouts = Checkouts( fileSystem: fileSystem, - parentDirectory: localFlutterRoot.parent, + parentDirectory: tempDir, platform: platform, processManager: processManager, stdio: stdio, diff --git a/dev/tools/test/codesign_test.dart b/dev/tools/test/codesign_test.dart index 51897d2c4f..25c436de1e 100644 --- a/dev/tools/test/codesign_test.dart +++ b/dev/tools/test/codesign_test.dart @@ -109,6 +109,8 @@ void main() { const FakeCommand(command: [ 'git', 'clone', + '--origin', + 'upstream', '--', 'file://$flutterRoot/', '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', @@ -194,6 +196,8 @@ void main() { const FakeCommand(command: [ 'git', 'clone', + '--origin', + 'upstream', '--', 'file://$flutterRoot/', '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', @@ -279,6 +283,8 @@ void main() { const FakeCommand(command: [ 'git', 'clone', + '--origin', + 'upstream', '--', 'file://$flutterRoot/', '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', @@ -336,6 +342,8 @@ void main() { const FakeCommand(command: [ 'git', 'clone', + '--origin', + 'upstream', '--', 'file://$flutterRoot/', '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', diff --git a/dev/tools/test/common.dart b/dev/tools/test/common.dart index ce2398ec95..306c33ceda 100644 --- a/dev/tools/test/common.dart +++ b/dev/tools/test/common.dart @@ -35,7 +35,7 @@ Matcher throwsExceptionWith(String messageSubString) { ); } -class TestStdio implements Stdio { +class TestStdio extends Stdio { TestStdio({ this.verbose = false, List stdin, @@ -43,36 +43,15 @@ class TestStdio implements Stdio { _stdin = stdin ?? []; } - final StringBuffer _error = StringBuffer(); - String get error => _error.toString(); + String get error => logs.where((String log) => log.startsWith(r'[error] ')).join('\n'); + + String get stdout => logs.where((String log) { + return log.startsWith(r'[status] ') || log.startsWith(r'[trace] '); + }).join('\n'); - final StringBuffer _stdout = StringBuffer(); - String get stdout => _stdout.toString(); final bool verbose; List _stdin; - @override - void printError(String message) { - _error.writeln(message); - } - - @override - void printStatus(String message) { - _stdout.writeln(message); - } - - @override - void printTrace(String message) { - if (verbose) { - _stdout.writeln(message); - } - } - - @override - void write(String message) { - _stdout.write(message); - } - @override String readLineSync() { if (_stdin.isEmpty) { diff --git a/dev/tools/test/roll_dev_integration_test.dart b/dev/tools/test/roll_dev_integration_test.dart index ebc751429f..8d92e160a7 100644 --- a/dev/tools/test/roll_dev_integration_test.dart +++ b/dev/tools/test/roll_dev_integration_test.dart @@ -7,7 +7,6 @@ import 'package:file/local.dart'; import 'package:platform/platform.dart'; import 'package:process/process.dart'; -import 'package:dev_tools/globals.dart'; import 'package:dev_tools/roll_dev.dart' show rollDev; import 'package:dev_tools/repository.dart'; import 'package:dev_tools/version.dart'; @@ -25,15 +24,17 @@ void main() { Checkouts checkouts; FrameworkRepository frameworkUpstream; FrameworkRepository framework; + Directory tempDir; setUp(() { platform = const LocalPlatform(); fileSystem = const LocalFileSystem(); processManager = const LocalProcessManager(); stdio = TestStdio(verbose: true); + tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_conductor_checkouts'); checkouts = Checkouts( fileSystem: fileSystem, - parentDirectory: localFlutterRoot.parent, + parentDirectory: tempDir, platform: platform, processManager: processManager, stdio: stdio, @@ -45,7 +46,7 @@ void main() { framework = FrameworkRepository( checkouts, name: 'test-framework', - upstream: 'file://${frameworkUpstream.checkoutDirectory.path}/', + fetchRemote: Remote(name: RemoteName.upstream, url: 'file://${frameworkUpstream.checkoutDirectory.path}/'), ); }); @@ -59,7 +60,7 @@ void main() { commit: latestCommit, // Ensure this test passes after a dev release with hotfixes force: true, - remote: 'origin', + remote: 'upstream', ); expect( @@ -96,7 +97,7 @@ void main() { commit: latestCommit, // Ensure this test passes after a dev release with hotfixes force: true, - remote: 'origin', + remote: 'upstream', ); expect( @@ -124,7 +125,7 @@ void main() { expect(finalVersion.m, 0); expect(finalVersion.n, 0); expect(finalVersion.commits, null); - }, skip: 'TODO(fujino): https://github.com/flutter/flutter/issues/80463'); + }); }, onPlatform: { 'windows': const Skip('Flutter Conductor only supported on macos/linux'), }); diff --git a/dev/tools/test/roll_dev_test.dart b/dev/tools/test/roll_dev_test.dart index e88cdaff2e..2cfeb91b00 100644 --- a/dev/tools/test/roll_dev_test.dart +++ b/dev/tools/test/roll_dev_test.dart @@ -83,6 +83,8 @@ void main() { const FakeCommand(command: [ 'git', 'clone', + '--origin', + 'upstream', '--', kUpstreamRemote, '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', @@ -131,6 +133,8 @@ void main() { const FakeCommand(command: [ 'git', 'clone', + '--origin', + 'upstream', '--', kUpstreamRemote, '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', @@ -193,16 +197,16 @@ void main() { ), false, ); - expect(stdio.stdout.contains(nextVersion), true); + expect(stdio.logs.join('').contains(nextVersion), true); }); - test( - 'exits with exception if --skip-tagging is provided but commit isn\'t ' - 'already tagged', () { + test("exits with exception if --skip-tagging is provided but commit isn't already tagged", () { processManager.addCommands([ const FakeCommand(command: [ 'git', 'clone', + '--origin', + 'upstream', '--', kUpstreamRemote, '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', @@ -283,6 +287,8 @@ void main() { const FakeCommand(command: [ 'git', 'clone', + '--origin', + 'upstream', '--', kUpstreamRemote, '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', @@ -356,6 +362,8 @@ void main() { const FakeCommand(command: [ 'git', 'clone', + '--origin', + 'upstream', '--', kUpstreamRemote, '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', @@ -433,6 +441,8 @@ void main() { const FakeCommand(command: [ 'git', 'clone', + '--origin', + 'upstream', '--', kUpstreamRemote, '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', @@ -526,6 +536,8 @@ void main() { const FakeCommand(command: [ 'git', 'clone', + '--origin', + 'upstream', '--', kUpstreamRemote, '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', @@ -623,6 +635,8 @@ void main() { const FakeCommand(command: [ 'git', 'clone', + '--origin', + 'upstream', '--', kUpstreamRemote, '${checkoutsParentDirectory}flutter_conductor_checkouts/framework', diff --git a/dev/tools/test/start_test.dart b/dev/tools/test/start_test.dart new file mode 100644 index 0000000000..6cb124cb2b --- /dev/null +++ b/dev/tools/test/start_test.dart @@ -0,0 +1,255 @@ +// 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:convert' show jsonDecode; + +import 'package:args/command_runner.dart'; +import 'package:dev_tools/proto/conductor_state.pb.dart' as pb; +import 'package:dev_tools/proto/conductor_state.pbenum.dart' show ReleasePhase; +import 'package:dev_tools/start.dart'; +import 'package:dev_tools/state.dart'; +import 'package:dev_tools/repository.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:platform/platform.dart'; + +import '../../../packages/flutter_tools/test/src/fake_process_manager.dart'; +import './common.dart'; + +void main() { + group('start command', () { + const String flutterRoot = '/flutter'; + const String checkoutsParentDirectory = '$flutterRoot/dev/tools/'; + const String frameworkMirror = 'https://github.com/user/flutter.git'; + const String engineMirror = 'https://github.com/user/engine.git'; + const String candidateBranch = 'flutter-1.2-candidate.3'; + const String releaseChannel = 'stable'; + const String revision = 'abcd1234'; + Checkouts checkouts; + MemoryFileSystem fileSystem; + FakePlatform platform; + TestStdio stdio; + FakeProcessManager processManager; + + setUp(() { + stdio = TestStdio(); + fileSystem = MemoryFileSystem.test(); + }); + + CommandRunner createRunner({ + Map environment, + String operatingSystem, + List commands, + }) { + operatingSystem ??= const LocalPlatform().operatingSystem; + final String pathSeparator = operatingSystem == 'windows' ? r'\' : '/'; + environment ??= { + 'HOME': '/path/to/user/home', + }; + final Directory homeDir = fileSystem.directory( + environment['HOME'], + ); + // Tool assumes this exists + homeDir.createSync(recursive: true); + platform = FakePlatform( + environment: environment, + operatingSystem: operatingSystem, + pathSeparator: pathSeparator, + ); + processManager = FakeProcessManager.list(commands ?? []); + checkouts = Checkouts( + fileSystem: fileSystem, + parentDirectory: fileSystem.directory(checkoutsParentDirectory), + platform: platform, + processManager: processManager, + stdio: stdio, + ); + final StartCommand command = StartCommand( + checkouts: checkouts, + flutterRoot: fileSystem.directory(flutterRoot), + ); + return CommandRunner('codesign-test', '')..addCommand(command); + } + + tearDown(() { + // Ensure we don't re-use these between tests. + processManager = null; + checkouts = null; + platform = null; + }); + + test('throws exception if run from Windows', () async { + final CommandRunner runner = createRunner( + commands: [ + const FakeCommand( + command: ['git', 'rev-parse', 'HEAD'], + stdout: revision, + ), + ], + operatingSystem: 'windows', + ); + await expectLater( + () async => runner.run(['start']), + throwsExceptionWith( + 'Error! This tool is only supported on macOS and Linux', + ), + ); + }); + + test('throws if --$kFrameworkMirrorOption not provided', () async { + final CommandRunner runner = createRunner( + commands: [ + const FakeCommand( + command: ['git', 'rev-parse', 'HEAD'], + stdout: revision, + ), + ], + ); + + await expectLater( + () async => runner.run(['start']), + throwsExceptionWith( + 'Expected either the CLI arg --$kFrameworkMirrorOption or the environment variable FRAMEWORK_MIRROR to be provided', + ), + ); + }); + + test('creates state file if provided correct inputs', () async { + const String revision2 = 'def789'; + const String revision3 = '123abc'; + + final List engineCommands = [ + FakeCommand( + command: [ + 'git', + 'clone', + '--origin', + 'upstream', + '--', + EngineRepository.defaultUpstream, + fileSystem.path.join( + checkoutsParentDirectory, + 'flutter_conductor_checkouts', + 'engine', + ), + ], + ), + const FakeCommand( + command: ['git', 'remote', 'add', 'mirror', engineMirror], + ), + const FakeCommand( + command: ['git', 'fetch', 'mirror'], + ), + const FakeCommand( + command: ['git', 'checkout', 'upstream/$candidateBranch'], + ), + const FakeCommand( + command: ['git', 'rev-parse', 'HEAD'], + stdout: revision2, + ), + const FakeCommand( + command: [ + 'git', + 'checkout', + '-b', + 'cherrypicks-$candidateBranch', + ], + ), + const FakeCommand( + command: ['git', 'rev-parse', 'HEAD'], + stdout: revision2, + ), + ]; + final List frameworkCommands = [ + FakeCommand( + command: [ + 'git', + 'clone', + '--origin', + 'upstream', + '--', + FrameworkRepository.defaultUpstream, + fileSystem.path.join( + checkoutsParentDirectory, + 'flutter_conductor_checkouts', + 'framework', + ), + ], + ), + const FakeCommand( + command: ['git', 'remote', 'add', 'mirror', frameworkMirror], + ), + const FakeCommand( + command: ['git', 'fetch', 'mirror'], + ), + const FakeCommand( + command: ['git', 'checkout', 'upstream/$candidateBranch'], + ), + const FakeCommand( + command: ['git', 'rev-parse', 'HEAD'], + stdout: revision3, + ), + const FakeCommand( + command: [ + 'git', + 'checkout', + '-b', + 'cherrypicks-$candidateBranch', + ], + ), + const FakeCommand( + command: ['git', 'rev-parse', 'HEAD'], + stdout: revision3, + ), + ]; + final CommandRunner runner = createRunner( + commands: [ + const FakeCommand( + command: ['git', 'rev-parse', 'HEAD'], + stdout: revision, + ), + ...engineCommands, + ...frameworkCommands, + ], + ); + + final String stateFilePath = fileSystem.path.join( + platform.environment['HOME'], + kStateFileName, + ); + + await runner.run([ + 'start', + '--$kFrameworkMirrorOption', + frameworkMirror, + '--$kEngineMirrorOption', + engineMirror, + '--$kCandidateOption', + candidateBranch, + '--$kReleaseOption', + releaseChannel, + '--$kStateOption', + stateFilePath, + ]); + + final File stateFile = fileSystem.file(stateFilePath); + + final pb.ConductorState state = pb.ConductorState(); + state.mergeFromProto3Json( + jsonDecode(stateFile.readAsStringSync()), + ); + + expect(state.isInitialized(), true); + expect(state.releaseChannel, releaseChannel); + expect(state.engine.candidateBranch, candidateBranch); + expect(state.engine.startingGitHead, revision2); + expect(state.framework.candidateBranch, candidateBranch); + expect(state.framework.startingGitHead, revision3); + expect(state.lastPhase, ReleasePhase.INITIALIZE); + expect(state.conductorVersion, revision); + }); + }, onPlatform: { + 'windows': const Skip('Flutter Conductor only supported on macos/linux'), + }); +}