blob: d467cb32604eee7a4bafaa4438afada9ba9b2d43 [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:io';
import 'package:build/build.dart';
import 'package:crypto/crypto.dart';
import 'package:path/path.dart' as p;
import 'package:pedantic/pedantic.dart';
import 'package:pool/pool.dart';
import 'util.dart';
/// Pool for async file writes, we don't want to use too many file handles.
final _descriptorPool = Pool(32);
/// An on-disk temporary environment for running executables that don't have
/// a standard Dart library API.
class ScratchSpace {
/// Whether or not this scratch space still exists.
bool exists = true;
/// The `packages` directory under the temp directory.
final Directory packagesDir;
/// The temp directory at the root of this [ScratchSpace].
final Directory tempDir;
// Assets which have a file created but are still being written to.
final _pendingWrites = <AssetId, Future<void>>{};
final _digests = <AssetId, Digest>{};
ScratchSpace._(this.tempDir)
: packagesDir = Directory(p.join(tempDir.path, 'packages'));
factory ScratchSpace() {
var tempDir = Directory(Directory.systemTemp
.createTempSync('scratch_space')
.resolveSymbolicLinksSync());
return ScratchSpace._(tempDir);
}
/// Copies [id] from the tmp dir and writes it back using the [writer].
///
/// Note that [BuildStep] implements [AssetWriter] and that is typically
/// what you will want to pass in.
///
/// This must be called for all outputs which you want to be included as a
/// part of the actual build (any other outputs will be deleted with the
/// tmp dir and won't be available to other [Builder]s).
///
/// If [requireContent] is true and the file is empty an
/// [EmptyOutputException] is thrown.
Future copyOutput(AssetId id, AssetWriter writer,
{bool requireContent = false}) async {
var file = fileFor(id);
var bytes = await _descriptorPool.withResource(file.readAsBytes);
if (requireContent && bytes.isEmpty) throw EmptyOutputException(id);
await writer.writeAsBytes(id, bytes);
}
/// Deletes the temp directory for this environment.
///
/// This class is no longer valid once the directory is deleted, you must
/// create a new [ScratchSpace].
Future delete() async {
if (!exists) {
throw StateError(
'Tried to delete a ScratchSpace which was already deleted');
}
exists = false;
_digests.clear();
if (_pendingWrites.isNotEmpty) {
try {
await Future.wait(_pendingWrites.values);
} catch (_) {
// Ignore any errors, we are essentially just draining this queue
// of pending writes but don't care about the result.
}
}
return tempDir.delete(recursive: true);
}
/// Copies [assetIds] to [tempDir] if they don't exist, using [reader] to
/// read assets and mark dependencies.
///
/// Note that [BuildStep] implements [AssetReader] and that is typically
/// what you will want to pass in.
///
/// Any asset that is under a `lib` dir will be output under a `packages`
/// directory corresponding to its package, and any other assets are output
/// directly under the temp dir using their unmodified path.
Future ensureAssets(Iterable<AssetId> assetIds, AssetReader reader) {
if (!exists) {
throw StateError('Tried to use a deleted ScratchSpace!');
}
var futures = assetIds.map((id) async {
var digest = await reader.digest(id);
var existing = _digests[id];
if (digest == existing) {
await _pendingWrites[id];
return;
}
_digests[id] = digest;
try {
await _pendingWrites.putIfAbsent(
id,
() => _descriptorPool.withResource(() async {
var file = fileFor(id);
if (await file.exists()) {
await file.delete();
}
await file.create(recursive: true);
await file.writeAsBytes(await reader.readAsBytes(id));
}));
} finally {
unawaited(_pendingWrites.remove(id));
}
}).toList();
return Future.wait(futures);
}
/// Returns the actual [File] in this environment corresponding to [id].
///
/// The returned [File] may or may not already exist. Call [ensureAssets]
/// with [id] to make sure it is actually present.
File fileFor(AssetId id) =>
File(p.join(tempDir.path, p.normalize(_relativePathFor(id))));
}
/// Returns a canonical uri for [id].
///
/// If [id] is under a `lib` directory then this returns a `package:` uri,
/// otherwise it just returns [id#path].
String canonicalUriFor(AssetId id) {
if (topLevelDir(id.path) == 'lib') {
var packagePath =
p.url.join(id.package, p.url.joinAll(p.url.split(id.path).skip(1)));
return 'package:$packagePath';
} else {
return id.path;
}
}
/// The path relative to the root of the environment for a given [id].
String _relativePathFor(AssetId id) =>
canonicalUriFor(id).replaceFirst('package:', 'packages/');
/// An indication that an output file which was expect to be non-empty had no
/// content.
class EmptyOutputException implements Exception {
final AssetId id;
EmptyOutputException(this.id);
}