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

import 'dart:async';

7
import 'package:build_daemon/client.dart';
8
import 'package:build_daemon/data/build_status.dart';
9
import 'package:build_daemon/data/build_status.dart' as build;
10 11
import 'package:build_daemon/data/build_target.dart';
import 'package:build_daemon/data/server_log.dart';
12
import 'package:crypto/crypto.dart' show md5;
13
import 'package:yaml/yaml.dart';
14 15

import '../artifacts.dart';
16
import '../base/common.dart';
17 18
import '../base/file_system.dart';
import '../base/io.dart';
19
import '../base/logger.dart';
20
import '../codegen.dart';
21
import '../dart/pub.dart';
22
import '../dart/sdk.dart';
23
import '../globals.dart' as globals;
24 25
import '../project.dart';

26
/// The minimum version of build_runner we can support in the flutter tool.
27 28
const String kMinimumBuildRunnerVersion = '1.7.1';
const String kSupportedBuildDaemonVersion = '2.1.0';
29

30 31 32 33 34
/// A wrapper for a build_runner process which delegates to a generated
/// build script.
///
/// This is only enabled if [experimentalBuildEnabled] is true, and only for
/// external flutter users.
35 36
class BuildRunner extends CodeGenerator {
  const BuildRunner();
37

38
  @override
39
  Future<void> generateBuildScript(FlutterProject flutterProject) async {
40 41
    final Directory entrypointDirectory = globals.fs.directory(globals.fs.path.join(flutterProject.dartTool.path, 'build', 'entrypoint'));
    final Directory generatedDirectory = globals.fs.directory(globals.fs.path.join(flutterProject.dartTool.path, 'flutter_tool'));
42 43 44 45 46
    final File buildSnapshot = entrypointDirectory.childFile('build.dart.snapshot');
    final File scriptIdFile = entrypointDirectory.childFile('id');
    final File syntheticPubspec = generatedDirectory.childFile('pubspec.yaml');

    // Check if contents of builders changed. If so, invalidate build script
47
    // and regenerate.
48
    final YamlMap builders = flutterProject.builders;
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
    final List<int> appliedBuilderDigest = _produceScriptId(builders);
    if (scriptIdFile.existsSync() && buildSnapshot.existsSync()) {
      final List<int> previousAppliedBuilderDigest = scriptIdFile.readAsBytesSync();
      bool digestsAreEqual = false;
      if (appliedBuilderDigest.length == previousAppliedBuilderDigest.length) {
        digestsAreEqual = true;
        for (int i = 0; i < appliedBuilderDigest.length; i++) {
          if (appliedBuilderDigest[i] != previousAppliedBuilderDigest[i]) {
            digestsAreEqual = false;
            break;
          }
        }
      }
      if (digestsAreEqual) {
        return;
      }
    }
    // Clean-up all existing artifacts.
    if (flutterProject.dartTool.existsSync()) {
      flutterProject.dartTool.deleteSync(recursive: true);
69
    }
70
    final Status status = globals.logger.startProgress('generating build script...', timeout: null);
71
    try {
72 73 74
      generatedDirectory.createSync(recursive: true);
      entrypointDirectory.createSync(recursive: true);
      flutterProject.dartTool.childDirectory('build').childDirectory('generated').createSync(recursive: true);
75 76
      final StringBuffer stringBuffer = StringBuffer();

77
      stringBuffer.writeln('name: flutter_tool');
78
      stringBuffer.writeln('dependencies:');
79
      final YamlMap builders = flutterProject.builders;
80
      if (builders != null) {
81
        for (final String name in builders.keys.cast<String>()) {
82
          final Object node = builders[name];
83 84 85
          // For relative paths, make sure it is accounted for
          // parent directories.
          if (node is YamlMap && node['path'] != null) {
86
            final String path = node['path'] as String;
87 88
            if (globals.fs.path.isRelative(path)) {
              final String convertedPath = globals.fs.path.join('..', '..', path);
89 90 91 92 93 94 95 96
              stringBuffer.writeln('  $name:');
              stringBuffer.writeln('    path: $convertedPath');
            } else {
              stringBuffer.writeln('  $name: $node');
            }
          } else {
            stringBuffer.writeln('  $name: $node');
          }
97
        }
98
      }
99
      stringBuffer.writeln('  build_runner: ^$kMinimumBuildRunnerVersion');
100
      stringBuffer.writeln('  build_daemon: $kSupportedBuildDaemonVersion');
101
      syntheticPubspec.writeAsStringSync(stringBuffer.toString());
102

103
      await pub.get(
104
        context: PubContext.pubGet,
105
        directory: generatedDirectory.path,
106 107 108
        upgrade: false,
        checkLastModified: false,
      );
109 110 111 112
      if (!scriptIdFile.existsSync()) {
        scriptIdFile.createSync(recursive: true);
      }
      scriptIdFile.writeAsBytesSync(appliedBuilderDigest);
113
      final ProcessResult generateResult = await globals.processManager.run(<String>[
114
        sdkBinaryName('pub'), 'run', 'build_runner', 'generate-build-script',
115 116 117 118
      ], workingDirectory: syntheticPubspec.parent.path);
      if (generateResult.exitCode != 0) {
        throwToolExit('Error generating build_script snapshot: ${generateResult.stderr}');
      }
119 120 121
      final File buildScript = globals.fs.file(generateResult.stdout.trim());
      final ProcessResult result = await globals.processManager.run(<String>[
        globals.artifacts.getArtifactPath(Artifact.engineDartBinary),
122 123
        '--snapshot=${buildSnapshot.path}',
        '--snapshot-kind=app-jit',
124
        '--packages=${globals.fs.path.join(generatedDirectory.path, '.packages')}',
125 126 127 128 129
        buildScript.path,
      ]);
      if (result.exitCode != 0) {
        throwToolExit('Error generating build_script snapshot: ${result.stderr}');
      }
130 131 132
    } finally {
      status.stop();
    }
133
  }
134 135

  @override
136 137
  Future<CodegenDaemon> daemon(
    FlutterProject flutterProject, {
138 139 140 141 142
    String mainPath,
    bool linkPlatformKernelIn = false,
    bool trackWidgetCreation = false,
    List<String> extraFrontEndOptions = const <String> [],
  }) async {
143
    await generateBuildScript(flutterProject);
144
    final String engineDartBinaryPath = globals.artifacts.getArtifactPath(Artifact.engineDartBinary);
145
    final File buildSnapshot = flutterProject
146 147 148
        .dartTool
        .childDirectory('build')
        .childDirectory('entrypoint')
149
        .childFile('build.dart.snapshot');
150 151 152 153 154
    final String scriptPackagesPath = flutterProject
        .dartTool
        .childDirectory('flutter_tool')
        .childFile('.packages')
        .path;
155
    final Status status = globals.logger.startProgress('starting build daemon...', timeout: null);
156 157 158
    BuildDaemonClient buildDaemonClient;
    try {
      final List<String> command = <String>[
159
        engineDartBinaryPath,
160
        '--packages=$scriptPackagesPath',
161
        buildSnapshot.path,
162
        'daemon',
163 164
        '--skip-build-script-check',
        '--delete-conflicting-outputs',
165
      ];
166 167 168 169
      buildDaemonClient = await BuildDaemonClient.connect(
        flutterProject.directory.path,
        command,
        logHandler: (ServerLog log) {
170
          if (log.message != null) {
171
            globals.printTrace(log.message);
172
          }
173
        },
174
      );
175 176 177
    } finally {
      status.stop();
    }
178 179 180 181 182 183
    // Empty string indicates we should build everything.
    final OutputLocation outputLocation = OutputLocation((OutputLocationBuilder b) => b
      ..output = ''
      ..useSymlinks = false
      ..hoist = false,
    );
184
    buildDaemonClient.registerBuildTarget(DefaultBuildTarget((DefaultBuildTargetBuilder builder) {
185 186
      builder.target = 'lib';
      builder.outputLocation = outputLocation.toBuilder();
187
    }));
188 189
    buildDaemonClient.registerBuildTarget(DefaultBuildTarget((DefaultBuildTargetBuilder builder) {
      builder.target = 'test';
190
      builder.outputLocation = outputLocation.toBuilder();
191
    }));
192
    return _BuildRunnerCodegenDaemon(buildDaemonClient);
193
  }
194 195
}

196
class _BuildRunnerCodegenDaemon implements CodegenDaemon {
197
  _BuildRunnerCodegenDaemon(this.buildDaemonClient);
198

199
  final BuildDaemonClient buildDaemonClient;
200

201 202 203
  @override
  CodegenStatus get lastStatus => _lastStatus;
  CodegenStatus _lastStatus;
204 205

  @override
206 207
  Stream<CodegenStatus> get buildResults => buildDaemonClient.buildResults.map((build.BuildResults results) {
    if (results.results.first.status == BuildStatus.failed) {
208
      return _lastStatus = CodegenStatus.Failed;
209 210
    }
    if (results.results.first.status == BuildStatus.started) {
211
      return _lastStatus = CodegenStatus.Started;
212 213
    }
    if (results.results.first.status == BuildStatus.succeeded) {
214
      return _lastStatus = CodegenStatus.Succeeded;
215
    }
216
    _lastStatus = null;
217
    return null;
218 219 220 221 222 223
  });

  @override
  void startBuild() {
    buildDaemonClient.startBuild();
  }
224
}
225 226 227 228

// Sorts the builders by name and produces a hashcode of the resulting iterable.
List<int> _produceScriptId(YamlMap builders) {
  if (builders == null || builders.isEmpty) {
229
    return md5.convert(globals.platform.version.codeUnits).bytes;
230
  }
231
  final List<String> orderedBuilderNames = builders.keys
232 233
    .cast<String>()
    .toList()..sort();
234 235 236 237 238 239
  final List<String> orderedBuilderValues = builders.values
    .map((dynamic value) => value.toString())
    .toList()..sort();
  return md5.convert(<String>[
    ...orderedBuilderNames,
    ...orderedBuilderValues,
240
    globals.platform.version,
241
  ].join('').codeUnits).bytes;
242
}