blob: 24ea4633cbb8c48fe9d302263665bb4a88fe2cd0 [file] [log] [blame]
// Copyright (c) 2018, 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:io';
import 'package:build/build.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
import 'package:pool/pool.dart';
import '../asset/reader.dart';
import '../environment/build_environment.dart';
import '../generate/build_directory.dart';
import '../generate/finalized_assets_view.dart';
import '../logging/logging.dart';
import '../package_graph/package_graph.dart';
/// Pool for async file operations, we don't want to use too many file handles.
final _descriptorPool = Pool(32);
final _logger = Logger('CreateOutputDir');
const _manifestName = '.build.manifest';
const _manifestSeparator = '\n';
/// Creates merged output directories for each [OutputLocation].
///
/// Returns whether it succeeded or not.
Future<bool> createMergedOutputDirectories(
Set<BuildDirectory> buildDirs,
PackageGraph packageGraph,
BuildEnvironment environment,
AssetReader reader,
FinalizedAssetsView finalizedAssetsView,
bool outputSymlinksOnly) async {
if (outputSymlinksOnly && reader is! PathProvidingAssetReader) {
_logger.severe(
'The current environment does not support symlinks, but symlinks were '
'requested.');
return false;
}
var conflictingOutputs = _conflicts(buildDirs);
if (conflictingOutputs.isNotEmpty) {
_logger.severe('Unable to create merged directory. '
'Conflicting outputs for $conflictingOutputs');
return false;
}
for (var target in buildDirs) {
var output = target.outputLocation?.path;
if (output != null) {
if (!await _createMergedOutputDir(
output,
target.directory,
packageGraph,
environment,
reader,
finalizedAssetsView,
// TODO(grouma) - retrieve symlink information from target only.
outputSymlinksOnly || target.outputLocation.useSymlinks,
target.outputLocation.hoist)) {
_logger.severe('Unable to create merged directory for $output.');
return false;
}
}
}
return true;
}
Set<String> _conflicts(Set<BuildDirectory> buildDirs) {
final seen = <String>{};
final conflicts = <String>{};
var outputLocations =
buildDirs.map((d) => d.outputLocation?.path).where((p) => p != null);
for (var location in outputLocations) {
if (!seen.add(location)) conflicts.add(location);
}
return conflicts;
}
Future<bool> _createMergedOutputDir(
String outputPath,
String root,
PackageGraph packageGraph,
BuildEnvironment environment,
AssetReader reader,
FinalizedAssetsView finalizedOutputsView,
bool symlinkOnly,
bool hoist) async {
try {
if (root == null) return false;
var outputDir = Directory(outputPath);
var outputDirExists = await outputDir.exists();
if (outputDirExists) {
if (!await _cleanUpOutputDir(outputDir, environment)) return false;
}
var builtAssets = finalizedOutputsView.allAssets(rootDir: root).toList();
if (root != '' &&
!builtAssets
.where((id) => id.package == packageGraph.root.name)
.any((id) => p.isWithin(root, id.path))) {
_logger.severe('No assets exist in $root, skipping output');
return false;
}
var outputAssets = <AssetId>[];
await logTimedAsync(_logger, 'Creating merged output dir `$outputPath`',
() async {
if (!outputDirExists) {
await outputDir.create(recursive: true);
}
outputAssets.addAll(await Future.wait(builtAssets.map((id) => _writeAsset(
id, outputDir, root, packageGraph, reader, symlinkOnly, hoist))));
var packagesFileContent = packageGraph.allPackages.keys
.map((p) => '$p:packages/$p/')
.join('\r\n');
var packagesAsset = AssetId(packageGraph.root.name, '.packages');
await _writeAsString(outputDir, packagesAsset, packagesFileContent);
outputAssets.add(packagesAsset);
if (!hoist) {
for (var dir in _findRootDirs(builtAssets, outputPath)) {
var link = Link(p.join(outputDir.path, dir, 'packages'));
if (!link.existsSync()) {
link.createSync(p.join('..', 'packages'), recursive: true);
}
}
}
});
await logTimedAsync(_logger, 'Writing asset manifest', () async {
var paths = outputAssets.map((id) => id.path).toList()..sort();
var content = paths.join(_manifestSeparator);
await _writeAsString(
outputDir, AssetId(packageGraph.root.name, _manifestName), content);
});
return true;
} on FileSystemException catch (e) {
if (e.osError?.errorCode != 1314) rethrow;
var devModeLink =
'https://docs.microsoft.com/en-us/windows/uwp/get-started/'
'enable-your-device-for-development';
_logger.severe('Unable to create symlink ${e.path}. Note that to create '
'symlinks on windows you need to either run in a console with admin '
'privileges or enable developer mode (see $devModeLink).');
return false;
}
}
Set<String> _findRootDirs(Iterable<AssetId> allAssets, String outputPath) {
var rootDirs = <String>{};
for (var id in allAssets) {
var parts = p.url.split(id.path);
if (parts.length == 1) continue;
var dir = parts.first;
if (dir == outputPath || dir == 'lib') continue;
rootDirs.add(parts.first);
}
return rootDirs;
}
Future<AssetId> _writeAsset(
AssetId id,
Directory outputDir,
String root,
PackageGraph packageGraph,
AssetReader reader,
bool symlinkOnly,
bool hoist) {
return _descriptorPool.withResource(() async {
String assetPath;
if (id.path.startsWith('lib/')) {
assetPath =
p.url.join('packages', id.package, id.path.substring('lib/'.length));
} else {
assetPath = id.path;
assert(id.package == packageGraph.root.name);
if (hoist && p.isWithin(root, id.path)) {
assetPath = p.relative(id.path, from: root);
}
}
var outputId = AssetId(packageGraph.root.name, assetPath);
try {
if (symlinkOnly) {
await Link(_filePathFor(outputDir, outputId)).create(
// We assert at the top of `createMergedOutputDirectories` that the
// reader implements this type when requesting symlinks.
(reader as PathProvidingAssetReader).pathTo(id),
recursive: true);
} else {
await _writeAsBytes(outputDir, outputId, await reader.readAsBytes(id));
}
} on AssetNotFoundException catch (e, __) {
if (p.basename(id.path).startsWith('.')) {
_logger.fine('Skipping missing hidden file ${id.path}');
} else {
_logger.severe(
'Missing asset ${e.assetId}, it may have been deleted during the '
'build. Please try rebuilding and if you continue to see the '
'error then file a bug at '
'https://github.com/dart-lang/build/issues/new.');
rethrow;
}
}
return outputId;
});
}
Future<void> _writeAsBytes(Directory outputDir, AssetId id, List<int> bytes) =>
_fileFor(outputDir, id).then((file) => file.writeAsBytes(bytes));
Future<void> _writeAsString(Directory outputDir, AssetId id, String contents) =>
_fileFor(outputDir, id).then((file) => file.writeAsString(contents));
Future<File> _fileFor(Directory outputDir, AssetId id) {
return File(_filePathFor(outputDir, id)).create(recursive: true);
}
String _filePathFor(Directory outputDir, AssetId id) {
String relativePath;
if (id.path.startsWith('lib')) {
relativePath =
p.join('packages', id.package, p.joinAll(p.url.split(id.path).skip(1)));
} else {
relativePath = id.path;
}
return p.join(outputDir.path, relativePath);
}
/// Checks for a manifest file in [outputDir] and deletes all referenced files.
///
/// Prompts the user with a few options if no manifest file is found.
///
/// Returns whether or not the directory was successfully cleaned up.
Future<bool> _cleanUpOutputDir(
Directory outputDir, BuildEnvironment environment) async {
var outputPath = outputDir.path;
var manifestFile = File(p.join(outputPath, _manifestName));
if (!manifestFile.existsSync()) {
if (outputDir.listSync(recursive: false).isNotEmpty) {
var choices = [
'Leave the directory unchanged and skip writing the build output',
'Delete the directory and all contents',
'Leave the directory in place and write over any existing files',
];
int choice;
try {
choice = await environment.prompt(
'Found existing directory `$outputPath` but no manifest file.\n'
'Please choose one of the following options:',
choices);
} on NonInteractiveBuildException catch (_) {
_logger.severe('Unable to create merged directory at $outputPath.\n'
'Choose a different directory or delete the contents of that '
'directory.');
return false;
}
switch (choice) {
case 0:
_logger.severe('Skipped creation of the merged output directory.');
return false;
case 1:
try {
outputDir.deleteSync(recursive: true);
} catch (e) {
_logger.severe(
'Failed to delete output dir at `$outputPath` with error:\n\n'
'$e');
return false;
}
// Actually recreate the directory, but as an empty one.
outputDir.createSync();
break;
case 2:
// Just do nothing here, we overwrite files by default.
break;
}
}
} else {
var previousOutputs = logTimedSync(
_logger,
'Reading manifest at ${manifestFile.path}',
() => manifestFile.readAsStringSync().split(_manifestSeparator));
logTimedSync(_logger, 'Deleting previous outputs in `$outputPath`', () {
for (var path in previousOutputs) {
var file = File(p.join(outputPath, path));
if (file.existsSync()) file.deleteSync();
}
_cleanEmptyDirectories(outputPath, previousOutputs);
});
}
return true;
}
/// Deletes all the directories which used to contain any path in
/// [removedFilePaths] if that directory is now empty.
void _cleanEmptyDirectories(
String outputPath, Iterable<String> removedFilePaths) {
for (var directory in removedFilePaths
.map((path) => p.join(outputPath, p.dirname(path)))
.toSet()) {
_deleteUp(directory, outputPath);
}
}
/// Deletes the directory at [from] and and any parent directories which are
/// subdirectories of [to] if they are empty.
void _deleteUp(String from, String to) {
var directoryPath = from;
while (p.isWithin(to, directoryPath)) {
var directory = Directory(directoryPath);
if (!directory.existsSync() || directory.listSync().isNotEmpty) return;
directory.deleteSync();
directoryPath = p.dirname(directoryPath);
}
}