22// Use of this source code is governed by a BSD-style license that can be
33// found in the LICENSE file.
44
5- import 'dart:io' as io show Directory, exitCode, stderr ;
5+ import 'dart:io' as io;
66
7+ import 'package:args/args.dart' ;
78import 'package:engine_build_configs/engine_build_configs.dart' ;
9+ import 'package:engine_build_configs/src/ci_yaml.dart' ;
810import 'package:engine_repo_tools/engine_repo_tools.dart' ;
11+ import 'package:meta/meta.dart' ;
912import 'package:path/path.dart' as p;
1013import 'package:platform/platform.dart' ;
14+ import 'package:source_span/source_span.dart' ;
15+ import 'package:yaml/yaml.dart' as y;
1116
1217// Usage:
13- // $ dart bin/check.dart [/path/to/engine/src]
18+ // $ dart bin/check.dart
19+ //
20+ // Or, for more options:
21+ // $ dart bin/check.dart --help
22+
23+ final _argParser =
24+ ArgParser ()
25+ ..addFlag ('verbose' , abbr: 'v' , help: 'Enable noisier diagnostic output' , negatable: false )
26+ ..addFlag ('help' , abbr: 'h' , help: 'Output usage information.' , negatable: false )
27+ ..addOption (
28+ 'engine-src-path' ,
29+ valueHelp: '/path/to/engine/src' ,
30+ defaultsTo: Engine .tryFindWithin ()? .srcDir.path,
31+ );
1432
1533void main (List <String > args) {
16- final String ? engineSrcPath;
17- if (args.isNotEmpty) {
18- engineSrcPath = args[0 ];
19- } else {
20- engineSrcPath = null ;
21- }
34+ run (
35+ args,
36+ stderr: io.stderr,
37+ stdout: io.stdout,
38+ platform: const LocalPlatform (),
39+ setExitCode: (exitCode) {
40+ io.exitCode = exitCode;
41+ },
42+ );
43+ }
2244
23- // Find the engine repo.
24- final Engine engine;
25- try {
26- engine = Engine .findWithin (engineSrcPath);
27- } catch (e) {
28- io.stderr.writeln (e);
29- io.exitCode = 1 ;
45+ @visibleForTesting
46+ void run (
47+ Iterable <String > args, {
48+ required Platform platform,
49+ required StringSink stderr,
50+ required StringSink stdout,
51+ required void Function (int ) setExitCode,
52+ }) {
53+ y.yamlWarningCallback = (String message, [SourceSpan ? span]) {};
54+
55+ final argResults = _argParser.parse (args);
56+ if (argResults.flag ('help' )) {
57+ stdout.writeln (_argParser.usage);
3058 return ;
3159 }
3260
61+ final verbose = argResults.flag ('verbose' );
62+ void debugPrint (String output) {
63+ if (! verbose) {
64+ return ;
65+ }
66+ stderr.writeln (output);
67+ }
68+
69+ void indentedPrint (Iterable <String > errors) {
70+ for (final error in errors) {
71+ stderr.writeln (' $error ' );
72+ }
73+ }
74+
75+ final supportsEmojis = ! platform.isWindows || platform.environment.containsKey ('WT_SESSION' );
76+ final symbolSuccess = supportsEmojis ? '✅' : '✓' ;
77+ final symbolFailure = supportsEmojis ? '❌' : 'X' ;
78+ void statusPrint (String describe, {required bool success}) {
79+ stderr.writeln ('${success ? symbolSuccess : symbolFailure } $describe ' );
80+ if (! success) {
81+ setExitCode (1 );
82+ }
83+ }
84+
85+ final engine = Engine .fromSrcPath (argResults.option ('engine-src-path' )! );
86+ debugPrint ('Initializing from ${p .relative (engine .srcDir .path )}' );
87+
3388 // Find and parse the engine build configs.
3489 final io.Directory buildConfigsDir = io.Directory (
3590 p.join (engine.flutterDir.path, 'ci' , 'builders' ),
@@ -39,36 +94,112 @@ void main(List<String> args) {
3994 // Treat it as an error if no build configs were found. The caller likely
4095 // expected to find some.
4196 final Map <String , BuilderConfig > configs = loader.configs;
97+
98+ // We can't make further progress if we didn't find any configurations.
99+ statusPrint (
100+ 'Loaded build configs under ${p .relative (buildConfigsDir .path )}' ,
101+ success: configs.isNotEmpty && loader.errors.isEmpty,
102+ );
42103 if (configs.isEmpty) {
43- io.stderr.writeln ('Error: No build configs found under ${buildConfigsDir .path }' );
44- io.exitCode = 1 ;
45104 return ;
46105 }
47- if (loader.errors.isNotEmpty) {
48- loader.errors.forEach (io.stderr.writeln);
49- io.exitCode = 1 ;
106+ indentedPrint (loader.errors);
107+
108+ // Find and parse the .ci.yaml configuration (for the engine).
109+ final CiConfig ? ciConfig;
110+ {
111+ final String ciYamlPath = p.join (engine.flutterDir.path, '.ci.yaml' );
112+ final String realCiYaml = io.File (ciYamlPath).readAsStringSync ();
113+ final y.YamlNode yamlNode = y.loadYamlNode (realCiYaml, sourceUrl: Uri .file (ciYamlPath));
114+ final loadedConfig = CiConfig .fromYaml (yamlNode);
115+
116+ statusPrint ('.ci.yaml at ${p .relative (ciYamlPath )} is valid' , success: loadedConfig.valid);
117+ if (! loadedConfig.valid) {
118+ indentedPrint ([loadedConfig.error! ]);
119+ ciConfig = null ;
120+ } else {
121+ ciConfig = loadedConfig;
122+ }
50123 }
51124
52125 // Check the parsed build configs for validity.
53126 final List <String > invalidErrors = checkForInvalidConfigs (configs);
54- if (invalidErrors.isNotEmpty) {
55- invalidErrors.forEach (io.stderr.writeln);
56- io.exitCode = 1 ;
57- }
127+ statusPrint ('All configuration files are valid' , success: invalidErrors.isEmpty);
128+ indentedPrint (invalidErrors);
58129
59130 // We require all builds within a builder config to be uniquely named.
60131 final List <String > duplicateErrors = checkForDuplicateConfigs (configs);
61- if (duplicateErrors.isNotEmpty) {
62- duplicateErrors.forEach (io.stderr.writeln);
63- io.exitCode = 1 ;
64- }
132+ statusPrint ('All builds within a builder are uniquely named' , success: duplicateErrors.isEmpty);
133+ indentedPrint (duplicateErrors);
65134
66135 // We require all builds to be named in a way that is understood by et.
67136 final List <String > buildNameErrors = checkForInvalidBuildNames (configs);
68- if (buildNameErrors.isNotEmpty) {
69- buildNameErrors.forEach (io.stderr.writeln);
70- io.exitCode = 1 ;
137+ statusPrint ('All build names must have a conforming prefix' , success: buildNameErrors.isEmpty);
138+ indentedPrint (buildNameErrors);
139+
140+ // If we have a successfully parsed .ci.yaml, perform additional checks.
141+ if (ciConfig == null ) {
142+ return ;
143+ }
144+
145+ // We require that targets that have `properties: release_build: "true"`:
146+ // (1) Each sub-build produces artifacts (`archives: [...]`)
147+ // (2) Each sub-build does not have `tests: [ ... ]`
148+ final buildConventionErrors = < String > [];
149+ for (final MapEntry (key: _, value: target) in ciConfig.ciTargets.entries) {
150+ final config = loader.configs[target.properties.configName];
151+ if (target.properties.configName == null ) {
152+ // * builder_cache targets do not have configuration files.
153+ debugPrint (' Skipping ${target .name }: No configuration file found' );
154+ continue ;
155+ }
156+
157+ // This would fail above during the general loading.
158+ if (config == null ) {
159+ throw StateError ('Unreachable' );
160+ }
161+
162+ final configConventionErrors = < String > [];
163+ if (target.properties.isReleaseBuilder) {
164+ // If there is a global generators step, assume artifacts are uploaded from the generators.
165+ if (config.generators.isNotEmpty) {
166+ debugPrint (' Skipping ${target .name }: Has "generators": [ ... ] which could do anything' );
167+ continue ;
168+ }
169+ // Check each build: it must have "archives: [ ... ]" and NOT "tests: [ ... ]"
170+ for (final build in config.builds) {
171+ if (build.archives.isEmpty) {
172+ configConventionErrors.add ('${build .name }: Does not have "archives: [ ... ]"' );
173+ }
174+ if (build.archives.any ((e) => e.includePaths.isEmpty)) {
175+ configConventionErrors.add (
176+ '${build .name }: Has an archive with an empty "include_paths": []' ,
177+ );
178+ }
179+ if (build.tests.isNotEmpty) {
180+ // TODO(matanlurey): https://github.com/flutter/flutter/issues/161990.
181+ if (target.properties.configName == 'windows_host_engine' &&
182+ build.name == r'ci\host_debug' ) {
183+ debugPrint (' Skipping: ${build .name }: Allow-listed during migration' );
184+ } else {
185+ configConventionErrors.add ('${build .name }: Includes "tests: [ ... ]"' );
186+ }
187+ }
188+ }
189+ }
190+
191+ if (configConventionErrors.isNotEmpty) {
192+ buildConventionErrors.add (
193+ '${p .basename (config .path )} (${target .name }, release_build = ${target .properties .isReleaseBuilder }):' ,
194+ );
195+ buildConventionErrors.addAll (configConventionErrors.map ((e) => ' $e ' ));
196+ }
71197 }
198+ statusPrint (
199+ 'All builder files conform to release_build standards' ,
200+ success: buildConventionErrors.isEmpty,
201+ );
202+ indentedPrint (buildConventionErrors);
72203}
73204
74205// This check ensures that all the json files were deserialized without errors.
0 commit comments