blob: b34593195ce8370e9fdd9515286017ec00fc63e3 [file] [log] [blame]
// 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;
}