// Copyright 2019 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // ignore_for_file: implementation_imports import 'dart:async'; import 'dart:io' as io; // ignore: dart_io_import import 'package:build/build.dart'; import 'package:build_runner_core/build_runner_core.dart' as core; import 'package:build_runner_core/src/asset_graph/graph.dart'; import 'package:build_runner_core/src/asset_graph/node.dart'; import 'package:build_runner_core/src/generate/build_impl.dart'; import 'package:build_runner_core/src/generate/options.dart'; import 'package:glob/glob.dart'; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'package:watcher/watcher.dart'; import '../artifacts.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; import '../base/platform.dart'; import '../compile.dart'; import '../convert.dart'; import '../dart/package_map.dart'; import '../globals.dart'; import '../web/compile.dart'; import 'build_script.dart'; /// A build_runner specific implementation of the [WebCompilationProxy]. class BuildRunnerWebCompilationProxy extends WebCompilationProxy { BuildRunnerWebCompilationProxy(); core.PackageGraph _packageGraph; BuildImpl _builder; PackageUriMapper _packageUriMapper; @override Future<bool> initialize({ Directory projectDirectory, String testOutputDir, bool release = false, }) async { // Create the .dart_tool directory if it doesn't exist. projectDirectory.childDirectory('.dart_tool').createSync(); final Directory generatedDirectory = projectDirectory .childDirectory('.dart_tool') .childDirectory('build') .childDirectory('generated'); // Override the generated output directory so this does not conflict with // other build_runner output. core.overrideGeneratedOutputDirectory('flutter_web'); _packageUriMapper = PackageUriMapper( path.absolute('lib/main.dart'), PackageMap.globalPackagesPath, null, null); _packageGraph = core.PackageGraph.forPath(projectDirectory.path); final core.BuildEnvironment buildEnvironment = core.OverrideableEnvironment( core.IOEnvironment(_packageGraph), onLog: (LogRecord record) { if (record.level == Level.SEVERE || record.level == Level.SHOUT) { printError(record.message); } else { printTrace(record.message); } }, reader: MultirootFileBasedAssetReader(_packageGraph, generatedDirectory)); final LogSubscription logSubscription = LogSubscription( buildEnvironment, verbose: false, logLevel: Level.FINE, ); final BuildOptions buildOptions = await BuildOptions.create( logSubscription, packageGraph: _packageGraph, skipBuildScriptCheck: true, trackPerformance: false, deleteFilesByDefault: true, enableLowResourcesMode: platform.environment['FLUTTER_LOW_RESOURCE_MODE']?.toLowerCase() == 'true', ); final Set<core.BuildDirectory> buildDirs = <core.BuildDirectory>{ if (testOutputDir != null) core.BuildDirectory( 'test', outputLocation: core.OutputLocation( testOutputDir, useSymlinks: !platform.isWindows, ), ), }; core.BuildResult result; try { result = await _runBuilder( buildEnvironment, buildOptions, release, buildDirs, ); return result.status == core.BuildStatus.success; } on core.BuildConfigChangedException { await _cleanAssets(projectDirectory); result = await _runBuilder( buildEnvironment, buildOptions, release, buildDirs, ); return result.status == core.BuildStatus.success; } on core.BuildScriptChangedException { await _cleanAssets(projectDirectory); result = await _runBuilder( buildEnvironment, buildOptions, release, buildDirs, ); return result.status == core.BuildStatus.success; } } @override Future<bool> invalidate({@required List<Uri> inputs}) async { final Status status = logger.startProgress('Recompiling sources...', timeout: null); final Map<AssetId, ChangeType> updates = <AssetId, ChangeType>{}; for (Uri input in inputs) { final AssetId assetId = AssetId.resolve(_packageUriMapper.map(input.toFilePath()).toString()); updates[assetId] = ChangeType.MODIFY; } core.BuildResult result; try { result = await _builder.run(updates); } finally { status.cancel(); } return result.status == core.BuildStatus.success; } Future<core.BuildResult> _runBuilder(core.BuildEnvironment buildEnvironment, BuildOptions buildOptions, bool release, Set<core.BuildDirectory> buildDirs) async { _builder = await BuildImpl.create( buildOptions, buildEnvironment, builders, <String, Map<String, dynamic>>{ 'flutter_tools:ddc': <String, dynamic>{ 'flutterWebSdk': artifacts.getArtifactPath(Artifact.flutterWebSdk), }, 'flutter_tools:entrypoint': <String, dynamic>{ 'release': release, 'flutterWebSdk': artifacts.getArtifactPath(Artifact.flutterWebSdk), }, 'flutter_tools:test_entrypoint': <String, dynamic>{ 'release': release, }, }, isReleaseBuild: false, ); return _builder.run( const <AssetId, ChangeType>{}, buildDirs: buildDirs, ); } Future<void> _cleanAssets(Directory projectDirectory) async { final File assetGraphFile = fs.file(core.assetGraphPath); AssetGraph assetGraph; try { assetGraph = AssetGraph.deserialize(await assetGraphFile.readAsBytes()); } catch (_) { printTrace('Failed to clean up asset graph.'); } final core.PackageGraph packageGraph = core.PackageGraph.forThisPackage(); await _cleanUpSourceOutputs(assetGraph, packageGraph); final Directory cacheDirectory = fs.directory(fs.path.join( projectDirectory.path, '.dart_tool', 'build', 'flutter_web', )); if (assetGraphFile.existsSync()) { assetGraphFile.deleteSync(); } if (cacheDirectory.existsSync()) { cacheDirectory.deleteSync(recursive: true); } } Future<void> _cleanUpSourceOutputs(AssetGraph assetGraph, core.PackageGraph packageGraph) async { final core.FileBasedAssetWriter writer = core.FileBasedAssetWriter(packageGraph); if (assetGraph?.outputs == null) { return; } for (AssetId id in assetGraph.outputs) { if (id.package != packageGraph.root.name) { continue; } final GeneratedAssetNode node = assetGraph.get(id); if (node.wasOutput) { // Note that this does a file.exists check in the root package and // only tries to delete the file if it exists. This way we only // actually delete to_source outputs, without reading in the build // actions. await writer.delete(id); } } } } /// Handles mapping a single root file scheme to a multiroot scheme. /// /// This allows one build_runner build to read the output from a previous /// isolated build. class MultirootFileBasedAssetReader extends core.FileBasedAssetReader { MultirootFileBasedAssetReader( core.PackageGraph packageGraph, this.generatedDirectory, ) : super(packageGraph); final Directory generatedDirectory; @override Future<bool> canRead(AssetId id) { if (packageGraph[id.package] == packageGraph.root && _missingSource(id)) { return _generatedFile(id).exists(); } return super.canRead(id); } @override Future<List<int>> readAsBytes(AssetId id) { if (packageGraph[id.package] == packageGraph.root && _missingSource(id)) { return _generatedFile(id).readAsBytes(); } return super.readAsBytes(id); } @override Future<String> readAsString(AssetId id, {Encoding encoding}) { if (packageGraph[id.package] == packageGraph.root && _missingSource(id)) { return _generatedFile(id).readAsString(); } return super.readAsString(id, encoding: encoding); } @override Stream<AssetId> findAssets(Glob glob, {String package}) async* { if (package == null || packageGraph.root.name == package) { await for (io.FileSystemEntity entity in glob.list(followLinks: true, root: packageGraph.root.path)) { if (entity is io.File && _isNotHidden(entity)) { yield _fileToAssetId(entity, packageGraph.root); } } final String generatedRoot = fs.path.join( generatedDirectory.path, packageGraph.root.name ); if (!fs.isDirectorySync(generatedRoot)) { return; } await for (io.FileSystemEntity entity in glob.list(followLinks: true, root: generatedRoot)) { if (entity is io.File && _isNotHidden(entity)) { yield _fileToAssetId(entity, packageGraph.root, generatedRoot); } } return; } yield* super.findAssets(glob, package: package); } bool _isNotHidden(io.FileSystemEntity entity) { return !path.basename(entity.path).startsWith('._'); } bool _missingSource(AssetId id) { return !fs.file(path.joinAll(<String>[packageGraph.root.path, ...id.pathSegments])).existsSync(); } File _generatedFile(AssetId id) { return fs.file( path.joinAll(<String>[generatedDirectory.path, packageGraph.root.name, ...id.pathSegments]) ); } /// Creates an [AssetId] for [file], which is a part of [packageNode]. AssetId _fileToAssetId(io.File file, core.PackageNode packageNode, [String root]) { final String filePath = path.normalize(file.absolute.path); final String relativePath = path.relative(filePath, from: root ?? packageNode.path); return AssetId(packageNode.name, relativePath); } }