| // 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 '../project.dart'; | |
| import 'build_system.dart'; | |
| import 'depfile.dart'; | |
| import 'exceptions.dart'; | |
| ////////////////////////////////////////////////////////////////////// | |
| // // | |
| // ✨ THINKING OF MOVING/REFACTORING THIS FILE? READ ME FIRST! ✨ // | |
| // // | |
| // There is a link to this file in //docs/tool/Engine-artifacts.md // | |
| // and it would be very kind of you to update the link, if needed. // | |
| // // | |
| ////////////////////////////////////////////////////////////////////// | |
| /// 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 [FileSystemEntity]. | |
| 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; | |
| /// The current project. | |
| late final FlutterProject _project = FlutterProject.fromDirectory(environment.projectDir); | |
| @override | |
| final sources = <File>[]; | |
| @override | |
| bool get containsNewDepfile => _containsNewDepfile; | |
| var _containsNewDepfile = false; | |
| /// Visit a depfile. | |
| /// | |
| /// If the depfile does not exist, marks [containsNewDepfile] as true and | |
| /// returns. This is used by the [Node] class to tell the [BuildSystem] to | |
| /// defer hash computation until after executing the target. | |
| void visitDepfile(String name) { | |
| final File depfile = environment.buildDir.childFile(name); | |
| if (!depfile.existsSync()) { | |
| _containsNewDepfile = true; | |
| return; | |
| } | |
| final Depfile parsedDepfile = environment.depFileService.parse(depfile); | |
| if (inputs) { | |
| sources.addAll(parsedDepfile.inputs); | |
| } else { | |
| sources.addAll(parsedDepfile.outputs); | |
| } | |
| } | |
| /// 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> rawParts = pattern.split('/'); | |
| final bool hasWildcard = rawParts.last.contains('*'); | |
| String? wildcardFile; | |
| if (hasWildcard) { | |
| wildcardFile = rawParts.removeLast(); | |
| } | |
| final segments = <String>[ | |
| ...environment.fileSystem.path.split(switch (rawParts.first) { | |
| // flutter root will not contain a symbolic link. | |
| Environment.kFlutterRootDirectory => environment.flutterRootDir.absolute.path, | |
| Environment.kProjectDirectory => environment.projectDir.resolveSymbolicLinksSync(), | |
| Environment.kWorkspaceDirectory => environment.fileSystem.path.dirname( | |
| environment.fileSystem.path.dirname(environment.packageConfigPath), | |
| ), | |
| Environment.kBuildDirectory => environment.buildDir.resolveSymbolicLinksSync(), | |
| Environment.kCacheDirectory => environment.cacheDir.resolveSymbolicLinksSync(), | |
| Environment.kOutputDirectory => environment.outputDir.resolveSymbolicLinksSync(), | |
| // If the pattern does not start with an env variable, then we have nothing | |
| // to resolve it to, error out. | |
| _ => throw InvalidPatternException(pattern), | |
| }), | |
| ...rawParts.skip(1), | |
| ]; | |
| 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.stamp` 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('cache') | |
| .childFile('engine.stamp'), | |
| ); | |
| return; | |
| } | |
| final String path = environment.artifacts.getArtifactPath( | |
| artifact, | |
| platform: platform, | |
| mode: mode, | |
| ); | |
| if (environment.fileSystem.isDirectorySync(path)) { | |
| sources.addAll(<File>[ | |
| for (final 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.stamp` 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('cache') | |
| .childFile('engine.stamp'), | |
| ); | |
| return; | |
| } | |
| final FileSystemEntity entity = environment.artifacts.getHostArtifact(artifact); | |
| if (entity is Directory) { | |
| sources.addAll(<File>[ | |
| for (final FileSystemEntity entity in entity.listSync(recursive: true)) | |
| if (entity is File) entity, | |
| ]); | |
| return; | |
| } | |
| sources.add(entity as File); | |
| } | |
| void visitProjectSource(ProjectSourceBuilder builder, bool optional) { | |
| final File source = builder(_project); | |
| final String path = source.absolute.path; | |
| if (optional && !environment.fileSystem.isFileSync(path)) { | |
| return; | |
| } | |
| sources.add(environment.fileSystem.file(path)); | |
| } | |
| } | |
| /// 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 defined in [Environment]. | |
| /// | |
| /// If [optional] is true, the file is not required to exist. In this case, it | |
| /// is never resolved as an input. | |
| 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; | |
| /// The source is provided by a [FlutterProject]. | |
| /// | |
| /// If [optional] is true, the file is not required to exist. In this case, it | |
| /// is never resolved as an input. | |
| /// | |
| /// Example: | |
| /// | |
| /// ```dart | |
| /// // A project's `pubspec.yaml` file: | |
| /// Source.fromProject((FlutterProject project) => project.pubspecFile); | |
| /// ``` | |
| const factory Source.fromProject(ProjectSourceBuilder sourceBuilder, {bool optional}) = | |
| _ProjectSource; | |
| /// 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] is 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; | |
| } | |
| typedef ProjectSourceBuilder = File Function(FlutterProject); | |
| class _ProjectSource implements Source { | |
| const _ProjectSource(this.builder, {this.optional = false}); | |
| final ProjectSourceBuilder builder; | |
| final bool optional; | |
| @override | |
| void accept(SourceVisitor visitor) => visitor.visitProjectSource(builder, optional); | |
| @override | |
| bool get implicit => false; | |
| } |