blob: 11ba92c78f2d4afd22d1e0495f25c8345b09fcce [file] [log] [blame]
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive.dart';
import 'package:build/build.dart';
import 'package:build_modules/build_modules.dart';
import 'package:crypto/crypto.dart';
import 'package:glob/glob.dart';
import 'package:path/path.dart' as p;
import 'package:scratch_space/scratch_space.dart';
import 'platforms.dart';
import 'web_entrypoint_builder.dart';
/// Compiles an the primary input of [buildStep] with dart2js.
///
/// If [skipPlatformCheck] is `true` then all `dart:` imports will be
/// allowed in all packages.
Future<void> bootstrapDart2Js(BuildStep buildStep, List<String> dart2JsArgs,
{bool skipPlatformCheck}) async {
skipPlatformCheck ??= false;
var dartEntrypointId = buildStep.inputId;
var moduleId =
dartEntrypointId.changeExtension(moduleExtension(dart2jsPlatform));
var args = <String>[];
{
var module = Module.fromJson(
json.decode(await buildStep.readAsString(moduleId))
as Map<String, dynamic>);
List<Module> allDeps;
try {
allDeps = (await module.computeTransitiveDependencies(buildStep,
throwIfUnsupported: !skipPlatformCheck))
..add(module);
} on UnsupportedModules catch (e) {
var librariesString = (await e.exactLibraries(buildStep).toList())
.map((lib) => AssetId(lib.id.package,
lib.id.path.replaceFirst(moduleLibraryExtension, '.dart')))
.join('\n');
log.warning('''
Skipping compiling ${buildStep.inputId} with dart2js because some of its
transitive libraries have sdk dependencies that not supported on this platform:
$librariesString
https://github.com/dart-lang/build/blob/master/docs/faq.md#how-can-i-resolve-skipped-compiling-warnings
''');
return;
}
var scratchSpace = await buildStep.fetchResource(scratchSpaceResource);
var allSrcs = allDeps.expand((module) => module.sources);
await scratchSpace.ensureAssets(allSrcs, buildStep);
var packageFile =
await _createPackageFile(allSrcs, buildStep, scratchSpace);
var dartPath = dartEntrypointId.path.startsWith('lib/')
? 'package:${dartEntrypointId.package}/'
'${dartEntrypointId.path.substring('lib/'.length)}'
: dartEntrypointId.path;
var jsOutputPath =
'${p.withoutExtension(dartPath.replaceFirst('package:', 'packages/'))}'
'$jsEntrypointExtension';
args = dart2JsArgs.toList()
..addAll([
'--packages=$packageFile',
'-o$jsOutputPath',
dartPath,
]);
}
var dart2js = await buildStep.fetchResource(dart2JsWorkerResource);
var result = await dart2js.compile(args);
var jsOutputId = dartEntrypointId.changeExtension(jsEntrypointExtension);
var jsOutputFile = scratchSpace.fileFor(jsOutputId);
if (result.succeeded && await jsOutputFile.exists()) {
log.info(result.output);
var rootDir = p.dirname(jsOutputFile.path);
var dartFile = p.basename(dartEntrypointId.path);
var fileGlob = Glob('$dartFile.js*');
var archive = Archive();
await for (var jsFile in fileGlob.list(root: rootDir)) {
if (jsFile.path.endsWith(jsEntrypointExtension) ||
jsFile.path.endsWith(jsEntrypointSourceMapExtension)) {
// These are explicitly output, and are not part of the archive.
continue;
}
if (jsFile is File) {
var fileName = p.relative(jsFile.path, from: rootDir);
var fileStats = await jsFile.stat();
archive.addFile(
ArchiveFile(fileName, fileStats.size, await jsFile.readAsBytes())
..mode = fileStats.mode
..lastModTime = fileStats.modified.millisecondsSinceEpoch);
}
}
if (archive.isNotEmpty) {
var archiveId =
dartEntrypointId.changeExtension(jsEntrypointArchiveExtension);
await buildStep.writeAsBytes(archiveId, TarEncoder().encode(archive));
}
// Explicitly write out the original js file and sourcemap - we can't output
// these as part of the archive because they already have asset nodes.
await scratchSpace.copyOutput(jsOutputId, buildStep);
var jsSourceMapId =
dartEntrypointId.changeExtension(jsEntrypointSourceMapExtension);
await _copyIfExists(jsSourceMapId, scratchSpace, buildStep);
} else {
log.severe(result.output);
}
}
Future<void> _copyIfExists(
AssetId id, ScratchSpace scratchSpace, AssetWriter writer) async {
var file = scratchSpace.fileFor(id);
if (await file.exists()) {
await scratchSpace.copyOutput(id, writer);
}
}
/// Creates a `.packages` file unique to this entrypoint at the root of the
/// scratch space and returns it's filename.
///
/// Since mulitple invocations of Dart2Js will share a scratch space and we only
/// know the set of packages involved the current entrypoint we can't construct
/// a `.packages` file that will work for all invocations of Dart2Js so a unique
/// file is created for every entrypoint that is run.
///
/// The filename is based off the MD5 hash of the asset path so that files are
/// unique regarless of situations like `web/foo/bar.dart` vs
/// `web/foo-bar.dart`.
Future<String> _createPackageFile(Iterable<AssetId> inputSources,
BuildStep buildStep, ScratchSpace scratchSpace) async {
var inputUri = buildStep.inputId.uri;
var packageFileName =
'.package-${md5.convert(inputUri.toString().codeUnits)}';
var packagesFile =
scratchSpace.fileFor(AssetId(buildStep.inputId.package, packageFileName));
var packageNames = inputSources.map((s) => s.package).toSet();
var packagesFileContent =
packageNames.map((n) => '$n:packages/$n/').join('\n');
await packagesFile
.writeAsString('# Generated for $inputUri\n$packagesFileContent');
return packageFileName;
}