blob: 5e9a210643b0bf6c215289912e94f86525e3f1f8 [file] [log] [blame]
// Copyright (c) 2013, 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.
/// Helper functionality to make working with IO easier.
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:async/async.dart';
import 'package:cli_util/cli_util.dart'
show EnvironmentNotFoundException, applicationConfigHome;
import 'package:http/http.dart' show ByteStream;
import 'package:http_multi_server/http_multi_server.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:pedantic/pedantic.dart';
import 'package:pool/pool.dart';
import 'package:stack_trace/stack_trace.dart';
import 'error_group.dart';
import 'exceptions.dart';
import 'exit_codes.dart' as exit_codes;
import 'log.dart' as log;
import 'third_party/tar/tar.dart';
import 'utils.dart';
export 'package:http/http.dart' show ByteStream;
/// The pool used for restricting access to asynchronous operations that consume
/// file descriptors.
///
/// The maximum number of allocated descriptors is based on empirical tests that
/// indicate that beyond 32, additional file reads don't provide substantial
/// additional throughput.
final _descriptorPool = Pool(32);
/// The assumed default file mode on Linux and macOS
const _defaultMode = 420; // 644₈
/// Mask for executable bits in file modes.
const _executableMask = 0x49; // 001 001 001
/// Determines if a file or directory exists at [path].
bool entryExists(String path) =>
dirExists(path) || fileExists(path) || linkExists(path);
/// Returns whether [link] exists on the file system.
///
/// This returns `true` for any symlink, regardless of what it points at or
/// whether it's broken.
bool linkExists(String link) => Link(link).existsSync();
/// Returns whether [file] exists on the file system.
///
/// This returns `true` for a symlink only if that symlink is unbroken and
/// points to a file.
bool fileExists(String file) => File(file).existsSync();
/// Returns the canonical path for [pathString].
///
/// This is the normalized, absolute path, with symlinks resolved. As in
/// [transitiveTarget], broken or recursive symlinks will not be fully resolved.
///
/// This doesn't require [pathString] to point to a path that exists on the
/// filesystem; nonexistent or unreadable path entries are treated as normal
/// directories.
String canonicalize(String pathString) {
var seen = <String>{};
var components =
Queue<String>.from(path.split(path.normalize(path.absolute(pathString))));
// The canonical path, built incrementally as we iterate through [components].
var newPath = components.removeFirst();
// Move through the components of the path, resolving each one's symlinks as
// necessary. A resolved component may also add new components that need to be
// resolved in turn.
while (components.isNotEmpty) {
seen.add(path.join(newPath, path.joinAll(components)));
var resolvedPath =
_resolveLink(path.join(newPath, components.removeFirst()));
var relative = path.relative(resolvedPath, from: newPath);
// If the resolved path of the component relative to `newPath` is just ".",
// that means component was a symlink pointing to its parent directory. We
// can safely ignore such components.
if (relative == '.') continue;
var relativeComponents = Queue<String>.from(path.split(relative));
// If the resolved path is absolute relative to `newPath`, that means it's
// on a different drive. We need to canonicalize the entire target of that
// symlink again.
if (path.isAbsolute(relative)) {
// If we've already tried to canonicalize the new path, we've encountered
// a symlink loop. Avoid going infinite by treating the recursive symlink
// as the canonical path.
if (seen.contains(relative)) {
newPath = relative;
} else {
newPath = relativeComponents.removeFirst();
relativeComponents.addAll(components);
components = relativeComponents;
}
continue;
}
// Pop directories off `newPath` if the component links upwards in the
// directory hierarchy.
while (relativeComponents.first == '..') {
newPath = path.dirname(newPath);
relativeComponents.removeFirst();
}
// If there's only one component left, [resolveLink] guarantees that it's
// not a link (or is a broken link). We can just add it to `newPath` and
// continue resolving the remaining components.
if (relativeComponents.length == 1) {
newPath = path.join(newPath, relativeComponents.single);
continue;
}
// If we've already tried to canonicalize the new path, we've encountered a
// symlink loop. Avoid going infinite by treating the recursive symlink as
// the canonical path.
var newSubPath = path.join(newPath, path.joinAll(relativeComponents));
if (seen.contains(newSubPath)) {
newPath = newSubPath;
continue;
}
// If there are multiple new components to resolve, add them to the
// beginning of the queue.
relativeComponents.addAll(components);
components = relativeComponents;
}
return newPath;
}
/// Returns the transitive target of [link] (if A links to B which links to C,
/// this will return C).
///
/// If [link] is part of a symlink loop (e.g. A links to B which links back to
/// A), this returns the path to the first repeated link (so
/// `transitiveTarget("A")` would return `"A"` and `transitiveTarget("A")` would
/// return `"B"`).
///
/// This accepts paths to non-links or broken links, and returns them as-is.
String _resolveLink(String link) {
var seen = <String>{};
while (linkExists(link) && seen.add(link)) {
link =
path.normalize(path.join(path.dirname(link), Link(link).targetSync()));
}
return link;
}
/// Reads the contents of the text file [file].
String readTextFile(String file) => File(file).readAsStringSync();
/// Reads the contents of the text file [file].
Future<String> readTextFileAsync(String file) {
return _descriptorPool.withResource(() => File(file).readAsString());
}
/// Reads the contents of the binary file [file].
List<int> readBinaryFile(String file) {
log.io('Reading binary file $file.');
var contents = File(file).readAsBytesSync();
log.io('Read ${contents.length} bytes from $file.');
return contents;
}
/// Reads the contents of the binary file [file] as a [Stream].
Stream<List<int>> readBinaryFileAsSream(String file) {
log.io('Reading binary file $file.');
var contents = File(file).openRead();
return contents;
}
/// Creates [file] and writes [contents] to it.
///
/// If [dontLogContents] is `true`, the contents of the file will never be
/// logged.
void writeTextFile(
String file,
String contents, {
bool dontLogContents = false,
Encoding encoding = utf8,
}) {
// Sanity check: don't spew a huge file.
log.io('Writing ${contents.length} characters to text file $file.');
if (!dontLogContents && contents.length < 1024 * 1024) {
log.fine('Contents:\n$contents');
}
deleteIfLink(file);
File(file).writeAsStringSync(contents, encoding: encoding);
}
/// Creates [file] and writes [contents] to it.
///
/// If [dontLogContents] is `true`, the contents of the file will never be
/// logged.
Future<void> writeTextFileAsync(
String file,
String contents, {
bool dontLogContents = false,
Encoding encoding = utf8,
}) async {
// Sanity check: don't spew a huge file.
log.io('Writing ${contents.length} characters to text file $file.');
if (!dontLogContents && contents.length < 1024 * 1024) {
log.fine('Contents:\n$contents');
}
deleteIfLink(file);
await File(file).writeAsString(contents, encoding: encoding);
}
/// Writes [stream] to a new file at path [file].
///
/// Replaces any file already at that path. Completes when the file is done
/// being written.
Future<String> createFileFromStream(Stream<List<int>> stream, String file) {
// TODO(nweiz): remove extra logging when we figure out the windows bot issue.
log.io('Creating $file from stream.');
return _descriptorPool.withResource(() async {
deleteIfLink(file);
await stream.pipe(File(file).openWrite());
log.fine('Created $file from stream.');
return file;
});
}
void _chmod(int mode, String file) {
runProcessSync('chmod', [mode.toRadixString(8), file]);
}
/// Deletes [file] if it's a symlink.
///
/// The [File] class overwrites the symlink targets when writing to a file,
/// which is never what we want, so this delete the symlink first if necessary.
void deleteIfLink(String file) {
if (!linkExists(file)) return;
log.io('Deleting symlink at $file.');
Link(file).deleteSync();
}
/// Ensures that [dir] and all its parent directories exist.
///
/// If they don't exist, creates them.
String ensureDir(String dir) {
Directory(dir).createSync(recursive: true);
return dir;
}
/// Creates a temp directory in [dir], whose name will be [prefix] with
/// characters appended to it to make a unique name.
///
/// Returns the path of the created directory.
String createTempDir(String base, String prefix) {
var tempDir = Directory(base).createTempSync(prefix);
log.io('Created temp directory ${tempDir.path}');
return tempDir.path;
}
/// Creates a temp directory in the system temp directory, whose name will be
/// 'pub_' with characters appended to it to make a unique name.
///
/// Returns the path of the created directory.
Future<String> _createSystemTempDir() async {
var tempDir = await Directory.systemTemp.createTemp('pub_');
log.io('Created temp directory ${tempDir.path}');
return tempDir.resolveSymbolicLinksSync();
}
String resolveSymlinksOfDir(String dir) {
return Directory(dir).resolveSymbolicLinksSync();
}
/// Lists the contents of [dir].
///
/// If [recursive] is `true`, lists subdirectory contents (defaults to `false`).
/// If [includeHidden] is `true`, includes files and directories beginning with
/// `.` (defaults to `false`). If [includeDirs] is `true`, includes directories
/// as well as files (defaults to `true`).
///
/// [allowed] is a list of hidden filenames to include even when
/// [includeHidden] is `false`.
///
/// Note that dart:io handles recursive symlinks in an unfortunate way. You
/// end up with two copies of every entity that is within the recursive loop.
/// We originally had our own directory list code that addressed that, but it
/// had a noticeable performance impact. In the interest of speed, we'll just
/// live with that annoying behavior.
///
/// The returned paths are guaranteed to begin with [dir]. Broken symlinks won't
/// be returned.
List<String> listDir(String dir,
{bool recursive = false,
bool includeHidden = false,
bool includeDirs = true,
Iterable<String> allowed = const <String>[]}) {
var allowlistFilter = createFileFilter(allowed);
// This is used in some performance-sensitive paths and can list many, many
// files. As such, it leans more heavily towards optimization as opposed to
// readability than most code in pub. In particular, it avoids using the path
// package, since re-parsing a path is very expensive relative to string
// operations.
return Directory(dir)
.listSync(recursive: recursive)
.where((entity) {
if (!includeDirs && entity is Directory) return false;
if (entity is Link) return false;
if (includeHidden) return true;
// Using substring here is generally problematic in cases where dir has one
// or more trailing slashes. If you do listDir("foo"), you'll get back
// paths like "foo/bar". If you do listDir("foo/"), you'll get "foo/bar"
// (note the trailing slash was dropped. If you do listDir("foo//"), you'll
// get "foo//bar".
//
// This means if you strip off the prefix, the resulting string may have a
// leading separator (if the prefix did not have a trailing one) or it may
// not. However, since we are only using the results of that to call
// contains() on, the leading separator is harmless.
assert(entity.path.startsWith(dir));
var pathInDir = entity.path.substring(dir.length);
// If the basename is in [allowed], don't count its "/." as making the
// file hidden.
if (allowlistFilter.any(pathInDir.contains)) {
final allowedBasename =
allowlistFilter.firstWhere(pathInDir.contains);
pathInDir =
pathInDir.substring(0, pathInDir.length - allowedBasename.length);
}
if (pathInDir.contains('/.')) return false;
if (!Platform.isWindows) return true;
return !pathInDir.contains('\\.');
})
.map((entity) => entity.path)
.toList();
}
/// Returns whether [dir] exists on the file system.
///
/// This returns `true` for a symlink only if that symlink is unbroken and
/// points to a directory.
bool dirExists(String dir) => Directory(dir).existsSync();
/// Tries to resiliently perform [operation].
///
/// Some file system operations can intermittently fail on Windows because
/// other processes are locking a file. We've seen this with virus scanners
/// when we try to delete or move something while it's being scanned. To
/// mitigate that, on Windows, this will retry the operation a few times if it
/// fails.
void _attempt(String description, void Function() operation) {
if (!Platform.isWindows) {
operation();
return;
}
String? getErrorReason(FileSystemException error) {
if (error.osError?.errorCode == 5) {
return 'access was denied';
}
if (error.osError?.errorCode == 32) {
return 'it was in use by another process';
}
if (error.osError?.errorCode == 145) {
return 'of dart-lang/sdk#25353';
}
return null;
}
for (var i = 0; i < 2; i++) {
try {
operation();
return;
} on FileSystemException catch (error) {
var reason = getErrorReason(error);
if (reason == null) rethrow;
log.io('Pub failed to $description because $reason. '
'Retrying in 50ms.');
sleep(Duration(milliseconds: 50));
}
}
try {
operation();
} on FileSystemException catch (error) {
var reason = getErrorReason(error);
if (reason == null) rethrow;
fail('Pub failed to $description because $reason.\n'
'This may be caused by a virus scanner or having a file\n'
'in the directory open in another application.');
}
}
/// Deletes whatever's at [path], whether it's a file, directory, or symlink.
///
/// If it's a directory, it will be deleted recursively.
void deleteEntry(String path) {
_attempt('delete entry', () {
if (linkExists(path)) {
log.io('Deleting link $path.');
Link(path).deleteSync();
} else if (dirExists(path)) {
log.io('Deleting directory $path.');
Directory(path).deleteSync(recursive: true);
} else if (fileExists(path)) {
log.io('Deleting file $path.');
File(path).deleteSync();
}
});
}
/// Attempts to delete whatever's at [path], but doesn't throw an exception if
/// the deletion fails.
void tryDeleteEntry(String path) {
try {
deleteEntry(path);
} catch (error, stackTrace) {
log.fine('Pub failed to delete $path: $error\n'
'${Chain.forTrace(stackTrace)}');
}
}
/// "Cleans" [dir].
///
/// If that directory already exists, it is deleted. Then a new empty directory
/// is created.
void cleanDir(String dir) {
if (entryExists(dir)) deleteEntry(dir);
ensureDir(dir);
}
/// Renames (i.e. moves) the directory [from] to [to].
void renameDir(String from, String to) {
_attempt('rename directory', () {
log.io('Renaming directory $from to $to.');
try {
Directory(from).renameSync(to);
} on IOException {
// Ensure that [to] isn't left in an inconsistent state. See issue 12436.
if (entryExists(to)) deleteEntry(to);
rethrow;
}
});
}
/// Creates a new symlink at path [symlink] that points to [target].
///
/// Returns a [Future] which completes to the path to the symlink file.
///
/// If [relative] is true, creates a symlink with a relative path from the
/// symlink to the target. Otherwise, uses the [target] path unmodified.
///
/// Note that on Windows, only directories may be symlinked to.
void createSymlink(String target, String symlink, {bool relative = false}) {
if (relative) {
// Relative junction points are not supported on Windows. Instead, just
// make sure we have a clean absolute path because it will interpret a
// relative path to be relative to the cwd, not the symlink, and will be
// confused by forward slashes.
if (Platform.isWindows) {
target = path.normalize(path.absolute(target));
} else {
// If the directory where we're creating the symlink was itself reached
// by traversing a symlink, we want the relative path to be relative to
// it's actual location, not the one we went through to get to it.
var symlinkDir = canonicalize(path.dirname(symlink));
target = path.normalize(path.relative(target, from: symlinkDir));
}
}
log.fine('Creating $symlink pointing to $target');
Link(symlink).createSync(target);
}
/// Creates a new symlink that creates an alias at [symlink] that points to the
/// `lib` directory of package [target].
///
/// If [target] does not have a `lib` directory, this shows a warning if
/// appropriate and then does nothing.
///
/// If [relative] is true, creates a symlink with a relative path from the
/// symlink to the target. Otherwise, uses the [target] path unmodified.
void createPackageSymlink(String name, String target, String symlink,
{bool isSelfLink = false, bool relative = false}) {
// See if the package has a "lib" directory. If not, there's nothing to
// symlink to.
target = path.join(target, 'lib');
if (!dirExists(target)) return;
log.fine("Creating ${isSelfLink ? "self" : ""}link for package '$name'.");
createSymlink(target, symlink, relative: relative);
}
/// Whether the current process is a pub subprocess being run from a test.
///
/// The "_PUB_TESTING" variable is automatically set for all the test code's
/// invocations of pub.
final bool runningFromTest =
Platform.environment.containsKey('_PUB_TESTING') && _assertionsEnabled;
final bool _assertionsEnabled = () {
try {
assert(false);
// ignore: avoid_catching_errors
} on AssertionError {
return true;
}
return false;
}();
final bool runningFromFlutter =
Platform.environment.containsKey('PUB_ENVIRONMENT') &&
(Platform.environment['PUB_ENVIRONMENT'] ?? '').contains('flutter_cli');
/// A regular expression to match the script path of a pub script running from
/// source in the Dart repo.
final _dartRepoRegExp = RegExp(r'/third_party/pkg/pub/('
r'bin/pub\.dart'
r'|'
r'test/.*_test\.dart'
r')$');
/// Whether pub is running from source in the Dart repo.
///
/// This can happen when running tests against the repo, as well as when
/// building Observatory.
final bool runningFromDartRepo = Platform.script.path.contains(_dartRepoRegExp);
/// The path to the root of the Dart repo.
///
/// This throws a [StateError] if it's called when not running pub from source
/// in the Dart repo.
final String dartRepoRoot = (() {
if (!runningFromDartRepo) {
throw StateError('Not running from source in the Dart repo.');
}
// Get the URL of the repo root in a way that works when either both running
// as a test or as a pub executable.
var url = Platform.script
.replace(path: Platform.script.path.replaceAll(_dartRepoRegExp, ''));
return path.fromUri(url);
})();
/// A line-by-line stream of standard input.
final StreamQueue<String> _stdinLines = StreamQueue(
ByteStream(stdin).toStringStream().transform(const LineSplitter()));
/// Displays a message and reads a yes/no confirmation from the user.
///
/// Returns a [Future] that completes to `true` if the user confirms or `false`
/// if they do not.
///
/// This will automatically append " (y/N)?" to the message, so [message]
/// should just be a fragment like, "Are you sure you want to proceed". The
/// default for an empty response, or any response not starting with `y` or `Y`
/// is false.
Future<bool> confirm(String message) {
log.fine('Showing confirm message: $message');
return stdinPrompt('$message (y/N)?').then(RegExp(r'^[yY]').hasMatch);
}
/// Writes [prompt] and reads a line from stdin.
Future<String> stdinPrompt(String prompt, {bool? echoMode}) async {
if (runningFromTest) {
log.message(prompt);
} else {
stdout.write('$prompt ');
}
if (echoMode != null && stdin.hasTerminal) {
final previousEchoMode = stdin.echoMode;
try {
stdin.echoMode = echoMode;
final result = await _stdinLines.next;
stdout.write('\n');
return result;
} finally {
stdin.echoMode = previousEchoMode;
}
} else {
return await _stdinLines.next;
}
}
/// Flushes the stdout and stderr streams, then exits the program with the given
/// status code.
///
/// This returns a Future that will never complete, since the program will have
/// exited already. This is useful to prevent Future chains from proceeding
/// after you've decided to exit.
Future flushThenExit(int status) {
return Future.wait([stdout.close(), stderr.close()])
.then((_) => exit(status));
}
/// Returns a [EventSink] that pipes all data to [consumer] and a [Future] that
/// will succeed when [EventSink] is closed or fail with any errors that occur
/// while writing.
Pair<EventSink<T>, Future> _consumerToSink<T>(StreamConsumer<T> consumer) {
var controller = StreamController<T>(sync: true);
var done = controller.stream.pipe(consumer);
return Pair(controller.sink, done);
}
/// Spawns and runs the process located at [executable], passing in [args].
///
/// Returns a [Future] that will complete with the results of the process after
/// it has ended.
///
/// The spawned process will inherit its parent's environment variables. If
/// [environment] is provided, that will be used to augment (not replace) the
/// the inherited variables.
Future<PubProcessResult> runProcess(
String executable,
List<String> args, {
workingDir,
Map<String, String>? environment,
bool runInShell = false,
}) {
ArgumentError.checkNotNull(executable, 'executable');
return _descriptorPool.withResource(() async {
ProcessResult result;
try {
result = await _doProcess(Process.run, executable, args,
workingDir: workingDir,
environment: environment,
runInShell: runInShell);
} on IOException catch (e) {
throw RunProcessException(
'Pub failed to run subprocess `$executable`: $e');
}
var pubResult =
PubProcessResult(result.stdout, result.stderr, result.exitCode);
log.processResult(executable, pubResult);
return pubResult;
});
}
/// Spawns the process located at [executable], passing in [args].
///
/// Returns a [Future] that will complete with the [Process] once it's been
/// started.
///
/// The spawned process will inherit its parent's environment variables. If
/// [environment] is provided, that will be used to augment (not replace) the
/// the inherited variables.
@visibleForTesting
Future<_PubProcess> startProcess(
String executable,
List<String> args, {
String? workingDir,
Map<String, String>? environment,
bool runInShell = false,
}) {
return _descriptorPool.request().then((resource) async {
Process ioProcess;
try {
ioProcess = await _doProcess(Process.start, executable, args,
workingDir: workingDir,
environment: environment,
runInShell: runInShell);
} on IOException catch (e) {
throw RunProcessException(
'Pub failed to run subprocess `$executable`: $e');
}
var process = _PubProcess(ioProcess);
unawaited(process.exitCode.whenComplete(resource.release));
return process;
});
}
/// Like [runProcess], but synchronous.
PubProcessResult runProcessSync(
String executable,
List<String> args, {
String? workingDir,
Map<String, String>? environment,
bool runInShell = false,
}) {
ArgumentError.checkNotNull(executable, 'executable');
ProcessResult result;
try {
result = _doProcess(Process.runSync, executable, args,
workingDir: workingDir,
environment: environment,
runInShell: runInShell);
} on IOException catch (e) {
throw RunProcessException('Pub failed to run subprocess `$executable`: $e');
}
var pubResult =
PubProcessResult(result.stdout, result.stderr, result.exitCode);
log.processResult(executable, pubResult);
return pubResult;
}
/// A wrapper around [Process] that exposes `dart:async`-style APIs.
class _PubProcess {
/// The underlying `dart:io` [Process].
final Process _process;
/// The mutable field for [stdin].
late EventSink<List<int>> _stdin;
/// The mutable field for [stdinClosed].
late Future _stdinClosed;
/// The mutable field for [stdout].
late ByteStream _stdout;
/// The mutable field for [stderr].
late ByteStream _stderr;
/// The mutable field for [exitCode].
late Future<int> _exitCode;
/// The sink used for passing data to the process's standard input stream.
///
/// Errors on this stream are surfaced through [stdinClosed], [stdout],
/// [stderr], and [exitCode], which are all members of an [ErrorGroup].
EventSink<List<int>> get stdin => _stdin;
// TODO(nweiz): write some more sophisticated Future machinery so that this
// doesn't surface errors from the other streams/futures, but still passes its
// unhandled errors to them. Right now it's impossible to recover from a stdin
// error and continue interacting with the process.
/// A [Future] that completes when [stdin] is closed, either by the user or by
/// the process itself.
///
/// This is in an [ErrorGroup] with [stdout], [stderr], and [exitCode], so any
/// error in process will be passed to it, but won't reach the top-level error
/// handler unless nothing has handled it.
Future get stdinClosed => _stdinClosed;
/// The process's standard output stream.
///
/// This is in an [ErrorGroup] with [stdinClosed], [stderr], and [exitCode],
/// so any error in process will be passed to it, but won't reach the
/// top-level error handler unless nothing has handled it.
ByteStream get stdout => _stdout;
/// The process's standard error stream.
///
/// This is in an [ErrorGroup] with [stdinClosed], [stdout], and [exitCode],
/// so any error in process will be passed to it, but won't reach the
/// top-level error handler unless nothing has handled it.
ByteStream get stderr => _stderr;
/// A [Future] that will complete to the process's exit code once the process
/// has finished running.
///
/// This is in an [ErrorGroup] with [stdinClosed], [stdout], and [stderr], so
/// any error in process will be passed to it, but won't reach the top-level
/// error handler unless nothing has handled it.
Future<int> get exitCode => _exitCode;
/// Creates a new [_PubProcess] wrapping [process].
_PubProcess(Process process) : _process = process {
var errorGroup = ErrorGroup();
var pair = _consumerToSink(process.stdin);
_stdin = pair.first;
_stdinClosed = errorGroup.registerFuture(pair.last);
_stdout = ByteStream(errorGroup.registerStream(process.stdout));
_stderr = ByteStream(errorGroup.registerStream(process.stderr));
var exitCodeCompleter = Completer<int>();
_exitCode = errorGroup.registerFuture(exitCodeCompleter.future);
_process.exitCode.then((code) => exitCodeCompleter.complete(code));
}
/// Sends [signal] to the underlying process.
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) =>
_process.kill(signal);
}
/// Calls [fn] with appropriately modified arguments.
///
/// [fn] should have the same signature as [Process.start], except that the
/// returned value may have any return type.
T _doProcess<T>(
T Function(
String,
List<String>, {
String? workingDirectory,
Map<String, String>? environment,
bool runInShell,
})
fn,
String executable,
List<String> args, {
String? workingDir,
Map<String, String>? environment,
bool runInShell = false,
}) {
// TODO(rnystrom): Should dart:io just handle this?
// Spawning a process on Windows will not look for the executable in the
// system path. So, if executable looks like it needs that (i.e. it doesn't
// have any path separators in it), then spawn it through a shell.
if (Platform.isWindows && !executable.contains('\\')) {
args = ['/c', executable, ...args];
executable = 'cmd';
}
log.process(executable, args, workingDir ?? '.');
return fn(executable, args,
workingDirectory: workingDir,
environment: environment,
runInShell: runInShell);
}
/// Updates [path]'s modification time.
void touch(String path) => File(path).setLastModifiedSync(DateTime.now());
/// Creates a temporary directory and passes its path to [fn].
///
/// Once the [Future] returned by [fn] completes, the temporary directory and
/// all its contents are deleted. [fn] can also return `null`, in which case
/// the temporary directory is deleted immediately afterwards.
///
/// Returns a future that completes to the value that the future returned from
/// [fn] completes to.
Future<T> withTempDir<T>(FutureOr<T> Function(String path) fn) async {
var tempDir = await _createSystemTempDir();
try {
return await fn(tempDir);
} finally {
deleteEntry(tempDir);
}
}
/// Binds an [HttpServer] to [host] and [port].
///
/// If [host] is "localhost", this will automatically listen on both the IPv4
/// and IPv6 loopback addresses.
Future<HttpServer> bindServer(String host, int port) async {
var server = host == 'localhost'
? await HttpMultiServer.loopback(port)
: await HttpServer.bind(host, port);
server.autoCompress = true;
return server;
}
/// Extracts a `.tar.gz` file from [stream] to [destination].
Future extractTarGz(Stream<List<int>> stream, String destination) async {
log.fine('Extracting .tar.gz stream to $destination.');
destination = path.absolute(destination);
final reader = TarReader(stream.transform(gzip.decoder));
while (await reader.moveNext()) {
final entry = reader.current;
final filePath = path.joinAll([
destination,
// Tar file names always use forward slashes
...path.posix.split(entry.name),
]);
if (!path.isWithin(destination, filePath)) {
// The tar contains entries that would be written outside of the
// destination. That doesn't happen by accident, assume that the tar file
// is malicious.
await reader.cancel();
throw FormatException('Invalid tar entry: ${entry.name}');
}
final parentDirectory = path.dirname(filePath);
bool checkValidTarget(String linkTarget) {
final isValid = path.isWithin(destination, linkTarget);
if (!isValid) {
log.fine('Skipping ${entry.name}: Invalid link target');
}
return isValid;
}
switch (entry.type) {
case TypeFlag.dir:
ensureDir(filePath);
break;
case TypeFlag.reg:
case TypeFlag.regA:
// Regular file
deleteIfLink(filePath);
ensureDir(parentDirectory);
await createFileFromStream(entry.contents, filePath);
if (Platform.isLinux || Platform.isMacOS) {
// Apply executable bits from tar header, but don't change r/w bits
// from the default
final mode = _defaultMode | (entry.header.mode & _executableMask);
if (mode != _defaultMode) {
_chmod(mode, filePath);
}
}
break;
case TypeFlag.symlink:
// Link to another file in this tar, relative from this entry.
final resolvedTarget = path.joinAll(
[parentDirectory, ...path.posix.split(entry.header.linkName!)]);
if (!checkValidTarget(resolvedTarget)) {
// Don't allow links to files outside of this tar.
break;
}
ensureDir(parentDirectory);
createSymlink(
path.relative(resolvedTarget, from: parentDirectory), filePath);
break;
case TypeFlag.link:
// We generate hardlinks as symlinks too, but their linkName is relative
// to the root of the tar file (unlike symlink entries, whose linkName
// is relative to the entry itself).
final fromDestination = path.join(destination, entry.header.linkName);
if (!checkValidTarget(fromDestination)) {
break; // Link points outside of the tar file.
}
final fromFile = path.relative(fromDestination, from: parentDirectory);
ensureDir(parentDirectory);
createSymlink(fromFile, filePath);
break;
default:
// Only extract files
continue;
}
}
log.fine('Extracted .tar.gz to $destination.');
}
/// Create a .tar.gz archive from a list of entries.
///
/// Each entry can be a [String], [Directory], or [File] object. The root of
/// the archive is considered to be [baseDir], which defaults to the current
/// working directory.
///
/// Returns a [ByteStream] that emits the contents of the archive.
ByteStream createTarGz(
List<String> contents, {
required String baseDir,
}) {
var buffer = StringBuffer();
buffer.write('Creating .tar.gz stream containing:\n');
contents.forEach(buffer.writeln);
log.fine(buffer.toString());
ArgumentError.checkNotNull(baseDir, 'baseDir');
baseDir = path.absolute(baseDir);
final tarContents = Stream.fromIterable(contents.map((entry) {
entry = path.absolute(entry);
if (!path.isWithin(baseDir, entry)) {
throw ArgumentError('Entry $entry is not inside $baseDir.');
}
final relative = path.relative(entry, from: baseDir);
// On Windows, we can't open some files without normalizing them
final file = File(path.normalize(entry));
final stat = file.statSync();
if (stat.type == FileSystemEntityType.link) {
log.message('$entry is a link locally, but will be uploaded as a '
'duplicate file.');
}
return TarEntry(
TarHeader(
// Ensure paths in tar files use forward slashes
name: path.url.joinAll(path.split(relative)),
// We want to keep executable bits, but otherwise use the default
// file mode
mode: _defaultMode | (stat.mode & _executableMask),
size: stat.size,
modified: stat.changed,
userName: 'pub',
groupName: 'pub',
),
file.openRead(),
);
}));
return ByteStream(tarContents
.transform(tarWriterWith(format: OutputFormat.gnuLongName))
.transform(gzip.encoder));
}
/// Contains the results of invoking a [Process] and waiting for it to complete.
class PubProcessResult {
final List<String> stdout;
final List<String> stderr;
final int exitCode;
PubProcessResult(String stdout, String stderr, this.exitCode)
: stdout = _toLines(stdout),
stderr = _toLines(stderr);
// TODO(rnystrom): Remove this and change to returning one string.
static List<String> _toLines(String output) {
var lines = splitLines(output);
if (lines.isNotEmpty && lines.last == '') lines.removeLast();
return lines;
}
bool get success => exitCode == exit_codes.SUCCESS;
}
/// The location for dart-specific configuration.
///
/// `null` if no config dir could be found.
final String? dartConfigDir = () {
if (runningFromTest &&
Platform.environment.containsKey('_PUB_TEST_CONFIG_DIR')) {
return Platform.environment['_PUB_TEST_CONFIG_DIR'];
}
try {
return applicationConfigHome('dart');
} on EnvironmentNotFoundException {
return null;
}
}();
/// Escape [x] for users to copy-paste in bash.
///
/// If x is alphanumeric we leave it as is.
///
/// Otherwise, wrap with single quotation, and use '\'' to insert single quote.
String protectArgument(String x) =>
RegExp(r'^[a-zA-Z0-9-_]+$').stringMatch(x) == null
? "'${x.replaceAll("'", r"'\''")}'"
: x;