| // Copyright (c) 2020, 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:async'; |
| import 'dart:io'; |
| import 'dart:isolate'; |
| import 'dart:math' as math; |
| |
| import 'package:args/args.dart'; |
| import 'package:path/path.dart' as p; |
| |
| import 'core.dart'; |
| import 'sdk.dart'; |
| |
| /// For commands where we are able to initialize the [ArgParser], this value |
| /// is used as the usageLineLength. |
| int? get dartdevUsageLineLength => |
| stdout.hasTerminal ? stdout.terminalColumns : null; |
| |
| /// Runs a tool's snapshot in an isolate. |
| /// |
| /// Waits for the spawned isolate to exit before returning. |
| Future<int> runFromSnapshot({ |
| required String snapshot, |
| required List<String> args, |
| required bool verbose, |
| }) async { |
| if (!checkArtifactExists(snapshot)) return 255; |
| |
| int retval = 0; |
| final result = Completer<int>(); |
| final exitPort = ReceivePort() |
| ..listen((msg) { |
| result.complete(0); |
| }); |
| final errorPort = ReceivePort() |
| ..listen((error) { |
| log.stderr(error.toString()); |
| result.complete(255); |
| }); |
| try { |
| await Isolate.spawnUri( |
| Uri.file(snapshot), |
| args, |
| null, |
| onExit: exitPort.sendPort, |
| onError: errorPort.sendPort, |
| ); |
| retval = await result.future; |
| } catch (e, st) { |
| log.stderr(e.toString()); |
| if (verbose) { |
| log.stderr(st.toString()); |
| } |
| retval = 255; |
| } |
| errorPort.close(); |
| exitPort.close(); |
| return retval; |
| } |
| |
| /// Global options for dartdev. |
| /// |
| /// ** READ THIS BEFORE MODIFYING ** |
| /// |
| /// Adding or changing behavior for global flags may have consequences for |
| /// integration with the VM. Check `runtime/bin/main_options.cc` in the |
| /// Dart SDK if adding or changing any flags. This is most important for |
| /// those that are intended to be run without a script such as |
| /// `dart --disable-analytics` as there is special handling. Any flags |
| /// added here should also be tested by hand with a compiled SDK as unit tests |
| /// running `dartdev.dart` directly do not hit that code path. |
| ArgParser globalDartdevOptionsParser({bool verbose = false}) { |
| var argParser = ArgParser( |
| usageLineLength: dartdevUsageLineLength, |
| allowTrailingOptions: false, |
| ); |
| argParser.addFlag('verbose', |
| abbr: 'v', negatable: false, help: 'Show additional command output.'); |
| argParser.addFlag('version', |
| negatable: false, help: 'Print the Dart SDK version.'); |
| argParser.addFlag('enable-analytics', |
| negatable: false, help: 'Enable analytics.'); |
| argParser.addFlag('disable-analytics', |
| negatable: false, help: 'Disable analytics.'); |
| argParser.addFlag('disable-telemetry', |
| negatable: false, help: 'Disable telemetry.', hide: true); |
| |
| argParser.addFlag('diagnostics', |
| negatable: false, help: 'Show tool diagnostic output.', hide: !verbose); |
| |
| argParser.addFlag( |
| 'analytics', |
| defaultsTo: true, |
| negatable: true, |
| help: 'Allow or disallow analytics for this `dart *` run without ' |
| 'changing the analytics configuration. ' |
| 'Deprecated: use `--suppress-analytics` instead.', |
| hide: true, |
| ); |
| |
| argParser.addFlag( |
| 'suppress-analytics', |
| negatable: false, |
| help: 'Disallow analytics for this `dart *` run without changing the ' |
| 'analytics configuration.', |
| ); |
| return argParser; |
| } |
| |
| /// Emit the given word with the correct pluralization. |
| String pluralize(String word, int count) => count == 1 ? word : '${word}s'; |
| |
| /// Make an absolute [filePath] relative to [dir] (for display purposes). |
| String relativePath(String filePath, Directory dir) { |
| var root = dir.absolute.path; |
| if (filePath.startsWith(root)) { |
| return filePath.substring(root.length + 1); |
| } |
| return filePath; |
| } |
| |
| /// String utility to trim some suffix from the end of a [String]. |
| String trimEnd(String s, String? suffix) { |
| if (suffix != null && suffix.isNotEmpty && s.endsWith(suffix)) { |
| return s.substring(0, s.length - suffix.length); |
| } |
| return s; |
| } |
| |
| extension FileSystemEntityExtension on FileSystemEntity { |
| String get name => p.basename(path); |
| |
| bool get isDartFile => this is File && p.extension(path) == '.dart'; |
| } |
| |
| /// Wraps [text] to the given [width], if provided. |
| String wrapText(String text, {int? width}) { |
| if (width == null) { |
| return text; |
| } |
| |
| var buffer = StringBuffer(); |
| var lineMaxEndIndex = width; |
| var lineStartIndex = 0; |
| |
| while (true) { |
| if (lineMaxEndIndex >= text.length) { |
| buffer.write(text.substring(lineStartIndex, text.length)); |
| break; |
| } else { |
| var lastSpaceIndex = text.lastIndexOf(' ', lineMaxEndIndex); |
| if (lastSpaceIndex == -1 || lastSpaceIndex <= lineStartIndex) { |
| // No space between [lineStartIndex] and [lineMaxEndIndex]. Get the |
| // _next_ space. |
| lastSpaceIndex = text.indexOf(' ', lineMaxEndIndex); |
| if (lastSpaceIndex == -1) { |
| // No space at all after [lineStartIndex]. |
| lastSpaceIndex = text.length; |
| buffer.write(text.substring(lineStartIndex, lastSpaceIndex)); |
| break; |
| } |
| } |
| buffer.write(text.substring(lineStartIndex, lastSpaceIndex)); |
| buffer.writeln(); |
| lineStartIndex = lastSpaceIndex + 1; |
| } |
| lineMaxEndIndex = lineStartIndex + width; |
| } |
| return buffer.toString(); |
| } |
| |
| // A valid Dart identifier that can be used for a package, i.e. no |
| // capital letters. |
| // https://dart.dev/guides/language/language-tour#important-concepts |
| final RegExp _identifierRegExp = RegExp(r'^[a-z_][a-z\d_]*$'); |
| |
| // non-contextual dart keywords. |
| // https://dart.dev/guides/language/language-tour#keywords |
| const Set<String> _keywords = <String>{ |
| 'abstract', |
| 'as', |
| 'assert', |
| 'async', |
| 'await', |
| 'break', |
| 'case', |
| 'catch', |
| 'class', |
| 'const', |
| 'continue', |
| 'covariant', |
| 'default', |
| 'deferred', |
| 'do', |
| 'dynamic', |
| 'else', |
| 'enum', |
| 'export', |
| 'extends', |
| 'extension', |
| 'external', |
| 'factory', |
| 'false', |
| 'final', |
| 'finally', |
| 'for', |
| 'function', |
| 'get', |
| 'hide', |
| 'if', |
| 'implements', |
| 'import', |
| 'in', |
| 'inout', |
| 'interface', |
| 'is', |
| 'late', |
| 'library', |
| 'mixin', |
| 'native', |
| 'new', |
| 'null', |
| 'of', |
| 'on', |
| 'operator', |
| 'out', |
| 'part', |
| 'patch', |
| 'required', |
| 'rethrow', |
| 'return', |
| 'set', |
| 'show', |
| 'source', |
| 'static', |
| 'super', |
| 'switch', |
| 'sync', |
| 'this', |
| 'throw', |
| 'true', |
| 'try', |
| 'typedef', |
| 'var', |
| 'void', |
| 'while', |
| 'with', |
| 'yield', |
| }; |
| |
| /// Whether [name] is a valid Pub package. |
| bool isValidPackageName(String name) => |
| _identifierRegExp.hasMatch(name) && !_keywords.contains(name); |
| |
| /// Convert a directory name into a reasonably legal pub package name. |
| String normalizeProjectName(String name) { |
| name = name.replaceAll('-', '_').replaceAll(' ', '_'); |
| // Strip any extension (like .dart). |
| var dotIndex = name.indexOf('.'); |
| if (dotIndex >= 0) { |
| name = name.substring(0, dotIndex); |
| } |
| return name; |
| } |
| |
| /// A utility class to generate a markdown table into a string. |
| /// |
| /// To use this class: |
| /// |
| /// ``` |
| /// var table = MarkdownTable(); |
| /// for (var foo in foos) { |
| /// table.startRow() |
| /// ..cell(foo.bar) |
| /// ..cell(foo.baz.toStringAsFixed(1), right: true) |
| /// ..cell(foo.qux); |
| /// } |
| /// print(table.finish()); |
| /// ``` |
| class MarkdownTable { |
| static const int defaultMaxWidth = 90; |
| static const int _minWidth = 3; |
| |
| final List<List<_MarkdownCell>> _data = []; |
| |
| MarkdownRow startRow() { |
| _data.add([]); |
| return MarkdownRow(this); |
| } |
| |
| String finish() { |
| if (_data.isEmpty) return ''; |
| var header = _data.first; |
| |
| var widths = <int>[]; |
| |
| for (int col = 0; col < header.length; col++) { |
| var width = _data.map((row) { |
| var item = row.length >= col ? row[col] : null; |
| return item?.value.length ?? 0; |
| }).reduce(math.max); |
| widths.add(math.max(width, _minWidth)); |
| } |
| |
| var buffer = StringBuffer(); |
| |
| for (var row in _data) { |
| buffer.write('| '); |
| for (int col = 0; col < row.length; col++) { |
| if (col != 0) buffer.write(' | '); |
| var cell = row[col]; |
| var width = math.min(widths[col], defaultMaxWidth); |
| var value = cell.value; |
| buffer.write(cell.right ? value.padLeft(width) : value.padRight(width)); |
| } |
| buffer.writeln(' |'); |
| |
| if (row == _data.first) { |
| // Write the alignment row. |
| buffer.write('| '); |
| for (int col = 0; col < row.length; col++) { |
| if (col != 0) buffer.write(' | '); |
| var cell = row[col]; |
| var width = math.min(widths[col], defaultMaxWidth); |
| var value = cell.right ? '--:' : '---'; |
| buffer.write(value.padLeft(width, '-')); |
| } |
| buffer.writeln(' |'); |
| } |
| } |
| |
| return buffer.toString(); |
| } |
| } |
| |
| /// Used to build a row in a markdown table. |
| class MarkdownRow { |
| final MarkdownTable _table; |
| |
| MarkdownRow(this._table); |
| |
| void cell(String value, {bool right = false}) { |
| _table._data.last.add(_MarkdownCell(value, right)); |
| } |
| } |
| |
| class _MarkdownCell { |
| final String value; |
| final bool right; |
| |
| _MarkdownCell(this.value, this.right); |
| } |
| |
| /// The lowest macOS version that Dart supports. |
| /// |
| /// From https://dart.dev/get-dart#system-requirements |
| const minimumSupportedMacOSVersion = 12; |