| // Copyright (c) 2025, 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. |
| |
| /// The directory and file structure for `dart install` on the host machine. |
| library; |
| |
| import 'dart:io'; |
| |
| import 'package:dart_data_home/dart_data_home.dart'; |
| import 'package:dartdev/src/utils.dart'; |
| |
| /// The root directory for Dart installations. |
| /// |
| /// This directory contains various subdirectories for binaries and app bundles. |
| /// |
| /// <pre> |
| /// [DartInstallDirectory] |
| /// ├── [bin]/ |
| /// │ └── (executables) |
| /// └── app-bundles/ |
| /// └── (packageName)/ |
| /// ├── git/ |
| /// │ └── (gitHash)/ |
| /// │ └── [AppBundleDirectory] (e.g., 'my_package/git/abcdef123/') |
| /// ├── hosted/ |
| /// │ └── (version)/ |
| /// │ └── [AppBundleDirectory] (e.g., 'my_package/hosted/1.0.0/') |
| /// └── local/ |
| /// └── [AppBundleDirectory] (e.g., 'my_package/local/') |
| /// </pre> |
| extension type DartInstallDirectory._(Directory directory) { |
| static final DartInstallDirectory _singleton = |
| DartInstallDirectory._(Directory(getDartDataHome('install'))); |
| |
| factory DartInstallDirectory() { |
| return _singleton; |
| } |
| |
| BinOnPathDirectory get bin => BinOnPathDirectory._( |
| Directory.fromUri( |
| directory.uri.resolve('bin/'), |
| ), |
| ); |
| |
| Directory get _appBundles => |
| Directory.fromUri(directory.uri.resolve('app-bundles/')); |
| |
| AppBundleDirectory gitAppBundle( |
| String packageName, |
| String gitHash, |
| ) => |
| AppBundleDirectory._( |
| Directory.fromUri( |
| _appBundles.uri.resolve('$packageName/git/$gitHash/'), |
| ), |
| ); |
| |
| AppBundleDirectory hostedAppBundle( |
| String packageName, |
| String version, |
| ) => |
| AppBundleDirectory._( |
| Directory.fromUri( |
| _appBundles.uri.resolve('$packageName/hosted/$version/'), |
| ), |
| ); |
| |
| AppBundleDirectory localAppBundle( |
| String packageName, |
| ) => |
| AppBundleDirectory._( |
| Directory.fromUri( |
| _appBundles.uri.resolve('$packageName/local/'), |
| ), |
| ); |
| |
| List<AppBundleDirectory> allAppBundlesSync({String? packageName}) { |
| final dartInstallAppbundlesDir = _appBundles; |
| if (!dartInstallAppbundlesDir.existsSync()) { |
| return []; |
| } |
| final packageDirs = |
| dartInstallAppbundlesDir.listSync().whereType<Directory>(); |
| final result = <AppBundleDirectory>[]; |
| for (final packageDir in packageDirs) { |
| if (packageName != null && packageDir.name != packageName) { |
| continue; |
| } |
| final gitDir = Directory.fromUri(packageDir.uri.resolve('git/')); |
| final hostedDir = Directory.fromUri(packageDir.uri.resolve('hosted/')); |
| final localDir = Directory.fromUri(packageDir.uri.resolve('local/')); |
| if (gitDir.existsSync()) { |
| result.addAll( |
| gitDir |
| .listSync() |
| .whereType<Directory>() |
| .map((d) => AppBundleDirectory._(d.ensureEndWithSeparator)), |
| ); |
| } |
| if (hostedDir.existsSync()) { |
| result.addAll( |
| hostedDir |
| .listSync() |
| .whereType<Directory>() |
| .map((d) => AppBundleDirectory._(d.ensureEndWithSeparator)), |
| ); |
| } |
| if (localDir.existsSync()) { |
| result.add(AppBundleDirectory._(localDir.ensureEndWithSeparator)); |
| } |
| } |
| return result; |
| } |
| } |
| |
| /// The directory that contains all executables available on `PATH`. |
| /// |
| /// <pre> |
| /// [BinOnPathDirectory] |
| /// └── [executable]s |
| /// </pre> |
| extension type BinOnPathDirectory._(Directory directory) { |
| /// An executable with [name] in the bin directory. |
| /// |
| /// The parameter [name] must not contain an extension. |
| ExecutableOnPath executable(String name) { |
| if (Platform.isLinux || Platform.isMacOS) { |
| return ExecutableOnPath._unix(Link.fromUri(directory.uri.resolve(name))); |
| } |
| if (Platform.isWindows) { |
| return ExecutableOnPath._windows( |
| File.fromUri(directory.uri.resolve('$name.bat'))); |
| } |
| throw UnsupportedError('Unsupported OS: ${Platform.operatingSystem}.'); |
| } |
| } |
| |
| /// An executable in [BinOnPathDirectory] available on `PATH`. |
| /// |
| /// [entity] is a [Link] on Linux and MacOS, and a [File] on Windows. |
| extension type ExecutableOnPath._(FileSystemEntity entity) { |
| factory ExecutableOnPath._unix(Link link) => ExecutableOnPath._(link); |
| |
| Link get unix { |
| if (Platform.isLinux || Platform.isMacOS) { |
| return entity as Link; |
| } |
| throw UnsupportedError('Wrong OS: ${Platform.operatingSystem}.'); |
| } |
| |
| factory ExecutableOnPath._windows(File file) => ExecutableOnPath._(file); |
| |
| File get windows { |
| if (Platform.isWindows) { |
| return entity as File; |
| } |
| throw UnsupportedError('Wrong OS: ${Platform.operatingSystem}.'); |
| } |
| |
| bool existsSync() => entity.existsSync(); |
| |
| void deleteSync() => entity.deleteSync(); |
| |
| static const _marker = 'target_file_path_marker'; |
| |
| void createSync(ExecutableInBundle target) { |
| if (Platform.isLinux || Platform.isMacOS) { |
| return unix.createSync(target.file.path, recursive: true); |
| } |
| if (Platform.isWindows) { |
| final wrapperScriptContents = ''' |
| @ECHO OFF |
| REM $_marker |
| "${target.file.path}" %* |
| EXIT /B %ERRORLEVEL% |
| '''; |
| if (!windows.existsSync()) { |
| windows.createSync(recursive: true); |
| } |
| return windows.writeAsStringSync(wrapperScriptContents); |
| } |
| throw UnsupportedError('Unsupported OS: ${Platform.operatingSystem}.'); |
| } |
| |
| ExecutableInBundle targetSync() { |
| if (Platform.isLinux || Platform.isMacOS) { |
| return ExecutableInBundle._(File(unix.targetSync())); |
| } |
| if (Platform.isWindows) { |
| final wrapperScriptContents = windows.readAsStringSync(); |
| final iterator = wrapperScriptContents.split('\n').iterator..moveNext(); |
| while (!iterator.current.contains(_marker)) { |
| iterator.moveNext(); |
| } |
| iterator.moveNext(); |
| final line = iterator.current; |
| final path = line.split('"')[1]; |
| return ExecutableInBundle._(File(path)); |
| } |
| throw UnsupportedError('Unsupported OS: ${Platform.operatingSystem}.'); |
| } |
| |
| bool equals(ExecutableOnPath other) => entity.path == other.entity.path; |
| } |
| |
| /// A directory containing an app bundle and its installation data. |
| /// |
| /// This directory is structured as follows: |
| /// |
| /// <pre> |
| /// [AppBundleDirectory] |
| /// ├── bundle/ (Contains the application code and assets) |
| /// │ ├── bin/ (Executables that can be run directly) |
| /// │ │ └── [ExecutableInBundle] (Specific executable files for the app bundle) |
| /// │ └── lib/ (Dynamic libraries required by the executables) |
| /// │ └── (dynamic libraries) (Platform-specific shared libraries) |
| /// ├── pubspec.lock (Generated by pub, locks package dependencies to specific versions) |
| /// └── pubspec.yaml (Declares project dependencies and metadata) |
| /// </pre> |
| extension type AppBundleDirectory._(Directory directory) { |
| String get packageName { |
| final result = tryPackageName; |
| if (result != null) { |
| return result; |
| } |
| throw StateError('${directory.path} is not a valid app bundle directory.'); |
| } |
| |
| String? get tryPackageName { |
| if (!directory.path.startsWith(DartInstallDirectory()._appBundles.path)) { |
| throw StateError( |
| '${directory.path} does not start with ${DartInstallDirectory()._appBundles.path}.', |
| ); |
| // return null; |
| } |
| final relativeSegments = directory.uri.pathSegments |
| .skip(DartInstallDirectory() |
| ._appBundles |
| .uri |
| .pathSegments |
| .where((e) => e.isNotEmpty) |
| .length) |
| .toList(); |
| if (relativeSegments.length < 2) { |
| throw StateError( |
| '$directory, $relativeSegments does not contain at least two path segments.', |
| ); |
| // return null; |
| } |
| if (relativeSegments[1] != 'hosted' && |
| relativeSegments[1] != 'git' && |
| relativeSegments[1] != 'local') { |
| throw StateError( |
| '$directory, $relativeSegments, ${relativeSegments[1]} is not hosted, git or local.', |
| ); |
| // return null; |
| } |
| return relativeSegments[0]; |
| } |
| |
| Directory get _binDirectory => |
| Directory.fromUri(directory.uri.resolve('bundle/bin/')); |
| |
| List<ExecutableInBundle> get executablesSync { |
| final binaries = _binDirectory |
| .listSync() |
| .whereType<File>() |
| .map((e) => ExecutableInBundle._(e)) |
| .toList(); |
| return binaries; |
| } |
| |
| /// The executables from this bundle which are available on `PATH`. |
| List<ExecutableOnPath> get executablesOnPathSync { |
| final result = <ExecutableOnPath>[]; |
| for (final executable in executablesSync) { |
| final onPath = executable.onPath; |
| if (onPath.existsSync() && onPath.targetSync().equals(executable)) { |
| result.add(onPath); |
| } |
| } |
| return result; |
| } |
| |
| /// An executable with [name] in the app bundle. |
| /// |
| /// The parameter [name] most not contain an extension. |
| ExecutableInBundle executable(String name) { |
| return ExecutableInBundle._(File.fromUri( |
| _binDirectory.uri.resolve( |
| Platform.isWindows ? '$name.exe' : name, |
| ), |
| )); |
| } |
| |
| File get pubspec => File.fromUri(directory.uri.resolve('pubspec.yaml')); |
| |
| File get pubspecLock => File.fromUri(directory.uri.resolve('pubspec.lock')); |
| } |
| |
| /// An executable inside an [AppBundleDirectory]. |
| extension type ExecutableInBundle._(File file) { |
| AppBundleDirectory get appBundle { |
| return AppBundleDirectory._(Directory.fromUri( |
| file.uri.resolve('../../'), |
| )); |
| } |
| |
| ExecutableOnPath get onPath => |
| DartInstallDirectory().bin.executable(file.basenameWithoutExtension); |
| |
| bool equals(ExecutableInBundle other) => file.path == other.file.path; |
| } |
| |
| extension DirectoryExtension on Directory { |
| Directory get ensureEndWithSeparator => Directory.fromUri(uri); |
| } |