blob: 2776186f38bfc891e4a93ab96530fb692c5a7256 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:devtools_tool/model.dart';
import 'package:io/io.dart';
import 'package:path/path.dart' as path;
abstract class DartSdkHelper {
static const commandDebugMessage = 'Consider running this command from your'
'Dart SDK directory locally to debug.';
static Future<void> fetchAndCheckoutMaster(
ProcessManager processManager,
) async {
final dartSdkLocation = localDartSdkLocation();
await processManager.runAll(
workingDirectory: dartSdkLocation,
additionalErrorMessage: commandDebugMessage,
commands: [
CliCommand.git(['fetch', 'origin']),
CliCommand.git(['rebase-update']),
CliCommand.git(['checkout', 'origin/main']),
],
);
}
}
String localDartSdkLocation() {
final localDartSdkLocation = Platform.environment['LOCAL_DART_SDK'];
if (localDartSdkLocation == null) {
throw Exception('LOCAL_DART_SDK environment variable not set. Please add '
'the following to your \'.bash_profile\' or \'.bash_rc\' file:\n'
'export LOCAL_DART_SDK=<absolute/path/to/my/dart/sdk>');
}
return localDartSdkLocation;
}
class CliCommand {
CliCommand(
this.exe,
// Args is mandatory to make it clearer to the caller that they should
// not be passing a full exe+args into the first string argument, because
// this can lead to bugs if paths have spaces and everything is not escaped.
this.args, {
this.throwOnException = true,
});
factory CliCommand.flutter(
List<String> args, {
bool throwOnException = true,
}) {
return CliCommand(
FlutterSdk.current.flutterExePath,
args,
throwOnException: throwOnException,
);
}
factory CliCommand.dart(
List<String> args, {
bool throwOnException = true,
}) {
return CliCommand(
FlutterSdk.current.dartExePath,
args,
throwOnException: throwOnException,
);
}
/// CliCommand helper for running git commands.
factory CliCommand.git(
List<String> args, {
bool throwOnException = true,
}) {
return CliCommand(
'git',
args,
throwOnException: throwOnException,
);
}
factory CliCommand.tool(
List<String> args, {
bool throwOnException = true,
}) {
return CliCommand(
// We must use the Dart VM from FlutterSdk.current here to ensure we
// consistently use the selected version for child invocations. We do
// not need to pass the --flutter-from-path flag down because using the
// tool will automatically select the one that's running the VM and we'll
// have selected that here.
FlutterSdk.current.dartExePath,
[
Platform.script.toFilePath(),
...args,
],
throwOnException: throwOnException,
);
}
late final String exe;
late final List<String> args;
final bool throwOnException;
@override
String toString() {
return [exe, ...args].join(' ');
}
}
typedef DevToolsProcessResult = ({int exitCode, String stdout, String stderr});
extension DevToolsProcessManagerExtension on ProcessManager {
Future<DevToolsProcessResult> runProcess(
CliCommand command, {
String? workingDirectory,
String? additionalErrorMessage = '',
}) async {
print('${workingDirectory ?? ''} > $command');
final processStdout = StringBuffer();
final processStderr = StringBuffer();
final process = await spawn(
command.exe,
command.args,
workingDirectory: workingDirectory,
);
process.stdout.transform(utf8.decoder).listen(processStdout.write);
process.stderr.transform(utf8.decoder).listen(processStderr.write);
final code = await process.exitCode;
if (command.throwOnException && code != 0) {
throw ProcessException(
command.exe,
command.args,
'Failed with exit code: $code. $additionalErrorMessage',
code,
);
}
return (
exitCode: code,
stdout: processStdout.toString(),
stderr: processStderr.toString()
);
}
Future<void> runAll({
required List<CliCommand> commands,
String? workingDirectory,
String? additionalErrorMessage = '',
}) async {
for (final command in commands) {
await runProcess(
command,
workingDirectory: workingDirectory,
additionalErrorMessage: additionalErrorMessage,
);
}
}
}
String pathFromRepoRoot(String pathFromRoot) {
return path.join(DevToolsRepo.getInstance().repoPath, pathFromRoot);
}
/// Returns the name of the git remote with id [remoteId] in
/// [workingDirectory].
///
/// When [workingDirectory] is null, this method will look for the remote in
/// the current directory.
///
/// [remoteId] should have the form <organization>/<repository>.git. For
/// example: 'flutter/flutter.git' or 'flutter/devtools.git'.
Future<String> findRemote(
ProcessManager processManager, {
required String remoteId,
String? workingDirectory,
}) async {
print('Searching for a remote that points to $remoteId.');
final remotesResult = await processManager.runProcess(
CliCommand.git(['remote', '-v']),
workingDirectory: workingDirectory,
);
final String remotes = remotesResult.stdout;
final remoteRegexp = RegExp(
r'^(?<remote>\S+)\s+(?<path>\S+)\s+\((?<action>\S+)\)',
multiLine: true,
);
final remoteRegexpResults = remoteRegexp.allMatches(remotes);
final RegExpMatch upstreamRemoteResult;
try {
upstreamRemoteResult = remoteRegexpResults.firstWhere(
(element) =>
// ignore: prefer_interpolation_to_compose_strings
RegExp(r'' + remoteId + '\$').hasMatch(element.namedGroup('path')!),
);
} on StateError {
throw StateError(
"Couldn't find a remote that points to flutter/devtools.git. "
"Instead got: \n$remotes",
);
}
final remoteUpstream = upstreamRemoteResult.namedGroup('remote')!;
print('Found upstream remote.');
return remoteUpstream;
}
extension JoinExtension on List<String> {
String joinWithNewLine() {
return '${join('\n')}\n';
}
}