blob: 069fa39af745f99e424446a1498ade16fb4cb414 [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: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 (new 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);
}
/// Parses settings from a user-provided YAML mapping.
factory ExecutableSettings.parse(YamlMap settings) {
List<String> arguments;
var argumentsNode = settings.nodes["arguments"];
if (argumentsNode != null) {
if (argumentsNode.value is String) {
try {
arguments = shellSplit(argumentsNode.value);
} on FormatException catch (error) {
throw new SourceSpanFormatException(
error.message, argumentsNode.span);
}
} else {
throw new SourceSpanFormatException(
"Must be a string.", argumentsNode.span);
}
}
String linuxExecutable;
String macOSExecutable;
String windowsExecutable;
var executableNode = settings.nodes["executable"];
if (executableNode != null) {
if (executableNode.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);
linuxExecutable = executableNode.value;
macOSExecutable = executableNode.value;
windowsExecutable = executableNode.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 new SourceSpanFormatException(
"Must be a map or a string.", executableNode.span);
}
}
return new ExecutableSettings(
arguments: arguments,
linuxExecutable: linuxExecutable,
macOSExecutable: macOSExecutable,
windowsExecutable: windowsExecutable);
}
/// 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 new SourceSpanFormatException(
"Must be a string.", executableNode.span);
}
if (!allowRelative) _assertNotRelative(executableNode);
return executableNode.value;
}
/// 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 new SourceSpanFormatException(
"Linux and Mac OS executables may not be relative paths.",
executableNode.span);
}
ExecutableSettings(
{Iterable<String> arguments,
String linuxExecutable,
String macOSExecutable,
String windowsExecutable})
: arguments =
arguments == null ? const [] : new List.unmodifiable(arguments),
_linuxExecutable = linuxExecutable,
_macOSExecutable = macOSExecutable,
_windowsExecutable = windowsExecutable;
/// Merges [this] with [other], with [other]'s settings taking priority.
ExecutableSettings merge(ExecutableSettings other) => new ExecutableSettings(
arguments: arguments.toList()..addAll(other.arguments),
linuxExecutable: other._linuxExecutable ?? _linuxExecutable,
macOSExecutable: other._macOSExecutable ?? _macOSExecutable,
windowsExecutable: other._windowsExecutable ?? _windowsExecutable);
}