diff --git a/BUILD.gn b/BUILD.gn index f21342d919a10..b2d336b196b69 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -131,6 +131,7 @@ group("flutter") { public_deps += [ "//flutter/shell/testing", "//flutter/tools/const_finder", + "//flutter/tools/engine_tool", "//flutter/tools/font_subset", ] } diff --git a/build/dart/rules.gni b/build/dart/rules.gni index c8242de63ab0a..ecd3ad901930a 100644 --- a/build/dart/rules.gni +++ b/build/dart/rules.gni @@ -390,3 +390,107 @@ template("application_snapshot") { } } } + +# Generates a Dart executable using `dart compile exe` +# +# Arguments +# main_dart (required): +# The Dart entrypoint file. +# +# package_config (required): +# The packages.json file for the app. +# +# output (optional): +# The output executable name. By default, the target name. On Windows, an +# 'exe' is automatically appended. +# +# deps (optional): +# Additional dependencies. Dependencies on the frontend server and +# Flutter's platform.dill are included by default. This rule creates and +# uses a depfile, so listing all Dart sources is not necessary. +template("dart_executable") { + assert(defined(invoker.main_dart), "Must specify 'main_dart'") + + main_dart = invoker.main_dart + + package_config = rebase_path(".dart_tool/package_config.json") + if (defined(invoker.package_config)) { + package_config = rebase_path(invoker.package_config) + } + name = target_name + + extra_deps = [] + if (defined(invoker.deps)) { + extra_deps += invoker.deps + } + + extra_inputs = [ main_dart ] + if (defined(invoker.inputs)) { + extra_inputs += invoker.inputs + } + extra_inputs += [ package_config ] + + ext = "" + if (is_win) { + ext = ".exe" + } + output = "$target_gen_dir/$name$ext" + if (defined(invoker.output)) { + output = invoker.output + } + + exe_vm_args = [ "--deterministic" ] + if (defined(invoker.vm_args)) { + exe_vm_args += invoker.vm_args + } + + abs_output = rebase_path(output) + exe_vm_args += [ + "compile", + "exe", + "--packages=$package_config", + "--output=$abs_output", + ] + + if (flutter_prebuilt_dart_sdk) { + action(target_name) { + forward_variables_from(invoker, + [ + "testonly", + "visibility", + ]) + deps = extra_deps + script = "//build/gn_run_binary.py" + inputs = extra_inputs + outputs = [ output ] + pool = "//flutter/build/dart:dart_pool" + + dart = rebase_path("$host_prebuilt_dart_sdk/bin/dart$ext", root_build_dir) + + args = [ dart ] + args += exe_vm_args + args += [ rebase_path(main_dart) ] + metadata = { + action_type = [ "dart_executable" ] + } + } + } else { + dart_action(target_name) { + forward_variables_from(invoker, + [ + "testonly", + "visibility", + ]) + script = main_dart + pool = "//flutter/build/dart:dart_pool" + deps = extra_deps + inputs = extra_inputs + outputs = [ output ] + vm_args = exe_vm_args + args = [] + metadata = { + action_type = [ "dart_executable" ] + } + } + } +} diff --git a/tools/engine_tool/BUILD.gn b/tools/engine_tool/BUILD.gn new file mode 100644 index 0000000000000..92998715dec6a --- /dev/null +++ b/tools/engine_tool/BUILD.gn @@ -0,0 +1,88 @@ +# Copyright 2013 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("//flutter/build/dart/rules.gni") + +dart_executable("engine_tool") { + main_dart = "bin/et.dart" + output = "$root_out_dir/et" +} + +group("tests") { + testonly = true + public_deps = [ + ":build_command_test", + ":entry_point_test", + ":fetch_command_test", + ":format_command_test", + ":gn_utils_test", + ":lint_command_test", + ":logger_test", + ":proc_utils_test", + ":query_command_test", + ":run_command_test", + ":test_command_test", + ":worker_pool_test", + ] +} + +dart_executable("build_command_test") { + testonly = true + main_dart = "test/build_command_test.dart" +} + +dart_executable("entry_point_test") { + testonly = true + main_dart = "test/entry_point_test.dart" +} + +dart_executable("fetch_command_test") { + testonly = true + main_dart = "test/fetch_command_test.dart" +} + +dart_executable("format_command_test") { + testonly = true + main_dart = "test/format_command_test.dart" +} + +dart_executable("gn_utils_test") { + testonly = true + main_dart = "test/gn_utils_test.dart" +} + +dart_executable("lint_command_test") { + testonly = true + main_dart = "test/lint_command_test.dart" +} + +dart_executable("logger_test") { + testonly = true + main_dart = "test/logger_test.dart" +} + +dart_executable("proc_utils_test") { + testonly = true + main_dart = "test/proc_utils_test.dart" +} + +dart_executable("query_command_test") { + testonly = true + main_dart = "test/query_command_test.dart" +} + +dart_executable("run_command_test") { + testonly = true + main_dart = "test/run_command_test.dart" +} + +dart_executable("test_command_test") { + testonly = true + main_dart = "test/test_command_test.dart" +} + +dart_executable("worker_pool_test") { + testonly = true + main_dart = "test/worker_pool_test.dart" +} diff --git a/tools/engine_tool/lib/src/gn_utils.dart b/tools/engine_tool/lib/src/gn_utils.dart index dc6887f74afed..bace3e0e94f33 100644 --- a/tools/engine_tool/lib/src/gn_utils.dart +++ b/tools/engine_tool/lib/src/gn_utils.dart @@ -32,8 +32,13 @@ enum BuildTargetType { staticLibrary, } -BuildTargetType? _buildTargetTypeFromString(String type) { +BuildTargetType? _buildTargetTypeFromString(String type, Map? metadata) { switch (type) { + case 'action': + if (_getActionType(metadata) == 'dart_executable') { + return BuildTargetType.executable; + } + return null; case 'executable': return BuildTargetType.executable; case 'shared_library': @@ -46,6 +51,16 @@ BuildTargetType? _buildTargetTypeFromString(String type) { } } +String? _getActionType(Map? metadata) { + if (metadata != null) { + final List? action_type = getListOfString(metadata, 'action_type'); + if (action_type != null && action_type.isNotEmpty) { + return action_type[0]; + } + } + return null; +} + // TODO(johnmccutchan): What should we do about source_sets and other // output-less targets? Also, what about action targets which are kind of // "internal" build steps? For now we are ignoring them. @@ -77,30 +92,9 @@ final class BuildTarget { Future> findTargets( Environment environment, Directory buildDir) async { final Map r = {}; - final List getBuildInfoCommandLine = [ - gnBinPath(environment), - 'desc', - buildDir.path, - '*', - '--format=json', - ]; - - final ProcessRunnerResult result = await environment.processRunner.runProcess( - getBuildInfoCommandLine, - workingDirectory: environment.engine.srcDir, - failOk: true); - - // Handle any process failures. - fatalIfFailed(environment, getBuildInfoCommandLine, result); - - late final Map jsonResult; - try { - jsonResult = jsonDecode(result.stdout) as Map; - } catch (e) { - environment.logger.fatal( - 'gn desc output could not be parsed:\nE=$e\nIN=${result.stdout}\n'); - } + final Map jsonResult = + await _runGnDesc(buildDir.path, '*', environment); for (final MapEntry targetEntry in jsonResult.entries) { final String label = targetEntry.key; if (targetEntry.value == null) { @@ -113,14 +107,15 @@ Future> findTargets( if (typeString == null) { environment.logger.fatal('gn desc is missing target type: $properties'); } - final BuildTargetType? type = _buildTargetTypeFromString(typeString!); + final Map? metadata = getMap(properties, 'metadata'); + final BuildTargetType? type = _buildTargetTypeFromString(typeString!, metadata); if (type == null) { // Target is a type that we don't support. continue; } final bool testOnly = getBool(properties, 'testonly'); final List outputs = - getListOfString(properties, 'outputs') ?? []; + await _runGnOutputs(buildDir.path, label, environment); File? executable; if (type == BuildTargetType.executable) { if (outputs.isEmpty) { @@ -135,6 +130,55 @@ Future> findTargets( return r; } +/// Returns the JSON output of running `gn desc buildDir label`. +Future> _runGnDesc( + String buildDir, String label, Environment environment) async { + final List commandline = [ + gnBinPath(environment), + 'desc', + buildDir, + label, + '--format=json', + ]; + + final ProcessRunnerResult result = await environment.processRunner.runProcess( + commandline, + workingDirectory: environment.engine.srcDir, + failOk: true); + + // Handle any process failures. + fatalIfFailed(environment, commandline, result); + + late final Map jsonResult; + try { + jsonResult = jsonDecode(result.stdout) as Map; + } catch (e) { + environment.logger.fatal( + 'gn desc output could not be parsed:\nE=$e\nIN=${result.stdout}\n'); + } + return jsonResult; +} + +/// Returns the output paths returned by `gn outputs buildDir label`. +Future> _runGnOutputs( + String buildDir, String label, Environment environment) async { + final List commandline = [ + gnBinPath(environment), + 'outputs', + buildDir, + label, + ]; + final ProcessRunnerResult result = await environment.processRunner.runProcess( + commandline, + workingDirectory: environment.engine.srcDir, + failOk: true); + + // Handle any process failures. + fatalIfFailed(environment, commandline, result); + + return result.stdout.split('\n'); +} + /// Process selectors and filter allTargets for matches. /// /// We support: diff --git a/tools/engine_tool/lib/src/json_utils.dart b/tools/engine_tool/lib/src/json_utils.dart index e813512e7cd02..2cacfcef2ac8a 100644 --- a/tools/engine_tool/lib/src/json_utils.dart +++ b/tools/engine_tool/lib/src/json_utils.dart @@ -117,3 +117,11 @@ List? getListOfString(Map map, String field) { } return (map[field]! as List).cast(); } + +/// Returns the value in map[field] iff it is a Map. null otherwise. +Map? getMap(Map map, String field) { + if (map[field] case final Map value) { + return value; + } + return null; +} diff --git a/tools/engine_tool/test/build_command_test.dart b/tools/engine_tool/test/build_command_test.dart index fe1c65f8e1cf5..81a77fdadb276 100644 --- a/tools/engine_tool/test/build_command_test.dart +++ b/tools/engine_tool/test/build_command_test.dart @@ -360,11 +360,11 @@ void main() { '//flutter/fml:fml_arc_unittests', ]); expect(result, equals(0)); - expect(testEnv.processHistory.length, greaterThanOrEqualTo(2)); - expect(testEnv.processHistory[3].command[0], contains('ninja')); - expect(testEnv.processHistory[3].command[2], endsWith('/host_debug')); + expect(testEnv.processHistory.length, greaterThan(7)); + expect(testEnv.processHistory[7].command[0], contains('ninja')); + expect(testEnv.processHistory[7].command[2], endsWith('/host_debug')); expect( - testEnv.processHistory[3].command[5], + testEnv.processHistory[7].command[5], equals('flutter/fml:fml_arc_unittests'), ); } finally { @@ -389,21 +389,25 @@ void main() { '//flutter/...', ]); expect(result, equals(0)); - expect(testEnv.processHistory.length, greaterThanOrEqualTo(2)); - expect(testEnv.processHistory[3].command[0], contains('ninja')); - expect(testEnv.processHistory[3].command[2], endsWith('/host_debug')); + expect(testEnv.processHistory.length, greaterThan(7)); + expect(testEnv.processHistory[7].command[0], contains('ninja')); + expect(testEnv.processHistory[7].command[2], endsWith('/host_debug')); expect( - testEnv.processHistory[3].command[5], + testEnv.processHistory[7].command[5], equals('flutter/display_list:display_list_unittests'), ); expect( - testEnv.processHistory[3].command[6], + testEnv.processHistory[7].command[6], equals('flutter/flow:flow_unittests'), ); expect( - testEnv.processHistory[3].command[7], + testEnv.processHistory[7].command[7], equals('flutter/fml:fml_arc_unittests'), ); + expect( + testEnv.processHistory[7].command[8], + equals('flutter/tools/engine_tool:build_command_test'), + ); } finally { testEnv.cleanup(); } diff --git a/tools/engine_tool/test/fixtures.dart b/tools/engine_tool/test/fixtures.dart index 53e088d3b213e..6f1f2eab9e7bf 100644 --- a/tools/engine_tool/test/fixtures.dart +++ b/tools/engine_tool/test/fixtures.dart @@ -316,6 +316,21 @@ String gnDescOutput() => ''' "toolchain": "//build/toolchain/mac:clang_x64", "type": "executable", "visibility": [ "*" ] - } + }, + "//flutter/tools/engine_tool:build_command_test": { + "args": [ "../../flutter/prebuilts/macos-x64/dart-sdk/bin/dart", "--deterministic", "compile", "exe", "--packages=/Users/chris/Developer/flutter/engine/src/flutter/tools/engine_tool/.dart_tool/package_config.json", "--output=/Users/chris/Developer/flutter/engine/src/out/host_debug/gen/flutter/tools/engine_tool/build_command_test", "/Users/chris/Developer/flutter/engine/src/flutter/tools/engine_tool/test/build_command_test.dart" ], + "deps": [ ], + "inputs": [ "//flutter/tools/engine_tool/test/build_command_test.dart", "//flutter/tools/engine_tool/.dart_tool/package_config.json" ], + "metadata": { + "action_type": [ "dart_executable" ] + }, + "outputs": [ "//out/host_debug/gen/flutter/tools/engine_tool/build_command_test" ], + "public": "*", + "script": "//build/gn_run_binary.py", + "testonly": true, + "toolchain": "//build/toolchain/mac:clang_x64", + "type": "action", + "visibility": [ "*" ] + } } '''; diff --git a/tools/engine_tool/test/gn_utils_test.dart b/tools/engine_tool/test/gn_utils_test.dart index 462c90703bb17..5d021ea35d275 100644 --- a/tools/engine_tool/test/gn_utils_test.dart +++ b/tools/engine_tool/test/gn_utils_test.dart @@ -39,6 +39,18 @@ void main() { final List cannedProcesses = [ CannedProcess((List command) => command.contains('desc'), stdout: fixtures.gnDescOutput()), + CannedProcess((List command) => + command.contains('outputs') && command.contains('//flutter/display_list:display_list_unittests'), + stdout: 'display_list_unittests'), + CannedProcess((List command) => + command.contains('outputs') && command.contains('//flutter/flow:flow_unittests'), + stdout: 'flow_unittests'), + CannedProcess((List command) => + command.contains('outputs') && command.contains('//flutter/fml:fml_arc_unittests'), + stdout: 'fml_arc_unittests'), + CannedProcess((List command) => + command.contains('outputs') && command.contains('//flutter/tools/engine_tool:build_command_test'), + stdout: 'build_command_test'), ]; test('find test targets', () async { @@ -47,7 +59,7 @@ void main() { final Environment env = testEnvironment.environment; final Map testTargets = await findTargets(env, engine.outDir); - expect(testTargets.length, equals(3)); + expect(testTargets.length, equals(4)); expect(testTargets['//flutter/display_list:display_list_unittests'], notEquals(null)); expect( @@ -55,6 +67,11 @@ void main() { .executable! .path, endsWith('display_list_unittests')); + expect( + testTargets['//flutter/tools/engine_tool:build_command_test']! + .executable! + .path, + endsWith('build_command_test')); }); test('process queue failure', () async { @@ -63,7 +80,7 @@ void main() { final Environment env = testEnvironment.environment; final Map testTargets = await findTargets(env, engine.outDir); - expect(selectTargets(['//...'], testTargets).length, equals(3)); + expect(selectTargets(['//...'], testTargets).length, equals(4)); expect( selectTargets(['//flutter/display_list'], testTargets).length, equals(0)); diff --git a/tools/engine_tool/test/query_command_test.dart b/tools/engine_tool/test/query_command_test.dart index f9da60c6c8b0c..e443e2845367b 100644 --- a/tools/engine_tool/test/query_command_test.dart +++ b/tools/engine_tool/test/query_command_test.dart @@ -162,7 +162,7 @@ void main() { expect(result, equals(0)); expect( env.logger.testLogs.length, - equals(4), + equals(5), ); expect(env.logger.testLogs[1].message, startsWith('//flutter/display_list:display_list_unittests')); diff --git a/tools/engine_tool/test/test_command_test.dart b/tools/engine_tool/test/test_command_test.dart index ecc86d3f8b28d..2345cd978e018 100644 --- a/tools/engine_tool/test/test_command_test.dart +++ b/tools/engine_tool/test/test_command_test.dart @@ -42,9 +42,13 @@ void main() { final List cannedProcesses = [ CannedProcess((List command) => command.contains('desc'), stdout: fixtures.gnDescOutput()), + CannedProcess((List command) => command.contains('outputs') && command.contains('//flutter/display_list:display_list_unittests'), + stdout: 'display_list_unittests'), + CannedProcess((List command) => command.contains('outputs') && command.contains('//flutter/tools/engine_tool:build_command_test'), + stdout: 'build_command_test'), ]; - test('test command executes test', () async { + test('test command executes executable test', () async { final TestEnvironment testEnvironment = TestEnvironment.withTestEngine( cannedProcesses: cannedProcesses, ); @@ -68,6 +72,30 @@ void main() { } }); + test('test command executes dart_executable test', () async { + final TestEnvironment testEnvironment = TestEnvironment.withTestEngine( + cannedProcesses: cannedProcesses, + ); + try { + final Environment env = testEnvironment.environment; + final ToolCommandRunner runner = ToolCommandRunner( + environment: env, + configs: configs, + ); + final int result = await runner.run([ + 'test', + '//flutter/tools/engine_tool:build_command_test', + ]); + expect(result, equals(0)); + expect(testEnvironment.processHistory.length, greaterThan(3)); + final int offset = testEnvironment.processHistory.length - 1; + expect(testEnvironment.processHistory[offset].command[0], + endsWith('build_command_test')); + } finally { + testEnvironment.cleanup(); + } + }); + test('test command skips non-testonly executables', () async { final TestEnvironment testEnvironment = TestEnvironment.withTestEngine( cannedProcesses: cannedProcesses, @@ -83,7 +111,7 @@ void main() { '//third_party/protobuf:protoc', ]); expect(result, equals(1)); - expect(testEnvironment.processHistory.length, lessThan(3)); + expect(testEnvironment.processHistory.length, lessThan(7)); expect(testEnvironment.processHistory.where((ExecutedProcess process) { return process.command[0].contains('protoc'); }), isEmpty);