blob: 2d185492cf4347bad3ce6b04f38c746b77900c8c [file] [edit]
// Copyright (c) 2015, 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.
/// Utilities to locate the Dart SDK.
library;
import 'dart:async';
import 'dart:io';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
export 'src/base_directories.dart';
/// The path to the root of the Dart SDK, or `null` if no SDK could be located.
///
/// Probes the host environment across four tiers (memoized on first access):
/// 1. `Platform.resolvedExecutable` (active JIT VM runtime)
/// 2. `DART_ROOT` and `DART_SDK` environment variables
/// 3. `PATH` environment variable traversal (dereferencing symlinks and
/// checking Flutter cache layouts)
/// 4. `FLUTTER_ROOT/bin/cache/dart-sdk` fallback
String? get sdkPath {
if (Zone.current[environmentOverridesKey] != null) {
return _resolveSdkPath();
}
if (!_sdkPathResolved) {
_cachedSdkPath = _resolveSdkPath();
_sdkPathResolved = true;
}
return _cachedSdkPath;
}
String? _cachedSdkPath;
bool _sdkPathResolved = false;
/// The path to the `dart` executable, or `null` if no executable could be
/// located.
///
/// If a valid [sdkPath] is found, returns `<sdkPath>/bin/dart` (`dart.exe` on
/// Windows). Otherwise, attempts direct `PATH` resolution for `dart`.
String? get dartExecutable {
if (Zone.current[environmentOverridesKey] != null) {
return _resolveDartExecutable();
}
if (!_dartExecutableResolved) {
_cachedDartExecutable = _resolveDartExecutable();
_dartExecutableResolved = true;
}
return _cachedDartExecutable;
}
String? _cachedDartExecutable;
bool _dartExecutableResolved = false;
/// Returns the path to the current Dart SDK.
@Deprecated("Use 'sdkPath' instead")
String? getSdkPath() => sdkPath;
/// Checks whether [candidatePath] represents a valid Dart SDK directory layout.
///
/// Validates that [candidatePath] contains the required file markers of a Dart
/// SDK:
/// - A libraries configuration (`lib/libraries.json` or
/// `lib/_internal/allowed_experiments.json`), AND
/// - An executable or version file (`bin/dart` / `bin/dart.exe` or `version`).
///
/// Returns `false` if [candidatePath] is empty. Relative paths (such as `.` or
/// subdirectories) are evaluated relative to the current working directory.
bool isValidSdkPath(String candidatePath) {
if (candidatePath.isEmpty) return false;
final hasLibrariesJson =
File(path.join(candidatePath, 'lib', 'libraries.json')).existsSync();
final hasAllowedExperiments =
File(
path.join(
candidatePath,
'lib',
'_internal',
'allowed_experiments.json',
),
).existsSync();
final hasDartBin =
File(
path.join(
candidatePath,
'bin',
Platform.isWindows ? 'dart.exe' : 'dart',
),
).existsSync();
final hasVersion = File(path.join(candidatePath, 'version')).existsSync();
return (hasLibrariesJson || hasAllowedExperiments) &&
(hasDartBin || hasVersion);
}
String? _resolveSdkPath() {
// Tier 1: DART_ROOT and DART_SDK environment variables (explicit overrides)
final dartRoot = _env['DART_ROOT'];
if (dartRoot != null && dartRoot.isNotEmpty && isValidSdkPath(dartRoot)) {
return path.absolute(dartRoot);
}
final dartSdk = _env['DART_SDK'];
if (dartSdk != null && dartSdk.isNotEmpty && isValidSdkPath(dartSdk)) {
return path.absolute(dartSdk);
}
// Tier 2: Platform.resolvedExecutable (Fast path for JIT VM)
final executablePath =
_env['_DART_RESOLVED_EXECUTABLE'] ?? Platform.resolvedExecutable;
if (executablePath.isNotEmpty) {
final candidate = path.dirname(path.dirname(executablePath));
if (isValidSdkPath(candidate)) {
return path.absolute(candidate);
}
}
// Tier 3: PATH traversal with Flutter cache awareness
for (final candidateExe in _getExecutablePaths('dart')) {
final file = File(candidateExe);
if (file.existsSync()) {
String resolved;
try {
resolved = file.resolveSymbolicLinksSync();
} on FileSystemException {
resolved = candidateExe;
}
// Direct SDK root check
final candidateSdk = path.dirname(path.dirname(resolved));
if (isValidSdkPath(candidateSdk)) {
return path.absolute(candidateSdk);
}
// Flutter wrapper check (e.g. flutter/bin/dart -> flutter/bin/cache/dart-sdk)
final flutterCacheSdk = path.join(
candidateSdk,
'bin',
'cache',
'dart-sdk',
);
if (isValidSdkPath(flutterCacheSdk)) {
return path.absolute(flutterCacheSdk);
}
}
}
// Tier 4: FLUTTER_ROOT fallback
final flutterRoot = _env['FLUTTER_ROOT'];
if (flutterRoot != null && flutterRoot.isNotEmpty) {
final flutterSdk = path.join(flutterRoot, 'bin', 'cache', 'dart-sdk');
if (isValidSdkPath(flutterSdk)) {
return path.absolute(flutterSdk);
}
}
return null;
}
String? _resolveDartExecutable() {
final sdk = sdkPath;
if (sdk != null) {
final exe = path.join(sdk, 'bin', Platform.isWindows ? 'dart.exe' : 'dart');
if (File(exe).existsSync()) {
return path.absolute(exe);
}
}
// Fallback: search PATH directly if SDK structure wasn't complete
for (final candidate in _getExecutablePaths('dart')) {
if (File(candidate).existsSync()) {
return path.absolute(candidate);
}
}
return null;
}
Iterable<String> _getExecutablePaths(String executableName) sync* {
final pathEnv = _env['PATH'];
if (pathEnv == null || pathEnv.isEmpty) return;
final separator = Platform.isWindows ? ';' : ':';
final entries = pathEnv.split(separator);
final extensions =
Platform.isWindows
? (_env['PATHEXT']?.split(';').where((e) => e.isNotEmpty).toList() ??
const ['.exe', '.bat', '.cmd', ''])
: const [''];
for (final entry in entries) {
if (entry.isEmpty) continue;
var cleanEntry = entry.trim();
if (cleanEntry.startsWith('"') &&
cleanEntry.endsWith('"') &&
cleanEntry.length >= 2) {
cleanEntry = cleanEntry.substring(1, cleanEntry.length - 1);
}
if (cleanEntry.isEmpty) continue;
for (final ext in extensions) {
yield path.join(cleanEntry, '$executableName$ext');
}
}
}
/// The user-specific application configuration folder for the current platform.
///
/// This is a location appropriate for storing application specific
/// configuration for the current user. The [productName] should be unique to
/// avoid clashes with other applications on the same machine. This method won't
/// actually create the folder, merely return the recommended location for
/// storing user-specific application configuration.
///
/// The folder location depends on the platform:
/// * `%APPDATA%\<productName>` on **Windows**,
/// * `$HOME/Library/Application Support/<productName>` on **Mac OS**,
/// * `$XDG_CONFIG_HOME/<productName>` on **Linux**
/// (if `$XDG_CONFIG_HOME` is defined), and,
/// * `$HOME/.config/<productName>` otherwise.
///
/// The chosen location aims to follow best practices for each platform,
/// honoring the [XDG Base Directory Specification][1] on Linux and
/// [File System Basics][2] on Mac OS.
///
/// Throws an [EnvironmentNotFoundException] if an environment entry,
/// `%APPDATA%` or `$HOME`, is needed and not available.
///
/// [1]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
/// [2]: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW1
@Deprecated('Use BaseDirectories(productName).configHome instead.')
String applicationConfigHome(String productName) =>
path.join(_configHome, productName);
String get _configHome {
if (Platform.isWindows) {
return _requireEnv('APPDATA');
}
if (Platform.isMacOS) {
return path.join(_requireEnv('HOME'), 'Library', 'Application Support');
}
if (Platform.isLinux) {
final xdgConfigHome = _env['XDG_CONFIG_HOME'];
if (xdgConfigHome != null) {
return xdgConfigHome;
}
// XDG Base Directory Specification says to use $HOME/.config/ when
// $XDG_CONFIG_HOME isn't defined.
return path.join(_requireEnv('HOME'), '.config');
}
// We have no guidelines, perhaps we should just do: $HOME/.config/
// same as XDG specification would specify as fallback.
return path.join(_requireEnv('HOME'), '.config');
}
String _requireEnv(String name) =>
_env[name] ?? (throw EnvironmentNotFoundException(name));
/// Exception thrown if a required environment entry does not exist.
///
/// Thrown if an expected and required platform specific environment entry is
/// not available.
class EnvironmentNotFoundException implements Exception {
/// Name of environment entry which was needed, but not found.
final String entryName;
String get message => 'Environment variable \'$entryName\' is not defined!';
EnvironmentNotFoundException(this.entryName);
@override
String toString() => message;
}
/// Zone value key used for testing environment overrides.
@visibleForTesting
const environmentOverridesKey = #_environmentOverrides;
// This zone override exists solely for testing (see lib/cli_util_test.dart).
Map<String, String> get _env =>
(Zone.current[environmentOverridesKey] as Map<String, String>?) ??
Platform.environment;