| // Copyright 2020 The Flutter Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. | |
| import 'dart:io'; | |
| import 'package:collection/collection.dart'; | |
| import 'package:devtools_tool/utils.dart'; | |
| import 'package:path/path.dart' as path; | |
| class DevToolsRepo { | |
| DevToolsRepo._create(this.repoPath); | |
| /// The path to the DevTools repository root. | |
| final String repoPath; | |
| /// The path to the DevTools 'tool' directory. | |
| String get toolDirectoryPath => path.join(repoPath, 'tool'); | |
| /// The path to the 'tool/flutter-sdk' directory. | |
| String get toolFlutterSdkPath => | |
| path.join(toolDirectoryPath, sdkDirectoryName); | |
| /// The name of the Flutter SDK directory. | |
| String get sdkDirectoryName => 'flutter-sdk'; | |
| /// The path to the DevTools 'devtools_app' directory. | |
| String get devtoolsAppDirectoryPath => | |
| path.join(repoPath, 'packages', 'devtools_app'); | |
| @override | |
| String toString() => '[DevTools $repoPath]'; | |
| /// This returns the DevToolsRepo instance based on the current working | |
| /// directory. | |
| /// | |
| /// Throws if the current working directory is not contained within a git | |
| /// checkout of DevTools. | |
| static DevToolsRepo getInstance() { | |
| final repoPath = _findRepoRoot(Directory.current); | |
| if (repoPath == null) { | |
| throw Exception( | |
| 'dt must be run from inside of the DevTools repository directory', | |
| ); | |
| } | |
| return DevToolsRepo._create(repoPath); | |
| } | |
| /// Returns the list of Dart or Flutter packages contained within the DevTools | |
| /// repository. | |
| /// | |
| /// A Dart or Flutter package is defined as any directory with a pubspec.yaml | |
| /// file. | |
| /// | |
| /// If a package path contains any part on its path that is in [skip], the | |
| /// package will not be included in the returned results. | |
| /// | |
| /// If [includeSubdirectories] is false, packages that are a subdirectory of | |
| /// another package will not be included in the returned results. | |
| List<Package> getPackages({ | |
| List<String> skip = const [], | |
| bool includeSubdirectories = true, | |
| }) { | |
| final result = <Package>[]; | |
| final repoDir = Directory(repoPath); | |
| for (final entity in repoDir.listSync()) { | |
| final name = path.basename(entity.path); | |
| if (entity is Directory && !name.startsWith('.')) { | |
| _collectPackages( | |
| entity, | |
| result, | |
| skip: skip, | |
| includeSubdirectories: includeSubdirectories, | |
| ); | |
| } | |
| } | |
| result.sort((a, b) => a.packagePath.compareTo(b.packagePath)); | |
| return result; | |
| } | |
| static String? _findRepoRoot(Directory dir) { | |
| // Look for README.md, packages, tool. | |
| if (_fileExists(dir, 'README.md') && | |
| _dirExists(dir, 'packages') && | |
| _dirExists(dir, 'tool')) { | |
| return dir.path; | |
| } | |
| if (dir.path == dir.parent.path) { | |
| return null; | |
| } else { | |
| return _findRepoRoot(dir.parent); | |
| } | |
| } | |
| void _collectPackages( | |
| Directory dir, | |
| List<Package> result, { | |
| bool includeSubdirectories = true, | |
| List<String> skip = const [], | |
| }) { | |
| // Do not collect packages from the Flutter SDK that is stored in the tool/ | |
| // directory. | |
| if (dir.path.contains(path.join('tool', 'flutter-sdk'))) return; | |
| if (_fileExists(dir, 'pubspec.yaml')) { | |
| final isTopLevelPackagesDir = dir.path.endsWith('packages'); | |
| final shouldSkip = | |
| skip.firstWhereOrNull((skipDir) => dir.path.contains(skipDir)) != | |
| null; | |
| if (isTopLevelPackagesDir || shouldSkip) { | |
| // Do not include the top level devtools/packages directory in the results | |
| // even though it has a pubspec.yaml file. Also skip any directories | |
| // specified by [skip]. | |
| final reason = isTopLevelPackagesDir | |
| ? 'each DevTools package is analyzed individually' | |
| : '${skip.toString()} directories are intentionally skipped'; | |
| print('Skipping ${dir.path} in _collectPackages because $reason.'); | |
| } else { | |
| final ancestor = result.firstWhereOrNull( | |
| (p) => | |
| // Remove the last segment of [dir]'s pathSegments to ensure we | |
| // are only checking ancestors and not sibling directories with | |
| // similar names. | |
| (List.from(dir.uri.pathSegments)..safeRemoveLast()) | |
| // TODO(kenz): this may cause issues for Windows paths. | |
| .join('/') | |
| .startsWith(p.packagePath), | |
| ); | |
| final ancestorDirectoryAdded = ancestor != null; | |
| if (!includeSubdirectories && ancestorDirectoryAdded) { | |
| print( | |
| 'Skipping ${dir.path} in _collectPackages because it is a ' | |
| 'subdirectory of another package (${ancestor.packagePath}).', | |
| ); | |
| } else { | |
| result.add(Package._(this, dir.path)); | |
| } | |
| } | |
| } | |
| for (final entity in dir.listSync(followLinks: false)) { | |
| final name = path.basename(entity.path); | |
| if (entity is Directory && !name.startsWith('.') && name != 'build') { | |
| _collectPackages( | |
| entity, | |
| result, | |
| skip: skip, | |
| includeSubdirectories: includeSubdirectories, | |
| ); | |
| } | |
| } | |
| } | |
| /// Reads the file at [uri], which should be a relative path from [repoPath]. | |
| String readFile(Uri uri) { | |
| return File(path.join(repoPath, uri.path)).readAsStringSync(); | |
| } | |
| } | |
| class FlutterSdk { | |
| FlutterSdk._(this.sdkPath); | |
| static FlutterSdk? _current; | |
| /// The current located Flutter SDK. | |
| /// | |
| /// Tries to locate from the running Dart VM. If not found, will print a | |
| /// warning and use Flutter from PATH. | |
| static FlutterSdk get current { | |
| if (_current == null) { | |
| throw Exception( | |
| 'Cannot use FlutterSdk.current before SDK has been selected.' | |
| 'SDK selection is done by DevToolsCommandRunner.runCommand().', | |
| ); | |
| } | |
| return _current!; | |
| } | |
| /// Sets the active Flutter SDK to the one that contains the Dart VM being | |
| /// used to run this script. | |
| /// | |
| /// Throws if the current VM is not inside a Flutter SDK. | |
| static void useFromCurrentVm() { | |
| _current = findFromCurrentVm(); | |
| } | |
| /// Sets the active Flutter SDK to the one found in the `PATH` environment | |
| /// variable (by running which/where). | |
| /// | |
| /// Throws if an SDK is not found on PATH. | |
| static void useFromPathEnvironmentVariable() { | |
| _current = findFromPathEnvironmentVariable(); | |
| } | |
| /// Sets the active Flutter SDK to the one at [sdkPath]. | |
| /// | |
| /// Throws if an SDK is not found at [sdkPath]. | |
| static void useFromPath(String sdkPath) { | |
| _current = findFromPath(sdkPath); | |
| } | |
| /// Finds the Flutter SDK that contains the Dart VM being used to run this | |
| /// script. | |
| /// | |
| /// Throws if the current VM is not inside a Flutter SDK. | |
| static FlutterSdk findFromCurrentVm() { | |
| // Look for it relative to the current Dart process. | |
| final dartVmPath = Platform.resolvedExecutable; | |
| final pathSegments = path.split(dartVmPath); | |
| // TODO(dantup): Should we add tool/flutter-sdk to the front here, to | |
| // ensure we _only_ ever use this one, to avoid potentially updating a | |
| // different Flutter if the user runs explicitly with another Flutter? | |
| final expectedSegments = path.posix.split('bin/cache/dart-sdk/bin/dart'); | |
| if (pathSegments.length >= expectedSegments.length) { | |
| // Remove the trailing 'dart'. | |
| pathSegments.removeLast(); | |
| expectedSegments.removeLast(); | |
| while (expectedSegments.isNotEmpty) { | |
| if (expectedSegments.last == pathSegments.last) { | |
| pathSegments.removeLast(); | |
| expectedSegments.removeLast(); | |
| } else { | |
| break; | |
| } | |
| } | |
| if (expectedSegments.isEmpty) { | |
| final flutterSdkRoot = path.joinAll(pathSegments); | |
| return FlutterSdk._(flutterSdkRoot); | |
| } | |
| } | |
| throw Exception( | |
| 'Unable to locate the Flutter SDK from the current running Dart VM:\n' | |
| '${Platform.resolvedExecutable}', | |
| ); | |
| } | |
| static FlutterSdk findFromPath(String sdkPath) { | |
| var resolvedPath = sdkPath; | |
| try { | |
| resolvedPath = File(sdkPath).resolveSymbolicLinksSync(); | |
| } catch (_) { | |
| // Fallback to the unresolved path if resolution fails. | |
| } | |
| final resolvedPathParent = path.dirname(resolvedPath); | |
| if (path.basename(resolvedPathParent) == 'bin') { | |
| return FlutterSdk._(path.dirname(resolvedPathParent)); | |
| } else if (path.basename(resolvedPath) == 'bin') { | |
| return FlutterSdk._(resolvedPathParent); | |
| } else if (Directory(path.join(resolvedPath, 'bin')).existsSync()) { | |
| return FlutterSdk._(resolvedPath); | |
| } | |
| throw Exception('Unable to locate the Flutter SDK at "$sdkPath"'); | |
| } | |
| /// Finds a Flutter SDK in the `PATH` environment variable | |
| /// (by running which/where). | |
| /// | |
| /// Throws if an SDK is not found on PATH. | |
| static FlutterSdk findFromPathEnvironmentVariable() { | |
| final whichCommand = Platform.isWindows ? 'where.exe' : 'which'; | |
| final result = Process.runSync(whichCommand, ['flutter']); | |
| if (result.exitCode == 0) { | |
| final sdkPath = result.stdout.toString().split('\n').first.trim(); | |
| return findFromPath(sdkPath); | |
| } | |
| throw Exception('Unable to locate the Flutter SDK on PATH'); | |
| } | |
| final String sdkPath; | |
| /// The name of the 'flutter' shell script for the current platform. | |
| static String get flutterExecutableName => shellScriptName('flutter'); | |
| /// The name of the 'dart' wrapper shell script in Flutter for the current | |
| /// platform. | |
| /// | |
| /// On windows, 'dart' is fine for running the .exe from the Dart SDK directly | |
| /// but the wrapper in the Flutter bin folder is a .bat and needs an explicit | |
| /// extension. | |
| static String get dartWrapperExecutableName => shellScriptName('dart'); | |
| String get flutterExePath => path.join(sdkPath, 'bin', flutterExecutableName); | |
| String get dartExePath => | |
| path.join(sdkPath, 'bin', dartWrapperExecutableName); | |
| String get dartSdkPath => path.join(sdkPath, 'bin', 'cache', 'dart-sdk'); | |
| String get pubToolPath => path.join(dartSdkPath, 'bin', 'pub'); | |
| @override | |
| String toString() => '[Flutter sdk: $sdkPath]'; | |
| } | |
| class Package { | |
| Package._(this.repo, this.packagePath); | |
| final DevToolsRepo repo; | |
| final String packagePath; | |
| String get relativePath => path.relative(packagePath, from: repo.repoPath); | |
| bool get hasAnyDartCode { | |
| final dartFiles = <String>[]; | |
| _collectDartFiles(Directory(packagePath), dartFiles); | |
| return dartFiles.isNotEmpty; | |
| } | |
| void _collectDartFiles(Directory dir, List<String> result) { | |
| for (final entity in dir.listSync(followLinks: false)) { | |
| final name = path.basename(entity.path); | |
| if (entity is Directory && !name.startsWith('.') && name != 'build') { | |
| _collectDartFiles(entity, result); | |
| } else if (entity is File && path.extension(name) == '.dart') { | |
| result.add(entity.path); | |
| } | |
| } | |
| } | |
| @override | |
| String toString() => '[Package $relativePath]'; | |
| } | |
| bool _fileExists(Directory parent, String name) { | |
| return FileSystemEntity.isFileSync(path.join(parent.path, name)); | |
| } | |
| bool _dirExists(Directory parent, String name) { | |
| return FileSystemEntity.isDirectorySync(path.join(parent.path, name)); | |
| } | |
| extension _SafeAccessList<T> on List<T> { | |
| T? safeRemoveLast() => isNotEmpty ? removeLast() : null; | |
| } |