assemble.dart 14.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7 8
import 'package:meta/meta.dart';

9
import '../base/common.dart';
10
import '../base/file_system.dart';
11
import '../build_info.dart';
12
import '../build_system/build_system.dart';
13 14
import '../build_system/depfile.dart';
import '../build_system/targets/android.dart';
15
import '../build_system/targets/assets.dart';
16
import '../build_system/targets/common.dart';
17
import '../build_system/targets/deferred_components.dart';
18 19 20
import '../build_system/targets/ios.dart';
import '../build_system/targets/linux.dart';
import '../build_system/targets/macos.dart';
21
import '../build_system/targets/web.dart';
22
import '../build_system/targets/windows.dart';
23
import '../cache.dart';
24
import '../convert.dart';
25
import '../globals_null_migrated.dart' as globals;
26
import '../project.dart';
27
import '../reporting/reporting.dart';
28 29
import '../runner/flutter_command.dart';

30
/// All currently implemented targets.
31
List<Target> _kDefaultTargets = <Target>[
32
  // Shared targets
33 34 35 36 37 38
  const CopyAssets(),
  const KernelSnapshot(),
  const AotElfProfile(TargetPlatform.android_arm),
  const AotElfRelease(TargetPlatform.android_arm),
  const AotAssemblyProfile(),
  const AotAssemblyRelease(),
39
  // macOS targets
40 41 42 43
  const DebugMacOSFramework(),
  const DebugMacOSBundleFlutterAssets(),
  const ProfileMacOSBundleFlutterAssets(),
  const ReleaseMacOSBundleFlutterAssets(),
44
  // Linux targets
45 46 47 48 49 50
  const DebugBundleLinuxAssets(TargetPlatform.linux_x64),
  const DebugBundleLinuxAssets(TargetPlatform.linux_arm64),
  const ProfileBundleLinuxAssets(TargetPlatform.linux_x64),
  const ProfileBundleLinuxAssets(TargetPlatform.linux_arm64),
  const ReleaseBundleLinuxAssets(TargetPlatform.linux_x64),
  const ReleaseBundleLinuxAssets(TargetPlatform.linux_arm64),
51
  // Web targets
52 53
  const WebServiceWorker(),
  const ReleaseAndroidApplication(),
54
  // This is a one-off rule for bundle and aot compat.
55
  const CopyFlutterBundle(),
56
  // Android targets,
57 58
  const DebugAndroidApplication(),
  const ProfileAndroidApplication(),
59 60 61 62 63 64 65
  // Android ABI specific AOT rules.
  androidArmProfileBundle,
  androidArm64ProfileBundle,
  androidx64ProfileBundle,
  androidArmReleaseBundle,
  androidArm64ReleaseBundle,
  androidx64ReleaseBundle,
66 67 68 69 70 71 72
  // Deferred component enabled AOT rules
  androidArmProfileDeferredComponentsBundle,
  androidArm64ProfileDeferredComponentsBundle,
  androidx64ProfileDeferredComponentsBundle,
  androidArmReleaseDeferredComponentsBundle,
  androidArm64ReleaseDeferredComponentsBundle,
  androidx64ReleaseDeferredComponentsBundle,
73
  // iOS targets
74 75 76
  const DebugIosApplicationBundle(),
  const ProfileIosApplicationBundle(),
  const ReleaseIosApplicationBundle(),
77
  // Windows targets
78 79 80 81
  const UnpackWindows(),
  const DebugBundleWindowsAssets(),
  const ProfileBundleWindowsAssets(),
  const ReleaseBundleWindowsAssets(),
82 83 84 85
  // Windows UWP targets
  const DebugBundleWindowsAssetsUwp(),
  const ProfileBundleWindowsAssetsUwp(),
  const ReleaseBundleWindowsAssetsUwp(),
86 87
];

88 89 90
/// Assemble provides a low level API to interact with the flutter tool build
/// system.
class AssembleCommand extends FlutterCommand {
91 92
  AssembleCommand({ bool verboseHelp = false, @required BuildSystem buildSystem })
    : _buildSystem = buildSystem {
93 94 95
    argParser.addMultiOption(
      'define',
      abbr: 'd',
96 97
      valueHelp: 'target=key=value',
      help: 'Allows passing configuration to a target, as in "--define=target=key=value".',
98
    );
99 100 101 102
    argParser.addOption(
      'performance-measurement-file',
      help: 'Output individual target performance to a JSON file.'
    );
103 104 105
    argParser.addMultiOption(
      'input',
      abbr: 'i',
106 107
      help: 'Allows passing additional inputs with "--input=key=value". Unlike '
      'defines, additional inputs do not generate a new configuration; instead '
108 109
      'they are treated as dependencies of the targets that use them.'
    );
110 111 112
    argParser.addOption('depfile',
      help: 'A file path where a depfile will be written. '
            'This contains all build inputs and outputs in a Make-style syntax.'
113
    );
114 115 116 117 118 119 120 121
    argParser.addOption('build-inputs', help: 'A file path where a newline-separated '
        'file containing all inputs used will be written after a build. '
        'This file is not included as a build input or output. This file is not '
        'written if the build fails for any reason.');
    argParser.addOption('build-outputs', help: 'A file path where a newline-separated '
        'file containing all outputs created will be written after a build. '
        'This file is not included as a build input or output. This file is not '
        'written if the build fails for any reason.');
122 123 124 125
    argParser.addOption('output', abbr: 'o', help: 'A directory where output '
        'files will be written. Must be either absolute or relative from the '
        'root of the current Flutter project.',
    );
126 127
    usesExtraDartFlagOptions(verboseHelp: verboseHelp);
    usesDartDefineOption();
128 129
    argParser.addOption(
      'resource-pool-size',
130
      help: 'The maximum number of concurrent tasks the build system will run.',
131 132 133
    );
  }

134 135
  final BuildSystem _buildSystem;

136
  @override
137
  String get description => 'Assemble and build Flutter resources.';
138

139 140
  @override
  String get name => 'assemble';
141

142
  @override
143
  Future<CustomDimensions> get usageValues async {
144 145
    final FlutterProject flutterProject = FlutterProject.current();
    if (flutterProject == null) {
146
      return const CustomDimensions();
147
    }
148
    try {
149 150 151 152
      return CustomDimensions(
        commandBuildBundleTargetPlatform: environment.defines[kTargetPlatform],
        commandBuildBundleIsModule: flutterProject.isModule,
      );
153
    } on Exception {
154 155
      // We've failed to send usage.
    }
156
    return const CustomDimensions();
157 158
  }

159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
  @override
  Future<Set<DevelopmentArtifact>> get requiredArtifacts async {
    final String platform = environment.defines[kTargetPlatform];
    if (platform == null) {
      return super.requiredArtifacts;
    }

    final TargetPlatform targetPlatform = getTargetPlatformForName(platform);
    final DevelopmentArtifact artifact = artifactFromTargetPlatform(targetPlatform);
    if (artifact != null) {
      return <DevelopmentArtifact>{artifact};
    }
    return super.requiredArtifacts;
  }

174
  /// The target(s) we are building.
175
  List<Target> createTargets() {
176 177 178
    if (argResults.rest.isEmpty) {
      throwToolExit('missing target name for flutter assemble.');
    }
179
    final String name = argResults.rest.first;
180
    final Map<String, Target> targetMap = <String, Target>{
181
      for (final Target target in _kDefaultTargets)
182 183 184
        target.name: target
    };
    final List<Target> results = <Target>[
185
      for (final String targetName in argResults.rest)
186 187 188 189
        if (targetMap.containsKey(targetName))
          targetMap[targetName]
    ];
    if (results.isEmpty) {
190
      throwToolExit('No target named "$name" defined.');
191
    }
192
    return results;
193 194
  }

195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
  bool isDeferredComponentsTargets() {
    for (final String targetName in argResults.rest) {
      if (deferredComponentsTargets.contains(targetName)) {
        return true;
      }
    }
    return false;
  }

  bool isDebug() {
    for (final String targetName in argResults.rest) {
      if (targetName.contains('debug')) {
        return true;
      }
    }
    return false;
  }

213 214 215
  Environment get environment => _environment ??= createEnvironment();
  Environment _environment;

216
  /// The environmental configuration for a build invocation.
217
  Environment createEnvironment() {
218
    final FlutterProject flutterProject = FlutterProject.current();
219
    String output = stringArg('output');
220 221 222 223
    if (output == null) {
      throwToolExit('--output directory is required for assemble.');
    }
    // If path is relative, make it absolute from flutter project.
224 225
    if (globals.fs.path.isRelative(output)) {
      output = globals.fs.path.join(flutterProject.directory.path, output);
226
    }
227
    final Environment result = Environment(
228
      outputDir: globals.fs.directory(output),
229 230 231
      buildDir: flutterProject.directory
          .childDirectory('.dart_tool')
          .childDirectory('flutter_build'),
232
      projectDir: flutterProject.directory,
233
      defines: _parseDefines(stringsArg('define')),
234
      inputs: _parseDefines(stringsArg('input')),
235 236
      cacheDir: globals.cache.getRoot(),
      flutterRootDir: globals.fs.directory(Cache.flutterRoot),
237 238 239 240
      artifacts: globals.artifacts,
      fileSystem: globals.fs,
      logger: globals.logger,
      processManager: globals.processManager,
241
      platform: globals.platform,
242 243
      engineVersion: globals.artifacts.isLocalEngine
        ? null
244 245
        : globals.flutterVersion.engineRevision,
      generateDartPluginRegistry: true,
246 247 248 249
    );
    return result;
  }

250
  Map<String, String> _parseDefines(List<String> values) {
251
    final Map<String, String> results = <String, String>{};
252
    for (final String chunk in values) {
253 254
      final int indexEquals = chunk.indexOf('=');
      if (indexEquals == -1) {
255 256
        throwToolExit('Improperly formatted define flag: $chunk');
      }
257 258
      final String key = chunk.substring(0, indexEquals);
      final String value = chunk.substring(indexEquals + 1);
259 260
      results[key] = value;
    }
261 262
    if (argResults.wasParsed(FlutterOptions.kExtraGenSnapshotOptions)) {
      results[kExtraGenSnapshotOptions] = (argResults[FlutterOptions.kExtraGenSnapshotOptions] as List<String>).join(',');
263
    }
264 265
    if (argResults.wasParsed(FlutterOptions.kDartDefinesOption)) {
      results[kDartDefines] = (argResults[FlutterOptions.kDartDefinesOption] as List<String>).join(',');
266
    }
267 268 269 270
    results[kDeferredComponents] = 'false';
    if (FlutterProject.current().manifest.deferredComponents != null && isDeferredComponentsTargets() && !isDebug()) {
      results[kDeferredComponents] = 'true';
    }
271 272
    if (argResults.wasParsed(FlutterOptions.kExtraFrontEndOptions)) {
      results[kExtraFrontEndOptions] = (argResults[FlutterOptions.kExtraFrontEndOptions] as List<String>).join(',');
273
    }
274 275 276 277 278
    return results;
  }

  @override
  Future<FlutterCommandResult> runCommand() async {
279
    final List<Target> targets = createTargets();
280 281 282 283 284 285 286 287 288 289
    final List<Target> nonDeferredTargets = <Target>[];
    final List<Target> deferredTargets = <AndroidAotDeferredComponentsBundle>[];
    for (final Target target in targets) {
      if (deferredComponentsTargets.contains(target.name)) {
        deferredTargets.add(target);
      } else {
        nonDeferredTargets.add(target);
      }
    }
    Target target;
290 291 292 293 294 295 296 297 298
    List<String> decodedDefines;
    try {
      decodedDefines = decodeDartDefines(environment.defines, kDartDefines);
    } on FormatException {
      throwToolExit(
        'Error parsing assemble command: your generated configuration may be out of date. '
        "Try re-running 'flutter build ios' or the appropriate build command."
      );
    }
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
    if (FlutterProject.current().manifest.deferredComponents != null
        && decodedDefines.contains('validate-deferred-components=true')
        && deferredTargets.isNotEmpty
        && !isDebug()) {
      // Add deferred components validation target that require loading units.
      target = DeferredComponentsGenSnapshotValidatorTarget(
        deferredComponentsDependencies: deferredTargets.cast<AndroidAotDeferredComponentsBundle>(),
        nonDeferredComponentsDependencies: nonDeferredTargets,
        title: 'Deferred components gen_snapshot validation',
      );
    } else if (targets.length > 1) {
      target = CompositeTarget(targets);
    } else if (targets.isNotEmpty) {
      target = targets.single;
    }
314
    final BuildResult result = await _buildSystem.build(
315
      target,
316
      environment,
317 318 319 320 321 322
      buildSystemConfig: BuildSystemConfig(
        resourcePoolSize: argResults.wasParsed('resource-pool-size')
          ? int.tryParse(stringArg('resource-pool-size'))
          : null,
        ),
      );
323
    if (!result.success) {
324
      for (final ExceptionMeasurement measurement in result.exceptions.values) {
325 326 327 328 329
        if (measurement.fatal || globals.logger.isVerbose) {
          globals.printError('Target ${measurement.target} failed: ${measurement.exception}',
            stackTrace: measurement.stackTrace
          );
        }
330
      }
331
      throwToolExit('');
332
    }
333
    globals.printTrace('build succeeded.');
334

335
    if (argResults.wasParsed('build-inputs')) {
336
      writeListIfChanged(result.inputFiles, stringArg('build-inputs'));
337 338
    }
    if (argResults.wasParsed('build-outputs')) {
339
      writeListIfChanged(result.outputFiles, stringArg('build-outputs'));
340
    }
341 342 343 344
    if (argResults.wasParsed('performance-measurement-file')) {
      final File outFile = globals.fs.file(argResults['performance-measurement-file']);
      writePerformanceData(result.performance.values, outFile);
    }
345
    if (argResults.wasParsed('depfile')) {
346
      final File depfileFile = globals.fs.file(stringArg('depfile'));
347
      final Depfile depfile = Depfile(result.inputFiles, result.outputFiles);
348 349 350 351 352
      final DepfileService depfileService = DepfileService(
        fileSystem: globals.fs,
        logger: globals.logger,
      );
      depfileService.writeToFile(depfile, globals.fs.file(depfileFile));
353
    }
354
    return FlutterCommandResult.success();
355 356
  }
}
357 358 359

@visibleForTesting
void writeListIfChanged(List<File> files, String path) {
360
  final File file = globals.fs.file(path);
361 362
  final StringBuffer buffer = StringBuffer();
  // These files are already sorted.
363
  for (final File file in files) {
364
    buffer.writeln(file.path);
365 366 367 368 369 370 371 372 373 374
  }
  final String newContents = buffer.toString();
  if (!file.existsSync()) {
    file.writeAsStringSync(newContents);
  }
  final String currentContents = file.readAsStringSync();
  if (currentContents != newContents) {
    file.writeAsStringSync(newContents);
  }
}
375

376 377 378 379 380 381 382
/// Output performance measurement data in [outFile].
@visibleForTesting
void writePerformanceData(Iterable<PerformanceMeasurement> measurements, File outFile) {
  final Map<String, Object> jsonData = <String, Object>{
    'targets': <Object>[
      for (final PerformanceMeasurement measurement in measurements)
        <String, Object>{
383
          'name': measurement.analyticsName,
384 385 386 387 388 389 390 391 392 393 394
          'skipped': measurement.skipped,
          'succeeded': measurement.succeeded,
          'elapsedMilliseconds': measurement.elapsedMilliseconds,
        }
    ]
  };
  if (!outFile.parent.existsSync()) {
    outFile.parent.createSync(recursive: true);
  }
  outFile.writeAsStringSync(json.encode(jsonData));
}