assemble.dart 11.1 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 18 19
import '../build_system/targets/ios.dart';
import '../build_system/targets/linux.dart';
import '../build_system/targets/macos.dart';
20
import '../build_system/targets/web.dart';
21
import '../build_system/targets/windows.dart';
22
import '../cache.dart';
23
import '../convert.dart';
24
import '../globals.dart' as globals;
25
import '../project.dart';
26
import '../reporting/reporting.dart';
27 28
import '../runner/flutter_command.dart';

29 30
/// All currently implemented targets.
const List<Target> _kDefaultTargets = <Target>[
31
  // Shared targets
32 33
  CopyAssets(),
  KernelSnapshot(),
34 35
  AotElfProfile(TargetPlatform.android_arm),
  AotElfRelease(TargetPlatform.android_arm),
36 37
  AotAssemblyProfile(),
  AotAssemblyRelease(),
38
  // macOS targets
39
  DebugMacOSFramework(),
40 41 42
  DebugMacOSBundleFlutterAssets(),
  ProfileMacOSBundleFlutterAssets(),
  ReleaseMacOSBundleFlutterAssets(),
43
  // Linux targets
44
  DebugBundleLinuxAssets(),
45 46
  ProfileBundleLinuxAssets(),
  ReleaseBundleLinuxAssets(),
47
  // Web targets
48
  WebServiceWorker(),
49
  ReleaseAndroidApplication(),
50
  // This is a one-off rule for bundle and aot compat.
51
  CopyFlutterBundle(),
52 53 54
  // Android targets,
  DebugAndroidApplication(),
  ProfileAndroidApplication(),
55 56 57 58 59 60 61
  // Android ABI specific AOT rules.
  androidArmProfileBundle,
  androidArm64ProfileBundle,
  androidx64ProfileBundle,
  androidArmReleaseBundle,
  androidArm64ReleaseBundle,
  androidx64ReleaseBundle,
62
  // iOS targets
63 64 65
  DebugIosApplicationBundle(),
  ProfileIosApplicationBundle(),
  ReleaseIosApplicationBundle(),
66 67 68
  // Windows targets
  UnpackWindows(),
  DebugBundleWindowsAssets(),
69 70
  ProfileBundleWindowsAssets(),
  ReleaseBundleWindowsAssets(),
71 72
];

73 74 75 76 77 78 79
/// Assemble provides a low level API to interact with the flutter tool build
/// system.
class AssembleCommand extends FlutterCommand {
  AssembleCommand() {
    argParser.addMultiOption(
      'define',
      abbr: 'd',
80
      help: 'Allows passing configuration to a target with --define=target=key=value.',
81
    );
82 83 84 85
    argParser.addOption(
      'performance-measurement-file',
      help: 'Output individual target performance to a JSON file.'
    );
86 87 88 89 90 91 92
    argParser.addMultiOption(
      'input',
      abbr: 'i',
      help: 'Allows passing additional inputs with --input=key=value. Unlike '
      'defines, additional inputs do not generate a new configuration, instead '
      'they are treated as dependencies of the targets that use them.'
    );
93 94 95
    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'
    );
96 97 98 99 100 101 102 103
    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 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.');
104 105 106 107
    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.',
    );
108
    argParser.addOption(kExtraGenSnapshotOptions);
109
    argParser.addOption(kExtraFrontEndOptions);
110
    argParser.addOption(kDartDefines);
111 112
    argParser.addOption(
      'resource-pool-size',
113
      help: 'The maximum number of concurrent tasks the build system will run.',
114 115 116
    );
  }

117
  @override
118
  String get description => 'Assemble and build Flutter resources.';
119

120 121
  @override
  String get name => 'assemble';
122

123 124
  @override
  Future<Map<CustomDimensions, String>> get usageValues async {
125 126
    final FlutterProject flutterProject = FlutterProject.current();
    if (flutterProject == null) {
127 128
      return const <CustomDimensions, String>{};
    }
129
    try {
130
      final Environment localEnvironment = createEnvironment();
131 132
      return <CustomDimensions, String>{
        CustomDimensions.commandBuildBundleTargetPlatform: localEnvironment.defines['TargetPlatform'],
133
        CustomDimensions.commandBuildBundleIsModule: '${flutterProject.isModule}',
134
      };
135
    } on Exception {
136 137 138
      // We've failed to send usage.
    }
    return const <CustomDimensions, String>{};
139 140
  }

141
  /// The target(s) we are building.
142
  List<Target> createTargets() {
143 144 145
    if (argResults.rest.isEmpty) {
      throwToolExit('missing target name for flutter assemble.');
    }
146
    final String name = argResults.rest.first;
147
    final Map<String, Target> targetMap = <String, Target>{
148
      for (final Target target in _kDefaultTargets)
149 150 151
        target.name: target
    };
    final List<Target> results = <Target>[
152
      for (final String targetName in argResults.rest)
153 154 155 156
        if (targetMap.containsKey(targetName))
          targetMap[targetName]
    ];
    if (results.isEmpty) {
157
      throwToolExit('No target named "$name" defined.');
158
    }
159
    return results;
160 161 162
  }

  /// The environmental configuration for a build invocation.
163
  Environment createEnvironment() {
164
    final FlutterProject flutterProject = FlutterProject.current();
165
    String output = stringArg('output');
166 167 168 169
    if (output == null) {
      throwToolExit('--output directory is required for assemble.');
    }
    // If path is relative, make it absolute from flutter project.
170 171
    if (globals.fs.path.isRelative(output)) {
      output = globals.fs.path.join(flutterProject.directory.path, output);
172
    }
173
    final Environment result = Environment(
174
      outputDir: globals.fs.directory(output),
175 176 177
      buildDir: flutterProject.directory
          .childDirectory('.dart_tool')
          .childDirectory('flutter_build'),
178
      projectDir: flutterProject.directory,
179
      defines: _parseDefines(stringsArg('define')),
180
      inputs: _parseDefines(stringsArg('input')),
181 182
      cacheDir: globals.cache.getRoot(),
      flutterRootDir: globals.fs.directory(Cache.flutterRoot),
183 184 185 186
      artifacts: globals.artifacts,
      fileSystem: globals.fs,
      logger: globals.logger,
      processManager: globals.processManager,
187 188 189
      engineVersion: globals.artifacts.isLocalEngine
        ? null
        : globals.flutterVersion.engineRevision
190 191 192 193
    );
    return result;
  }

194
  Map<String, String> _parseDefines(List<String> values) {
195
    final Map<String, String> results = <String, String>{};
196
    for (final String chunk in values) {
197 198
      final int indexEquals = chunk.indexOf('=');
      if (indexEquals == -1) {
199 200
        throwToolExit('Improperly formatted define flag: $chunk');
      }
201 202
      final String key = chunk.substring(0, indexEquals);
      final String value = chunk.substring(indexEquals + 1);
203 204
      results[key] = value;
    }
205 206 207 208
    // Workaround for extraGenSnapshot formatting.
    if (argResults.wasParsed(kExtraGenSnapshotOptions)) {
      results[kExtraGenSnapshotOptions] = argResults[kExtraGenSnapshotOptions] as String;
    }
209 210 211
    if (argResults.wasParsed(kDartDefines)) {
      results[kDartDefines] = argResults[kDartDefines] as String;
    }
212 213 214
    if (argResults.wasParsed(kExtraFrontEndOptions)) {
      results[kExtraFrontEndOptions] = argResults[kExtraFrontEndOptions] as String;
    }
215 216 217 218 219
    return results;
  }

  @override
  Future<FlutterCommandResult> runCommand() async {
220
    final List<Target> targets = createTargets();
221
    final Target target = targets.length == 1 ? targets.single : _CompositeTarget(targets);
222 223 224 225 226 227 228 229 230
    final BuildResult result = await globals.buildSystem.build(
      target,
      createEnvironment(),
      buildSystemConfig: BuildSystemConfig(
        resourcePoolSize: argResults.wasParsed('resource-pool-size')
          ? int.tryParse(stringArg('resource-pool-size'))
          : null,
        ),
      );
231
    if (!result.success) {
232
      for (final ExceptionMeasurement measurement in result.exceptions.values) {
233 234 235 236 237
        if (measurement.fatal || globals.logger.isVerbose) {
          globals.printError('Target ${measurement.target} failed: ${measurement.exception}',
            stackTrace: measurement.stackTrace
          );
        }
238
      }
239
      throwToolExit('');
240
    }
241
    globals.printTrace('build succeeded.');
242
    if (argResults.wasParsed('build-inputs')) {
243
      writeListIfChanged(result.inputFiles, stringArg('build-inputs'));
244 245
    }
    if (argResults.wasParsed('build-outputs')) {
246
      writeListIfChanged(result.outputFiles, stringArg('build-outputs'));
247
    }
248 249 250 251
    if (argResults.wasParsed('performance-measurement-file')) {
      final File outFile = globals.fs.file(argResults['performance-measurement-file']);
      writePerformanceData(result.performance.values, outFile);
    }
252
    if (argResults.wasParsed('depfile')) {
253
      final File depfileFile = globals.fs.file(stringArg('depfile'));
254
      final Depfile depfile = Depfile(result.inputFiles, result.outputFiles);
255 256 257 258 259
      final DepfileService depfileService = DepfileService(
        fileSystem: globals.fs,
        logger: globals.logger,
      );
      depfileService.writeToFile(depfile, globals.fs.file(depfileFile));
260
    }
261
    return FlutterCommandResult.success();
262 263
  }
}
264 265 266

@visibleForTesting
void writeListIfChanged(List<File> files, String path) {
267
  final File file = globals.fs.file(path);
268 269
  final StringBuffer buffer = StringBuffer();
  // These files are already sorted.
270
  for (final File file in files) {
271
    buffer.writeln(file.path);
272 273 274 275 276 277 278 279 280 281
  }
  final String newContents = buffer.toString();
  if (!file.existsSync()) {
    file.writeAsStringSync(newContents);
  }
  final String currentContents = file.readAsStringSync();
  if (currentContents != newContents) {
    file.writeAsStringSync(newContents);
  }
}
282

283 284 285 286 287 288 289
/// 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>{
290
          'name': measurement.analyticsName,
291 292 293 294 295 296 297 298 299 300 301 302
          'skipped': measurement.skipped,
          'succeeded': measurement.succeeded,
          'elapsedMilliseconds': measurement.elapsedMilliseconds,
        }
    ]
  };
  if (!outFile.parent.existsSync()) {
    outFile.parent.createSync(recursive: true);
  }
  outFile.writeAsStringSync(json.encode(jsonData));
}

303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
class _CompositeTarget extends Target {
  _CompositeTarget(this.dependencies);

  @override
  final List<Target> dependencies;

  @override
  String get name => '_composite';

  @override
  Future<void> build(Environment environment) async { }

  @override
  List<Source> get inputs => <Source>[];

  @override
  List<Source> get outputs => <Source>[];
}