flutter_flutter/dev/bots/run_command.dart
Gray Mackall 91cc68f695
[Reland] Unmodified android sdk bundle (#179920)
Relands https://github.com/flutter/flutter/pull/179647

There were only two failing tests (there were also two bringup failures,
the `mac_arm64_mokey` versions of these)

https://ci.chromium.org/ui/p/flutter/builders/prod/Mac_mokey%20run_release_test/3866/overview

https://ci.chromium.org/ui/p/flutter/builders/prod/Mac_mokey%20run_debug_test_android/5454/overview

They failed with the same logs
```
Task result:
{
  "success": false,
  "reason": "Task failed: flutter run  had unexpected output on standard error."
}
```

The stderr in question:
```
[2025-12-15 14:06:56.311794] [STDOUT] run:stderr: - integration_test requires Android NDK 28.2.13676358
[2025-12-15 14:06:56.311989] [STDOUT] run:stderr: Fix this issue by using the highest Android NDK version (they are backward compatible).
[2025-12-15 14:06:56.312019] [STDOUT] run:stderr: Add the following to /opt/s/w/ir/x/w/rc/tmpr6kj09d_/flutter sdk/dev/integration_tests/ui/android/app/build.gradle:
[2025-12-15 14:06:56.312074] [STDOUT] run:stderr: 
[2025-12-15 14:06:56.312092] [STDOUT] run:stderr:     android {
[2025-12-15 14:06:56.312144] [STDOUT] run:stderr:         ndkVersion = "28.2.13676358"
[2025-12-15 14:06:56.312162] [STDOUT] run:stderr:         ...
[2025-12-15 14:06:56.312205] [STDOUT] run:stderr:     }
```

We can just set the ndk version to `flutter.ndkVersion`, which is this
value. It's also what is set in the templates, so it isn't a hack that
doesn't represent a real flutter app. Verified the tests pass now with
```
../../bin/cache/dart-sdk/bin/dart bin/test_runner.dart test -t run_release_test
../../bin/cache/dart-sdk/bin/dart bin/test_runner.dart test -t run_debug_test_android
```

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Gray Mackall <mackall@google.com>
2025-12-16 19:34:38 +00:00

344 lines
12 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:core' hide print;
import 'dart:io' as io;
import 'package:path/path.dart' as path;
import 'utils.dart';
/// Runs the `executable` and returns standard output as a stream of lines.
///
/// The returned stream reaches its end immediately after the command exits.
///
/// If `expectNonZeroExit` is false and the process exits with a non-zero exit
/// code fails the test immediately by exiting the test process with exit code
/// 1.
Stream<String> runAndGetStdout(
String executable,
List<String> arguments, {
String? workingDirectory,
Map<String, String>? environment,
bool expectNonZeroExit = false,
}) async* {
final output = StreamController<String>();
final Future<CommandResult?> command = runCommand(
executable,
arguments,
workingDirectory: workingDirectory,
environment: environment,
expectNonZeroExit: expectNonZeroExit,
// Capture the output so it's not printed to the console by default.
outputMode: OutputMode.capture,
outputListener: (String line, io.Process process) {
output.add(line);
},
);
// Close the stream controller after the command is complete. Otherwise,
// the yield* will never finish.
command.whenComplete(output.close);
yield* output.stream;
}
/// Represents a running process launched using [startCommand].
class Command {
Command._(this.process, this._time, this._savedStdout, this._savedStderr);
/// The raw process that was launched for this command.
final io.Process process;
final Stopwatch _time;
final Future<String> _savedStdout;
final Future<String> _savedStderr;
}
/// The result of running a command using [startCommand] and [runCommand];
class CommandResult {
CommandResult._(this.exitCode, this.elapsedTime, this.flattenedStdout, this.flattenedStderr);
/// The exit code of the process.
final int exitCode;
/// The amount of time it took the process to complete.
final Duration elapsedTime;
/// Standard output decoded as a string using UTF8 decoder.
final String? flattenedStdout;
/// Standard error output decoded as a string using UTF8 decoder.
final String? flattenedStderr;
}
/// Starts the `executable` and returns a command object representing the
/// running process.
///
/// `outputListener` is called for every line of standard output from the
/// process, and is given the [Process] object. This can be used to interrupt
/// an indefinitely running process, for example, by waiting until the process
/// emits certain output.
///
/// `outputMode` controls where the standard output from the command process
/// goes. See [OutputMode].
Future<Command> startCommand(
String executable,
List<String> arguments, {
String? workingDirectory,
Map<String, String>? environment,
OutputMode outputMode = OutputMode.print,
bool Function(String)? removeLine,
void Function(String, io.Process)? outputListener,
}) async {
final String relativeWorkingDir = path.relative(workingDirectory ?? io.Directory.current.path);
final commandDescription =
'${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
print('RUNNING: cd $cyan$relativeWorkingDir$reset; $green$commandDescription$reset');
final time = Stopwatch()..start();
print('workingDirectory: $workingDirectory, executable: $executable, arguments: $arguments');
// Override ANDROID_NDK_PATH with a valid discovered path or empty string to clear a potentially bad value.
// See also dev/devicelab/lib/framework/utils.dart.
final finalEnvironment = <String, String>{
...?environment,
'ANDROID_NDK_PATH': ?_discoverBestNdkPath(),
};
final io.Process process = await io.Process.start(
executable,
arguments,
workingDirectory: workingDirectory,
environment: finalEnvironment,
);
return Command._(
process,
time,
process.stdout
.transform<String>(const Utf8Decoder())
.transform(const LineSplitter())
.where((String line) => removeLine == null || !removeLine(line))
.map<String>((String line) {
final formattedLine = '$line\n';
if (outputListener != null) {
outputListener(formattedLine, process);
}
switch (outputMode) {
case OutputMode.print:
print(line);
case OutputMode.capture:
break;
}
return line;
})
.join('\n'),
process.stderr
.transform<String>(const Utf8Decoder())
.transform(const LineSplitter())
.map<String>((String line) {
switch (outputMode) {
case OutputMode.print:
print(line);
case OutputMode.capture:
break;
}
return line;
})
.join('\n'),
);
}
String? _bestNdkPath;
String? _discoverBestNdkPath() {
if (_bestNdkPath != null) {
return _bestNdkPath;
}
// If we found a valid NDK, return it.
// Otherwise return empty string to clear any bad inherited value.
final Map<String, String> env = io.Platform.environment;
String? androidHome = env['ANDROID_HOME'] ?? env['ANDROID_SDK_ROOT'];
if (androidHome == null) {
// Try to find it in common default locations.
if (io.Platform.isMacOS) {
final String? home = env['HOME'];
if (home != null) {
androidHome = path.join(home, 'Library', 'Android', 'sdk');
}
} else if (io.Platform.isLinux) {
final String? home = env['HOME'];
if (home != null) {
androidHome = path.join(home, 'Android', 'Sdk');
}
} else if (io.Platform.isWindows) {
final String? homeDrive = env['HOMEDRIVE'];
final String? homePath = env['HOMEPATH'];
if (homeDrive != null && homePath != null) {
androidHome = path.join(homeDrive + homePath, 'AppData', 'Local', 'Android', 'sdk');
}
}
}
if (androidHome == null) {
return _bestNdkPath = null;
}
final ndkDir = io.Directory(path.join(androidHome, 'ndk'));
if (!ndkDir.existsSync()) {
return _bestNdkPath = null;
}
final versions = <String>[];
for (final io.FileSystemEntity entity in ndkDir.listSync()) {
if (entity is io.Directory) {
final String name = path.basename(entity.path);
// flutter_tools ignores non-SemVer directories. NDK versions should start with a number.
if (int.tryParse(name.split('.').first) != null) {
versions.add(name);
}
}
}
if (versions.isEmpty) {
return _bestNdkPath = null;
}
// Sort numerically/lexicographically to find the "highest" version.
// We assume versions are like "21.4.7075529" or "23.1.7779620".
// A simple sort works for major versions if they have the same number of digits,
// but to be safe we parse the major version.
versions.sort((String a, String b) {
// Try to parse major version.
final int? aMajor = int.tryParse(a.split('.').first);
final int? bMajor = int.tryParse(b.split('.').first);
if (aMajor != null && bMajor != null) {
if (aMajor != bMajor) {
return aMajor.compareTo(bMajor);
}
}
// Fallback to string comparison.
return a.compareTo(b);
});
return _bestNdkPath = path.join(ndkDir.path, versions.last);
}
/// Runs the `executable` and waits until the process exits.
///
/// If the process exits with a non-zero exit code and `expectNonZeroExit` is
/// false, calls foundError (which does not terminate execution!).
///
/// `outputListener` is called for every line of standard output from the
/// process, and is given the [Process] object. This can be used to interrupt
/// an indefinitely running process, for example, by waiting until the process
/// emits certain output.
///
/// Returns the result of the finished process.
///
/// `outputMode` controls where the standard output from the command process
/// goes. See [OutputMode].
Future<CommandResult> runCommand(
String executable,
List<String> arguments, {
String? workingDirectory,
Map<String, String>? environment,
bool expectNonZeroExit = false,
int? expectedExitCode,
String? failureMessage,
OutputMode outputMode = OutputMode.print,
bool Function(String)? removeLine,
void Function(String, io.Process)? outputListener,
}) async {
final commandDescription =
'${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
final String relativeWorkingDir = workingDirectory ?? path.relative(io.Directory.current.path);
if (dryRun) {
printProgress(_prettyPrintRunCommand(executable, arguments, workingDirectory));
return CommandResult._(
0,
Duration.zero,
'$executable ${arguments.join(' ')}',
'Simulated execution due to --dry-run',
);
}
final Command command = await startCommand(
executable,
arguments,
workingDirectory: workingDirectory,
environment: environment,
outputMode: outputMode,
removeLine: removeLine,
outputListener: outputListener,
);
final result = CommandResult._(
await command.process.exitCode,
command._time.elapsed,
await command._savedStdout,
await command._savedStderr,
);
if ((result.exitCode == 0) == expectNonZeroExit ||
(expectedExitCode != null && result.exitCode != expectedExitCode)) {
// Print the output when we get unexpected results (unless output was
// printed already).
switch (outputMode) {
case OutputMode.print:
break;
case OutputMode.capture:
print(result.flattenedStdout);
print(result.flattenedStderr);
}
final allOutput = '${result.flattenedStdout}\n${result.flattenedStderr}';
foundError(<String>[
?failureMessage,
'${bold}Command: $green$commandDescription$reset',
if (failureMessage == null)
'$bold${red}Command exited with exit code ${result.exitCode} but expected ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'} exit code.$reset',
'${bold}Working directory: $cyan${path.absolute(relativeWorkingDir)}$reset',
if (allOutput.isNotEmpty && allOutput.length < 512)
'${bold}stdout and stderr output:\n$allOutput',
]);
} else {
print(
'ELAPSED TIME: ${prettyPrintDuration(result.elapsedTime)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset',
);
}
return result;
}
final String _flutterRoot = path.dirname(
path.dirname(path.dirname(path.fromUri(io.Platform.script))),
);
String _prettyPrintRunCommand(String executable, List<String> arguments, String? workingDirectory) {
final output = StringBuffer();
// Print CWD relative to the root.
output.write('|> ');
output.write(path.relative(executable, from: _flutterRoot));
if (workingDirectory != null) {
output.write(' (${path.relative(workingDirectory, from: _flutterRoot)})');
}
output.writeln(': ');
output.writeAll(arguments.map((String a) => ' $a'), '\n');
return output.toString();
}
/// Specifies what to do with the command output from [runCommand] and [startCommand].
enum OutputMode {
/// Forwards standard output and standard error streams to the test process'
/// standard output stream (i.e. stderr is redirected to stdout).
///
/// Use this mode if all you want is print the output of the command to the
/// console. The output is no longer available after the process exits.
print,
/// Saves standard output and standard error streams in memory.
///
/// Captured output can be retrieved from the [CommandResult] object.
///
/// Use this mode in tests that need to inspect the output of a command, or
/// when the output should not be printed to console.
capture,
}