1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
// 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 '../artifacts.dart';
import '../base/file_system.dart';
import '../build_info.dart';
import 'build_system.dart';
import 'exceptions.dart';
/// A set of source files.
abstract class ResolvedFiles {
/// Whether any of the sources we evaluated contained a missing depfile.
///
/// If so, the build system needs to rerun the visitor after executing the
/// build to ensure all hashes are up to date.
bool get containsNewDepfile;
/// The resolved source files.
List<File> get sources;
}
/// Collects sources for a [Target] into a single list of [FileSystemEntities].
class SourceVisitor implements ResolvedFiles {
/// Create a new [SourceVisitor] from an [Environment].
SourceVisitor(this.environment, [ this.inputs = true ]);
/// The current environment.
final Environment environment;
/// Whether we are visiting inputs or outputs.
///
/// Defaults to `true`.
final bool inputs;
@override
final List<File> sources = <File>[];
@override
bool get containsNewDepfile => _containsNewDepfile;
bool _containsNewDepfile = false;
/// Visit a depfile which contains both input and output files.
///
/// If the file is missing, this visitor is marked as [containsNewDepfile].
/// This is used by the [Node] class to tell the [BuildSystem] to
/// defer hash computation until after executing the target.
// depfile logic adopted from https://github.com/flutter/flutter/blob/7065e4330624a5a216c8ffbace0a462617dc1bf5/dev/devicelab/lib/framework/apk_utils.dart#L390
void visitDepfile(String name) {
final File depfile = environment.buildDir.childFile(name);
if (!depfile.existsSync()) {
_containsNewDepfile = true;
return;
}
final String contents = depfile.readAsStringSync();
final List<String> colonSeparated = contents.split(': ');
if (colonSeparated.length != 2) {
environment.logger.printError('Invalid depfile: ${depfile.path}');
return;
}
if (inputs) {
sources.addAll(_processList(colonSeparated[1].trim()));
} else {
sources.addAll(_processList(colonSeparated[0].trim()));
}
}
final RegExp _separatorExpr = RegExp(r'([^\\]) ');
final RegExp _escapeExpr = RegExp(r'\\(.)');
Iterable<File> _processList(String rawText) {
return rawText
// Put every file on right-hand side on the separate line
.replaceAllMapped(_separatorExpr, (Match match) => '${match.group(1)}\n')
.split('\n')
// Expand escape sequences, so that '\ ', for example,ß becomes ' '
.map<String>((String path) => path.replaceAllMapped(_escapeExpr, (Match match) => match.group(1)!).trim())
.where((String path) => path.isNotEmpty)
.toSet()
.map(environment.fileSystem.file);
}
/// Visit a [Source] which contains a file URL.
///
/// The URL may include constants defined in an [Environment]. If
/// [optional] is true, the file is not required to exist. In this case, it
/// is never resolved as an input.
void visitPattern(String pattern, bool optional) {
// perform substitution of the environmental values and then
// of the local values.
final List<String> segments = <String>[];
final List<String> rawParts = pattern.split('/');
final bool hasWildcard = rawParts.last.contains('*');
String? wildcardFile;
if (hasWildcard) {
wildcardFile = rawParts.removeLast();
}
// If the pattern does not start with an env variable, then we have nothing
// to resolve it to, error out.
switch (rawParts.first) {
case Environment.kProjectDirectory:
segments.addAll(
environment.fileSystem.path.split(environment.projectDir.resolveSymbolicLinksSync()));
break;
case Environment.kBuildDirectory:
segments.addAll(environment.fileSystem.path.split(
environment.buildDir.resolveSymbolicLinksSync()));
break;
case Environment.kCacheDirectory:
segments.addAll(
environment.fileSystem.path.split(environment.cacheDir.resolveSymbolicLinksSync()));
break;
case Environment.kFlutterRootDirectory:
// flutter root will not contain a symbolic link.
segments.addAll(
environment.fileSystem.path.split(environment.flutterRootDir.absolute.path));
break;
case Environment.kOutputDirectory:
segments.addAll(
environment.fileSystem.path.split(environment.outputDir.resolveSymbolicLinksSync()));
break;
default:
throw InvalidPatternException(pattern);
}
rawParts.skip(1).forEach(segments.add);
final String filePath = environment.fileSystem.path.joinAll(segments);
if (!hasWildcard) {
if (optional && !environment.fileSystem.isFileSync(filePath)) {
return;
}
sources.add(environment.fileSystem.file(
environment.fileSystem.path.normalize(filePath)));
return;
}
// Perform a simple match by splitting the wildcard containing file one
// the `*`. For example, for `/*.dart`, we get [.dart]. We then check
// that part of the file matches. If there are values before and after
// the `*` we need to check that both match without overlapping. For
// example, `foo_*_.dart`. We want to match `foo_b_.dart` but not
// `foo_.dart`. To do so, we first subtract the first section from the
// string if the first segment matches.
final List<String> wildcardSegments = wildcardFile?.split('*') ?? <String>[];
if (wildcardSegments.length > 2) {
throw InvalidPatternException(pattern);
}
if (!environment.fileSystem.directory(filePath).existsSync()) {
environment.fileSystem.directory(filePath).createSync(recursive: true);
}
for (final FileSystemEntity entity in environment.fileSystem.directory(filePath).listSync()) {
final String filename = environment.fileSystem.path.basename(entity.path);
if (wildcardSegments.isEmpty) {
sources.add(environment.fileSystem.file(entity.absolute));
} else if (wildcardSegments.length == 1) {
if (filename.startsWith(wildcardSegments[0]) ||
filename.endsWith(wildcardSegments[0])) {
sources.add(environment.fileSystem.file(entity.absolute));
}
} else if (filename.startsWith(wildcardSegments[0])) {
if (filename.substring(wildcardSegments[0].length).endsWith(wildcardSegments[1])) {
sources.add(environment.fileSystem.file(entity.absolute));
}
}
}
}
/// Visit a [Source] which is defined by an [Artifact] from the flutter cache.
///
/// If the [Artifact] points to a directory then all child files are included.
/// To increase the performance of builds that use a known revision of Flutter,
/// these are updated to point towards the engine.version file instead of
/// the artifact itself.
void visitArtifact(Artifact artifact, TargetPlatform? platform, BuildMode? mode) {
// This is not a local engine.
if (environment.engineVersion != null) {
sources.add(environment.flutterRootDir
.childDirectory('bin')
.childDirectory('internal')
.childFile('engine.version'),
);
return;
}
final String path = environment.artifacts
.getArtifactPath(artifact, platform: platform, mode: mode);
if (environment.fileSystem.isDirectorySync(path)) {
sources.addAll(<File>[
for (FileSystemEntity entity in environment.fileSystem.directory(path).listSync(recursive: true))
if (entity is File)
entity,
]);
return;
}
sources.add(environment.fileSystem.file(path));
}
/// Visit a [Source] which is defined by an [HostArtifact] from the flutter cache.
///
/// If the [Artifact] points to a directory then all child files are included.
/// To increase the performance of builds that use a known revision of Flutter,
/// these are updated to point towards the engine.version file instead of
/// the artifact itself.
void visitHostArtifact(HostArtifact artifact) {
// This is not a local engine.
if (environment.engineVersion != null) {
sources.add(environment.flutterRootDir
.childDirectory('bin')
.childDirectory('internal')
.childFile('engine.version'),
);
return;
}
final FileSystemEntity entity = environment.artifacts.getHostArtifact(artifact);
if (entity is Directory) {
sources.addAll(<File>[
for (FileSystemEntity entity in entity.listSync(recursive: true))
if (entity is File)
entity,
]);
return;
}
sources.add(entity as File);
}
}
/// A description of an input or output of a [Target].
abstract class Source {
/// This source is a file URL which contains some references to magic
/// environment variables.
const factory Source.pattern(String pattern, { bool optional }) = _PatternSource;
/// The source is provided by an [Artifact].
///
/// If [artifact] points to a directory then all child files are included.
const factory Source.artifact(Artifact artifact, {TargetPlatform? platform, BuildMode? mode}) = _ArtifactSource;
/// The source is provided by an [HostArtifact].
///
/// If [artifact] points to a directory then all child files are included.
const factory Source.hostArtifact(HostArtifact artifact) = _HostArtifactSource;
/// Visit the particular source type.
void accept(SourceVisitor visitor);
/// Whether the output source provided can be known before executing the rule.
///
/// This does not apply to inputs, which are always explicit and must be
/// evaluated before the build.
///
/// For example, [Source.pattern] and [Source.version] are not implicit
/// provided they do not use any wildcards.
bool get implicit;
}
class _PatternSource implements Source {
const _PatternSource(this.value, { this.optional = false });
final String value;
final bool optional;
@override
void accept(SourceVisitor visitor) => visitor.visitPattern(value, optional);
@override
bool get implicit => value.contains('*');
}
class _ArtifactSource implements Source {
const _ArtifactSource(this.artifact, { this.platform, this.mode });
final Artifact artifact;
final TargetPlatform? platform;
final BuildMode? mode;
@override
void accept(SourceVisitor visitor) => visitor.visitArtifact(artifact, platform, mode);
@override
bool get implicit => false;
}
class _HostArtifactSource implements Source {
const _HostArtifactSource(this.artifact);
final HostArtifact artifact;
@override
void accept(SourceVisitor visitor) => visitor.visitHostArtifact(artifact);
@override
bool get implicit => false;
}