blob: e89f36eb26949270cb35da0dee14507cd4472fac [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.
//
// @dart=2.7
import 'dart:io';
import 'package:io/io.dart';
import 'package:path/path.dart' as p;
import 'package:source_span/source_span.dart';
import 'package:yaml/yaml.dart';
/// User-provided settings for invoking an executable.
class ExecutableSettings {
/// Additional arguments to pass to the executable.
final List<String> arguments;
/// The path to the executable on Linux.
///
/// This may be an absolute path or a basename, in which case it will be
/// looked up on the system path. It may not be relative.
final String _linuxExecutable;
/// The path to the executable on Mac OS.
///
/// This may be an absolute path or a basename, in which case it will be
/// looked up on the system path. It may not be relative.
final String _macOSExecutable;
/// The path to the executable on Windows.
///
/// This may be an absolute path; a basename, in which case it will be looked
/// up on the system path; or a relative path, in which case it will be looked
/// up relative to the paths in the `LOCALAPPDATA`, `PROGRAMFILES`, and
/// `PROGRAMFILES(X64)` environment variables.
final String _windowsExecutable;
/// The path to the executable for the current operating system.
String get executable {
if (Platform.isMacOS) return _macOSExecutable;
if (!Platform.isWindows) return _linuxExecutable;
if (p.isAbsolute(_windowsExecutable)) return _windowsExecutable;
if (p.basename(_windowsExecutable) == _windowsExecutable) {
return _windowsExecutable;
}
var prefixes = [
Platform.environment['LOCALAPPDATA'],
Platform.environment['PROGRAMFILES'],
Platform.environment['PROGRAMFILES(X86)']
];
for (var prefix in prefixes) {
if (prefix == null) continue;
var path = p.join(prefix, _windowsExecutable);
if (File(path).existsSync()) return path;
}
// If we can't find a path that works, return one that doesn't. This will
// cause an "executable not found" error to surface.
return p.join(
prefixes.firstWhere((prefix) => prefix != null, orElse: () => '.'),
_windowsExecutable);
}
/// Whether to invoke the browser in headless mode.
///
/// This is currently only supported by Chrome.
bool get headless => _headless ?? true;
final bool _headless;
/// Parses settings from a user-provided YAML mapping.
factory ExecutableSettings.parse(YamlMap settings) {
List<String> arguments;
var argumentsNode = settings.nodes['arguments'];
if (argumentsNode != null) {
var value = argumentsNode.value;
if (value is String) {
try {
arguments = shellSplit(value);
} on FormatException catch (error) {
throw SourceSpanFormatException(error.message, argumentsNode.span);
}
} else {
throw SourceSpanFormatException(
'Must be a string.', argumentsNode.span);
}
}
String linuxExecutable;
String macOSExecutable;
String windowsExecutable;
var executableNode = settings.nodes['executable'];
if (executableNode != null) {
var value = executableNode.value;
if (value is String) {
// Don't check this on Windows because people may want to set relative
// paths in their global config.
if (!Platform.isWindows) {
_assertNotRelative(executableNode as YamlScalar);
}
linuxExecutable = value;
macOSExecutable = value;
windowsExecutable = value;
} else if (executableNode is YamlMap) {
linuxExecutable = _getExecutable(executableNode.nodes['linux']);
macOSExecutable = _getExecutable(executableNode.nodes['mac_os']);
windowsExecutable = _getExecutable(executableNode.nodes['windows'],
allowRelative: true);
} else {
throw SourceSpanFormatException(
'Must be a map or a string.', executableNode.span);
}
}
var headless = true;
var headlessNode = settings.nodes['headless'];
if (headlessNode != null) {
var value = headlessNode.value;
if (value is bool) {
headless = value;
} else {
throw SourceSpanFormatException(
'Must be a boolean.', headlessNode.span);
}
}
return ExecutableSettings(
arguments: arguments,
linuxExecutable: linuxExecutable,
macOSExecutable: macOSExecutable,
windowsExecutable: windowsExecutable,
headless: headless);
}
/// Asserts that [executableNode] is a string or `null` and returns it.
///
/// If [allowRelative] is `false` (the default), asserts that the value isn't
/// a relative path.
static String _getExecutable(YamlNode executableNode,
{bool allowRelative = false}) {
if (executableNode == null || executableNode.value == null) return null;
if (executableNode.value is! String) {
throw SourceSpanFormatException('Must be a string.', executableNode.span);
}
if (!allowRelative) _assertNotRelative(executableNode as YamlScalar);
return executableNode.value as String;
}
/// Throws a [SourceSpanFormatException] if [executableNode]'s value is a
/// relative POSIX path that's not just a plain basename.
///
/// We loop up basenames on the PATH and we can resolve absolute paths, but we
/// have no way of interpreting relative paths.
static void _assertNotRelative(YamlScalar executableNode) {
var executable = executableNode.value as String;
if (!p.posix.isRelative(executable)) return;
if (p.posix.basename(executable) == executable) return;
throw SourceSpanFormatException(
'Linux and Mac OS executables may not be relative paths.',
executableNode.span);
}
ExecutableSettings(
{Iterable<String> arguments,
String linuxExecutable,
String macOSExecutable,
String windowsExecutable,
bool headless})
: arguments = arguments == null ? const [] : List.unmodifiable(arguments),
_linuxExecutable = linuxExecutable,
_macOSExecutable = macOSExecutable,
_windowsExecutable = windowsExecutable,
_headless = headless;
/// Merges [this] with [other], with [other]'s settings taking priority.
ExecutableSettings merge(ExecutableSettings other) => ExecutableSettings(
arguments: arguments.toList()..addAll(other.arguments),
headless: other._headless ?? _headless,
linuxExecutable: other._linuxExecutable ?? _linuxExecutable,
macOSExecutable: other._macOSExecutable ?? _macOSExecutable,
windowsExecutable: other._windowsExecutable ?? _windowsExecutable);
}