| // 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 'package:file/file.dart'; |
| import 'package:file/local.dart'; |
| import 'package:path/path.dart' show Context; |
| import 'package:platform/platform.dart'; |
| |
| const Map<String, String> _osToPathStyle = <String, String>{ |
| 'linux': 'posix', |
| 'macos': 'posix', |
| 'android': 'posix', |
| 'ios': 'posix', |
| 'fuchsia': 'posix', |
| 'windows': 'windows', |
| }; |
| |
| /// Sanitizes the executable path on Windows. |
| /// https://github.com/dart-lang/sdk/issues/37751 |
| String sanitizeExecutablePath(String executable, |
| {Platform platform = const LocalPlatform()}) { |
| if (executable.isEmpty) { |
| return executable; |
| } |
| if (!platform.isWindows) { |
| return executable; |
| } |
| if (executable.contains(' ') && !executable.contains('"')) { |
| // Use quoted strings to indicate where the file name ends and the arguments begin; |
| // otherwise, the file name is ambiguous. |
| return '"$executable"'; |
| } |
| return executable; |
| } |
| |
| /// Searches the `PATH` for the executable that [command] is supposed to launch. |
| /// |
| /// This first builds a list of candidate paths where the executable may reside. |
| /// If [command] is already an absolute path, then the `PATH` environment |
| /// variable will not be consulted, and the specified absolute path will be the |
| /// only candidate that is considered. |
| /// |
| /// Once the list of candidate paths has been constructed, this will pick the |
| /// first such path that represents an existent file. |
| /// |
| /// Return `null` if there were no viable candidates, meaning the executable |
| /// could not be found. |
| /// |
| /// If [platform] is not specified, it will default to the current platform. |
| String? getExecutablePath( |
| String command, |
| String? workingDirectory, { |
| Platform platform = const LocalPlatform(), |
| FileSystem fs = const LocalFileSystem(), |
| bool errorOnNull = false, |
| }) { |
| assert(_osToPathStyle[platform.operatingSystem] == fs.path.style.name); |
| try { |
| workingDirectory ??= fs.currentDirectory.path; |
| } on FileSystemException { |
| // The `currentDirectory` getter can throw a FileSystemException for example |
| // when the process doesn't have read/list permissions in each component of |
| // the cwd path. In this case, fall back on '.'. |
| workingDirectory ??= '.'; |
| } |
| Context context = Context(style: fs.path.style, current: workingDirectory); |
| |
| // TODO(goderbauer): refactor when github.com/google/platform.dart/issues/2 |
| // is available. |
| String pathSeparator = platform.isWindows ? ';' : ':'; |
| |
| List<String> extensions = <String>[]; |
| if (platform.isWindows && context.extension(command).isEmpty) { |
| extensions = platform.environment['PATHEXT']!.split(pathSeparator); |
| } |
| |
| List<String> candidates = <String>[]; |
| if (command.contains(context.separator)) { |
| candidates = _getCandidatePaths( |
| command, <String>[workingDirectory], extensions, context); |
| } else { |
| List<String> searchPath = |
| platform.environment['PATH']!.split(pathSeparator); |
| candidates = _getCandidatePaths(command, searchPath, extensions, context); |
| } |
| for (String path in candidates) { |
| final File candidate = fs.file(path); |
| FileStat stat = candidate.statSync(); |
| // Only return files or links that exist. |
| if (stat.type == FileSystemEntityType.notFound || |
| stat.type == FileSystemEntityType.directory) { |
| continue; |
| } |
| |
| const int isExecutable = 0x40; |
| const int isReadable = 0x100; |
| const int isExecutableAndReadable = isExecutable | isReadable; |
| // Should only return files or links that are readable and executable by the |
| // user. |
| |
| // On Windows it's not actually possible to only return files that are |
| // readable, since Dart reports files that have had read permission removed |
| // as being readable, but not checking for it is the same as checking for it |
| // and finding it readable, so we use the same check here on all platforms, |
| // so that if Dart ever gets fixed, it'll just work. |
| if (stat.mode & isExecutableAndReadable == isExecutableAndReadable) { |
| return path; |
| } |
| } |
| if (errorOnNull) { |
| throw ArgumentError('Failed to resolve $command to an executable.\n' |
| 'workingDirectory: $workingDirectory, ' |
| 'candidates: ${candidates.length}'); |
| } |
| return null; |
| } |
| |
| /// Returns all possible combinations of `$searchPath\$command.$ext` for |
| /// `searchPath` in [searchPaths] and `ext` in [extensions]. |
| /// |
| /// If [extensions] is empty, it will just enumerate all |
| /// `$searchPath\$command`. |
| /// If [command] is an absolute path, it will just enumerate |
| /// `$command.$ext`. |
| List<String> _getCandidatePaths( |
| String command, |
| List<String> searchPaths, |
| List<String> extensions, |
| Context context, |
| ) { |
| List<String> withExtensions = extensions.isNotEmpty |
| ? extensions.map((String ext) => '$command$ext').toList() |
| : <String>[command]; |
| if (context.isAbsolute(command)) { |
| return withExtensions; |
| } |
| return searchPaths |
| .map((String path) => |
| withExtensions.map((String command) => context.join(path, command))) |
| .expand((Iterable<String> e) => e) |
| .toList() |
| .cast<String>(); |
| } |