2
2
// Use of this source code is governed by a BSD-style license that can be
3
3
// found in the LICENSE file.
4
4
5
- import 'dart:io' as io show Directory, exitCode, stderr ;
5
+ import 'dart:io' as io;
6
6
7
+ import 'package:args/args.dart' ;
7
8
import 'package:engine_build_configs/engine_build_configs.dart' ;
9
+ import 'package:engine_build_configs/src/ci_yaml.dart' ;
8
10
import 'package:engine_repo_tools/engine_repo_tools.dart' ;
11
+ import 'package:meta/meta.dart' ;
9
12
import 'package:path/path.dart' as p;
10
13
import 'package:platform/platform.dart' ;
14
+ import 'package:source_span/source_span.dart' ;
15
+ import 'package:yaml/yaml.dart' as y;
11
16
12
17
// 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
+ );
14
32
15
33
void 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
+ }
22
44
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);
30
58
return ;
31
59
}
32
60
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
+
33
88
// Find and parse the engine build configs.
34
89
final io.Directory buildConfigsDir = io.Directory (
35
90
p.join (engine.flutterDir.path, 'ci' , 'builders' ),
@@ -39,36 +94,112 @@ void main(List<String> args) {
39
94
// Treat it as an error if no build configs were found. The caller likely
40
95
// expected to find some.
41
96
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
+ );
42
103
if (configs.isEmpty) {
43
- io.stderr.writeln ('Error: No build configs found under ${buildConfigsDir .path }' );
44
- io.exitCode = 1 ;
45
104
return ;
46
105
}
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
+ }
50
123
}
51
124
52
125
// Check the parsed build configs for validity.
53
126
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);
58
129
59
130
// We require all builds within a builder config to be uniquely named.
60
131
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);
65
134
66
135
// We require all builds to be named in a way that is understood by et.
67
136
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
+ }
71
197
}
198
+ statusPrint (
199
+ 'All builder files conform to release_build standards' ,
200
+ success: buildConventionErrors.isEmpty,
201
+ );
202
+ indentedPrint (buildConventionErrors);
72
203
}
73
204
74
205
// This check ensures that all the json files were deserialized without errors.
0 commit comments