source.dart 8.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7
// 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';
8
import '../globals.dart' as globals;
9 10 11
import 'build_system.dart';
import 'exceptions.dart';

12 13 14 15 16 17 18 19 20 21 22 23
/// 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;
}

24
/// Collects sources for a [Target] into a single list of [FileSystemEntities].
25
class SourceVisitor implements ResolvedFiles {
26 27 28 29 30 31 32 33 34 35 36
  /// 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;

37
  @override
38 39
  final List<File> sources = <File>[];

40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
  @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) {
59
      globals.printError('Invalid depfile: ${depfile.path}');
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
      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()
81
        .map((String path) => globals.fs.file(path));
82 83
  }

84
  /// Visit a [Source] which contains a file URL.
85
  ///
86
  /// The URL may include constants defined in an [Environment]. If
87 88 89
  /// [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) {
90 91 92 93 94 95 96 97 98 99 100 101 102 103
    // 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(
104
            globals.fs.path.split(environment.projectDir.resolveSymbolicLinksSync()));
105 106
        break;
      case Environment.kBuildDirectory:
107
        segments.addAll(globals.fs.path.split(
108 109 110 111
            environment.buildDir.resolveSymbolicLinksSync()));
        break;
      case Environment.kCacheDirectory:
        segments.addAll(
112
            globals.fs.path.split(environment.cacheDir.resolveSymbolicLinksSync()));
113 114
        break;
      case Environment.kFlutterRootDirectory:
115
        // flutter root will not contain a symbolic link.
116
        segments.addAll(
117
            globals.fs.path.split(environment.flutterRootDir.absolute.path));
118
        break;
119 120
      case Environment.kOutputDirectory:
        segments.addAll(
121
            globals.fs.path.split(environment.outputDir.resolveSymbolicLinksSync()));
122
        break;
123 124 125 126
      default:
        throw InvalidPatternException(pattern);
    }
    rawParts.skip(1).forEach(segments.add);
127
    final String filePath = globals.fs.path.joinAll(segments);
128
    if (!hasWildcard) {
129
      if (optional && !globals.fs.isFileSync(filePath)) {
130
        return;
131
      }
132
      sources.add(globals.fs.file(globals.fs.path.normalize(filePath)));
133 134 135 136 137 138 139 140 141 142 143 144 145
      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('*');
    if (wildcardSegments.length > 2) {
      throw InvalidPatternException(pattern);
    }
146
    if (!globals.fs.directory(filePath).existsSync()) {
147 148
      throw Exception('$filePath does not exist!');
    }
149
    for (final FileSystemEntity entity in globals.fs.directory(filePath).listSync()) {
150
      final String filename = globals.fs.path.basename(entity.path);
151
      if (wildcardSegments.isEmpty) {
152
        sources.add(globals.fs.file(entity.absolute));
153 154 155
      } else if (wildcardSegments.length == 1) {
        if (filename.startsWith(wildcardSegments[0]) ||
            filename.endsWith(wildcardSegments[0])) {
156
          sources.add(globals.fs.file(entity.absolute));
157 158 159
        }
      } else if (filename.startsWith(wildcardSegments[0])) {
        if (filename.substring(wildcardSegments[0].length).endsWith(wildcardSegments[1])) {
160
          sources.add(globals.fs.file(entity.absolute));
161 162 163 164 165 166 167 168 169
        }
      }
    }
  }

  /// 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.
  void visitArtifact(Artifact artifact, TargetPlatform platform, BuildMode mode) {
170 171
    final String path = globals.artifacts.getArtifactPath(artifact, platform: platform, mode: mode);
    if (globals.fs.isDirectorySync(path)) {
172
      sources.addAll(<File>[
173
        for (FileSystemEntity entity in globals.fs.directory(path).listSync(recursive: true))
174
          if (entity is File)
175
            entity,
176 177
      ]);
    } else {
178
      sources.add(globals.fs.file(path));
179 180 181 182 183 184
    }
  }
}

/// A description of an input or output of a [Target].
abstract class Source {
185
  /// This source is a file URL which contains some references to magic
186
  /// environment variables.
187
  const factory Source.pattern(String pattern, { bool optional }) = _PatternSource;
188 189 190
  /// The source is provided by an [Artifact].
  ///
  /// If [artifact] points to a directory then all child files are included.
191
  const factory Source.artifact(Artifact artifact, {TargetPlatform platform, BuildMode mode}) = _ArtifactSource;
192 193 194 195 196 197 198 199 200 201

  /// 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
202
  /// provided they do not use any wildcards.
203 204 205 206
  bool get implicit;
}

class _PatternSource implements Source {
207
  const _PatternSource(this.value, { this.optional = false });
208 209

  final String value;
210
  final bool optional;
211 212

  @override
213
  void accept(SourceVisitor visitor) => visitor.visitPattern(value, optional);
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231

  @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;
}