blob: 36714c2b7532290d70c8424dd41c023c7ae94820 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. 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 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'package:process/process.dart';
import '../base/bot_detector.dart';
import '../base/common.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/io.dart' as io;
import '../base/io.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../cache.dart';
import '../convert.dart';
import '../dart/package_map.dart';
import '../project.dart';
import '../reporting/reporting.dart';
/// The [Pub] instance.
Pub get pub => context.get<Pub>()!;
/// The console environment key used by the pub tool.
const String _kPubEnvironmentKey = 'PUB_ENVIRONMENT';
/// The console environment key used by the pub tool to find the cache directory.
const String _kPubCacheEnvironmentKey = 'PUB_CACHE';
typedef MessageFilter = String? Function(String message);
bool _tryDeleteDirectory(Directory directory, Logger logger) {
try {
if (directory.existsSync()) {
directory.deleteSync(recursive: true);
}
} on FileSystemException {
logger.printWarning('Failed to delete directory at: ${directory.path}');
return false;
}
return true;
}
/// Represents Flutter-specific data that is added to the `PUB_ENVIRONMENT`
/// environment variable and allows understanding the type of requests made to
/// the package site on Flutter's behalf.
// DO NOT update without contacting kevmoo.
// We have server-side tooling that assumes the values are consistent.
class PubContext {
PubContext._(this._values) {
for (final String item in _values) {
if (!_validContext.hasMatch(item)) {
throw ArgumentError.value(
_values, 'value', 'Must match RegExp ${_validContext.pattern}');
}
}
}
static PubContext getVerifyContext(String commandName) =>
PubContext._(<String>['verify', commandName.replaceAll('-', '_')]);
static final PubContext create = PubContext._(<String>['create']);
static final PubContext createPackage = PubContext._(<String>['create_pkg']);
static final PubContext createPlugin = PubContext._(<String>['create_plugin']);
static final PubContext interactive = PubContext._(<String>['interactive']);
static final PubContext pubGet = PubContext._(<String>['get']);
static final PubContext pubUpgrade = PubContext._(<String>['upgrade']);
static final PubContext pubAdd = PubContext._(<String>['add']);
static final PubContext pubRemove = PubContext._(<String>['remove']);
static final PubContext pubForward = PubContext._(<String>['forward']);
static final PubContext pubPassThrough = PubContext._(<String>['passthrough']);
static final PubContext runTest = PubContext._(<String>['run_test']);
static final PubContext flutterTests = PubContext._(<String>['flutter_tests']);
static final PubContext updatePackages = PubContext._(<String>['update_packages']);
final List<String> _values;
static final RegExp _validContext = RegExp('[a-z][a-z_]*[a-z]');
@override
String toString() => 'PubContext: ${_values.join(':')}';
String toAnalyticsString() {
return _values.map((String s) => s.replaceAll('_', '-')).toList().join('-');
}
}
/// Describes the amount of output that should get printed from a `pub` command.
/// [PubOutputMode.all] indicates that the complete output is printed. This is
/// typically the default.
/// [PubOutputMode.none] indicates that no output should be printed.
/// [PubOutputMode.summaryOnly] indicates that only summary information should be printed.
enum PubOutputMode {
none,
all,
summaryOnly,
}
/// A handle for interacting with the pub tool.
abstract class Pub {
/// Create a default [Pub] instance.
factory Pub({
required FileSystem fileSystem,
required Logger logger,
required ProcessManager processManager,
required Platform platform,
required BotDetector botDetector,
required Usage usage,
}) = _DefaultPub;
/// Create a [Pub] instance with a mocked [stdio].
@visibleForTesting
factory Pub.test({
required FileSystem fileSystem,
required Logger logger,
required ProcessManager processManager,
required Platform platform,
required BotDetector botDetector,
required Usage usage,
required Stdio stdio,
}) = _DefaultPub.test;
/// Runs `pub get` for [project].
///
/// [context] provides extra information to package server requests to
/// understand usage.
///
/// If [shouldSkipThirdPartyGenerator] is true, the overall pub get will be
/// skipped if the package config file has a "generator" other than "pub".
/// Defaults to true.
///
/// [outputMode] determines how verbose the output from `pub get` will be.
/// If [PubOutputMode.all] is used, `pub get` will print its typical output
/// which includes information about all changed dependencies. If
/// [PubOutputMode.summaryOnly] is used, only summary information will be printed.
/// This is useful for cases where the user is typically not interested in
/// what dependencies were changed, such as when running `flutter create`.
///
/// Will also resolve dependencies in the example folder if present.
Future<void> get({
required PubContext context,
required FlutterProject project,
bool upgrade = false,
bool offline = false,
String? flutterRootOverride,
bool checkUpToDate = false,
bool shouldSkipThirdPartyGenerator = true,
PubOutputMode outputMode = PubOutputMode.all,
});
/// Runs pub in 'batch' mode.
///
/// forwarding complete lines written by pub to its stdout/stderr streams to
/// the corresponding stream of this process, optionally applying filtering.
/// The pub process will not receive anything on its stdin stream.
///
/// The `--trace` argument is passed to `pub` when `showTraceForErrors`
/// `isRunningOnBot` is true.
///
/// [context] provides extra information to package server requests to
/// understand usage.
Future<void> batch(
List<String> arguments, {
required PubContext context,
String? directory,
MessageFilter? filter,
String failureMessage = 'pub failed',
});
/// Runs pub in 'interactive' mode.
///
/// This will run the pub process with StdioInherited (unless [_stdio] is set
/// for testing).
///
/// The pub process will be run in current working directory, so `--directory`
/// should be passed appropriately in [arguments]. This ensures output from
/// pub will refer to relative paths correctly.
///
/// [touchesPackageConfig] should be true if this is a command expected to
/// create a new `.dart_tool/package_config.json` file.
Future<void> interactively(
List<String> arguments, {
FlutterProject? project,
required PubContext context,
required String command,
bool touchesPackageConfig = false,
bool generateSyntheticPackage = false,
PubOutputMode outputMode = PubOutputMode.all
});
}
class _DefaultPub implements Pub {
_DefaultPub({
required FileSystem fileSystem,
required Logger logger,
required ProcessManager processManager,
required Platform platform,
required BotDetector botDetector,
required Usage usage,
}) : _fileSystem = fileSystem,
_logger = logger,
_platform = platform,
_botDetector = botDetector,
_usage = usage,
_processUtils = ProcessUtils(
logger: logger,
processManager: processManager,
),
_processManager = processManager,
_stdio = null;
@visibleForTesting
_DefaultPub.test({
required FileSystem fileSystem,
required Logger logger,
required ProcessManager processManager,
required Platform platform,
required BotDetector botDetector,
required Usage usage,
required Stdio stdio,
}) : _fileSystem = fileSystem,
_logger = logger,
_platform = platform,
_botDetector = botDetector,
_usage = usage,
_processUtils = ProcessUtils(
logger: logger,
processManager: processManager,
),
_processManager = processManager,
_stdio = stdio;
final FileSystem _fileSystem;
final Logger _logger;
final ProcessUtils _processUtils;
final Platform _platform;
final BotDetector _botDetector;
final Usage _usage;
final ProcessManager _processManager;
final Stdio? _stdio;
@override
Future<void> get({
required PubContext context,
required FlutterProject project,
bool upgrade = false,
bool offline = false,
bool generateSyntheticPackage = false,
bool generateSyntheticPackageForExample = false,
String? flutterRootOverride,
bool checkUpToDate = false,
bool shouldSkipThirdPartyGenerator = true,
PubOutputMode outputMode = PubOutputMode.all,
}) async {
final String directory = project.directory.path;
final File packageConfigFile = project.packageConfigFile;
final File lastVersion = _fileSystem.file(
_fileSystem.path.join(directory, '.dart_tool', 'version'));
final File currentVersion = _fileSystem.file(
_fileSystem.path.join(Cache.flutterRoot!, 'version'));
final File pubspecYaml = project.pubspecFile;
final File pubLockFile = _fileSystem.file(
_fileSystem.path.join(directory, 'pubspec.lock')
);
if (shouldSkipThirdPartyGenerator && project.packageConfigFile.existsSync()) {
Map<String, Object?> packageConfigMap;
try {
packageConfigMap = jsonDecode(
project.packageConfigFile.readAsStringSync(),
) as Map<String, Object?>;
} on FormatException {
packageConfigMap = <String, Object?>{};
}
final bool isPackageConfigGeneratedByThirdParty =
packageConfigMap.containsKey('generator') &&
packageConfigMap['generator'] != 'pub';
if (isPackageConfigGeneratedByThirdParty) {
_logger.printTrace('Skipping pub get: generated by third-party.');
return;
}
}
// If the pubspec.yaml is older than the package config file and the last
// flutter version used is the same as the current version skip pub get.
// This will incorrectly skip pub on the master branch if dependencies
// are being added/removed from the flutter framework packages, but this
// can be worked around by manually running pub.
if (checkUpToDate &&
packageConfigFile.existsSync() &&
pubLockFile.existsSync() &&
pubspecYaml.lastModifiedSync().isBefore(pubLockFile.lastModifiedSync()) &&
pubspecYaml.lastModifiedSync().isBefore(packageConfigFile.lastModifiedSync()) &&
lastVersion.existsSync() &&
lastVersion.readAsStringSync() == currentVersion.readAsStringSync()) {
_logger.printTrace('Skipping pub get: version match.');
return;
}
final String command = upgrade ? 'upgrade' : 'get';
final bool verbose = _logger.isVerbose;
final List<String> args = <String>[
if (_logger.supportsColor)
'--color',
if (verbose)
'--verbose',
'--directory',
_fileSystem.path.relative(directory),
...<String>[
command,
],
if (offline)
'--offline',
'--example',
];
await _runWithStdioInherited(
args,
command: command,
context: context,
directory: directory,
failureMessage: 'pub $command failed',
flutterRootOverride: flutterRootOverride,
outputMode: outputMode,
);
await _updateVersionAndPackageConfig(project);
}
/// Runs pub with [arguments] and [ProcessStartMode.inheritStdio] mode.
///
/// Uses [ProcessStartMode.normal] and [Pub._stdio] if [Pub.test] constructor
/// was used.
///
/// Prints the stdout and stderr of the whole run, unless silenced using
/// [printProgress].
///
/// Sends an analytics event.
Future<void> _runWithStdioInherited(
List<String> arguments, {
required String command,
required PubOutputMode outputMode,
required PubContext context,
required String directory,
String failureMessage = 'pub failed',
String? flutterRootOverride,
}) async {
int exitCode;
final List<String> pubCommand = <String>[..._pubCommand, ...arguments];
final Map<String, String> pubEnvironment = await _createPubEnvironment(context: context, flutterRootOverride: flutterRootOverride, summaryOnly: outputMode == PubOutputMode.summaryOnly);
try {
if (outputMode != PubOutputMode.none) {
final io.Stdio? stdio = _stdio;
if (stdio == null) {
// Let pub inherit stdio and output directly to the tool's stdout and
// stderr handles.
final io.Process process = await _processUtils.start(
pubCommand,
workingDirectory: _fileSystem.path.current,
environment: pubEnvironment,
mode: ProcessStartMode.inheritStdio,
);
exitCode = await process.exitCode;
} else {
// Omit [mode] parameter to send output to [process.stdout] and
// [process.stderr].
final io.Process process = await _processUtils.start(
pubCommand,
workingDirectory: _fileSystem.path.current,
environment: pubEnvironment,
);
// Direct pub output to [Pub._stdio] for tests.
final StreamSubscription<List<int>> stdoutSubscription =
process.stdout.listen(stdio.stdout.add);
final StreamSubscription<List<int>> stderrSubscription =
process.stderr.listen(stdio.stderr.add);
await Future.wait<void>(<Future<void>>[
stdoutSubscription.asFuture<void>(),
stderrSubscription.asFuture<void>(),
]);
unawaited(stdoutSubscription.cancel());
unawaited(stderrSubscription.cancel());
exitCode = await process.exitCode;
}
} else {
// Do not try to use [ProcessUtils.start] here, because it requires you
// to read all data out of the stdout and stderr streams. If you don't
// read the streams, it may appear to work fine on your platform but
// will block the tool's process on Windows.
// See https://api.dart.dev/stable/dart-io/Process/start.html
//
// [ProcessUtils.run] will send the output to [result.stdout] and
// [result.stderr], which we will ignore.
final RunResult result = await _processUtils.run(
pubCommand,
workingDirectory: _fileSystem.path.current,
environment: pubEnvironment,
);
exitCode = result.exitCode;
}
// The exception is rethrown, so don't catch only Exceptions.
} catch (exception) { // ignore: avoid_catches_without_on_clauses
if (exception is io.ProcessException) {
final StringBuffer buffer = StringBuffer('${exception.message}\n');
final String directoryExistsMessage = _fileSystem.directory(directory).existsSync()
? 'exists'
: 'does not exist';
buffer.writeln('Working directory: "$directory" ($directoryExistsMessage)');
buffer.write(_stringifyPubEnv(pubEnvironment));
throw io.ProcessException(
exception.executable,
exception.arguments,
buffer.toString(),
exception.errorCode,
);
}
rethrow;
}
final int code = exitCode;
final String result = code == 0 ? 'success' : 'failure';
PubResultEvent(
context: context.toAnalyticsString(),
result: result,
usage: _usage,
).send();
if (code != 0) {
final StringBuffer buffer = StringBuffer('$failureMessage\n');
buffer.writeln('command: "${pubCommand.join(' ')}"');
buffer.write(_stringifyPubEnv(pubEnvironment));
buffer.writeln('exit code: $code');
_logger.printTrace(buffer.toString());
throwToolExit(null, exitCode: code);
}
}
// For surfacing pub env in crash reporting
String _stringifyPubEnv(Map<String, String> map, {String prefix = 'pub env'}) {
if (map.isEmpty) {
return '';
}
final StringBuffer buffer = StringBuffer();
buffer.writeln('$prefix: {');
for (final MapEntry<String, String> entry in map.entries) {
buffer.writeln(' "${entry.key}": "${entry.value}",');
}
buffer.writeln('}');
return buffer.toString();
}
@override
Future<void> batch(
List<String> arguments, {
required PubContext context,
String? directory,
MessageFilter? filter,
String failureMessage = 'pub failed',
String? flutterRootOverride,
}) async {
final bool showTraceForErrors = await _botDetector.isRunningOnBot;
String lastPubMessage = 'no message';
String? filterWrapper(String line) {
lastPubMessage = line;
if (filter == null) {
return line;
}
return filter(line);
}
if (showTraceForErrors) {
arguments.insert(0, '--trace');
}
final Map<String, String> pubEnvironment = await _createPubEnvironment(context: context, flutterRootOverride: flutterRootOverride);
final List<String> pubCommand = <String>[..._pubCommand, ...arguments];
final int code = await _processUtils.stream(
pubCommand,
workingDirectory: directory,
mapFunction: filterWrapper, // may set versionSolvingFailed, lastPubMessage
environment: pubEnvironment,
);
String result = 'success';
if (code != 0) {
result = 'failure';
}
PubResultEvent(
context: context.toAnalyticsString(),
result: result,
usage: _usage,
).send();
if (code != 0) {
final StringBuffer buffer = StringBuffer('$failureMessage\n');
buffer.writeln('command: "${pubCommand.join(' ')}"');
buffer.write(_stringifyPubEnv(pubEnvironment));
buffer.writeln('exit code: $code');
buffer.writeln('last line of pub output: "${lastPubMessage.trim()}"');
throwToolExit(
buffer.toString(),
exitCode: code,
);
}
}
@override
Future<void> interactively(
List<String> arguments, {
FlutterProject? project,
required PubContext context,
required String command,
bool touchesPackageConfig = false,
bool generateSyntheticPackage = false,
PubOutputMode outputMode = PubOutputMode.all
}) async {
await _runWithStdioInherited(
arguments,
command: command,
directory: _fileSystem.currentDirectory.path,
context: context,
outputMode: outputMode,
);
if (touchesPackageConfig && project != null) {
await _updateVersionAndPackageConfig(project);
}
}
/// The command used for running pub.
late final List<String> _pubCommand = _computePubCommand();
List<String> _computePubCommand() {
// TODO(zanderso): refactor to use artifacts.
final String sdkPath = _fileSystem.path.joinAll(<String>[
Cache.flutterRoot!,
'bin',
'cache',
'dart-sdk',
'bin',
'dart',
]);
if (!_processManager.canRun(sdkPath)) {
throwToolExit(
'Your Flutter SDK download may be corrupt or missing permissions to run. '
'Try re-downloading the Flutter SDK into a directory that has read/write '
'permissions for the current user.'
);
}
return <String>[sdkPath, 'pub', '--suppress-analytics'];
}
// Returns the environment value that should be used when running pub.
//
// Includes any existing environment variable, if one exists.
//
// [context] provides extra information to package server requests to
// understand usage.
Future<String> _getPubEnvironmentValue(PubContext pubContext) async {
// DO NOT update this function without contacting kevmoo.
// We have server-side tooling that assumes the values are consistent.
final String? existing = _platform.environment[_kPubEnvironmentKey];
final List<String> values = <String>[
if (existing != null && existing.isNotEmpty) existing,
if (await _botDetector.isRunningOnBot) 'flutter_bot',
'flutter_cli',
...pubContext._values,
];
return values.join(':');
}
/// There are 2 ways to get the pub cache location
///
/// 1) Provide the _kPubCacheEnvironmentKey.
/// 2) The pub default user-level pub cache.
///
/// If we are using 2, check if there are pre-packaged packages in
/// $FLUTTER_ROOT/.pub-preload-cache and install them in the user-level cache.
String? _getPubCacheIfAvailable() {
if (_platform.environment.containsKey(_kPubCacheEnvironmentKey)) {
return _platform.environment[_kPubCacheEnvironmentKey];
}
_preloadPubCache();
// Use pub's default location by returning null.
return null;
}
/// Load any package-files stored in FLUTTER_ROOT/.pub-preload-cache into the
/// pub cache if it exists.
///
/// Deletes the [preloadCacheDir].
void _preloadPubCache() {
final String flutterRootPath = Cache.flutterRoot!;
final Directory flutterRoot = _fileSystem.directory(flutterRootPath);
final Directory preloadCacheDir = flutterRoot.childDirectory('.pub-preload-cache');
if (preloadCacheDir.existsSync()) {
/// We only want to inform about existing caches on first run of a freshly
/// downloaded Flutter SDK. Therefore it is conditioned on the existence
/// of the .pub-preload-cache dir.
final Iterable<String> cacheFiles =
preloadCacheDir
.listSync()
.map((FileSystemEntity f) => f.path)
.where((String path) => path.endsWith('.tar.gz'));
_processManager.runSync(<String>[..._pubCommand, 'cache', 'preload', ...cacheFiles]);
_tryDeleteDirectory(preloadCacheDir, _logger);
}
}
/// The full environment used when running pub.
///
/// [context] provides extra information to package server requests to
/// understand usage.
Future<Map<String, String>> _createPubEnvironment({
required PubContext context,
String? flutterRootOverride,
bool? summaryOnly = false,
}) async {
final Map<String, String> environment = <String, String>{
'FLUTTER_ROOT': flutterRootOverride ?? Cache.flutterRoot!,
_kPubEnvironmentKey: await _getPubEnvironmentValue(context),
if (summaryOnly ?? false) 'PUB_SUMMARY_ONLY': '1',
};
final String? pubCache = _getPubCacheIfAvailable();
if (pubCache != null) {
environment[_kPubCacheEnvironmentKey] = pubCache;
}
return environment;
}
/// Updates the .dart_tool/version file to be equal to current Flutter
/// version.
///
/// Calls [_updatePackageConfig] for [project] and [project.example] (if it
/// exists).
///
/// This should be called after pub invocations that are expected to update
/// the packageConfig.
Future<void> _updateVersionAndPackageConfig(FlutterProject project) async {
if (!project.packageConfigFile.existsSync()) {
throwToolExit('${project.directory}: pub did not create .dart_tools/package_config.json file.');
}
final File lastVersion = _fileSystem.file(
_fileSystem.path.join(project.directory.path, '.dart_tool', 'version'),
);
final File currentVersion = _fileSystem.file(
_fileSystem.path.join(Cache.flutterRoot!, 'version'));
lastVersion.writeAsStringSync(currentVersion.readAsStringSync());
await _updatePackageConfig(project);
if (project.hasExampleApp && project.example.pubspecFile.existsSync()) {
await _updatePackageConfig(project.example);
}
}
/// Update the package configuration file in [project].
///
/// Creates a corresponding `package_config_subset` file that is used by the
/// build system to avoid rebuilds caused by an updated pub timestamp.
///
/// if `project.generateSyntheticPackage` is `true` then insert flutter_gen
/// synthetic package into the package configuration. This is used by the l10n
/// localization tooling to insert a new reference into the package_config
/// file, allowing the import of a package URI that is not specified in the
/// pubspec.yaml
///
/// For more information, see:
/// * [generateLocalizations], `in lib/src/localizations/gen_l10n.dart`
Future<void> _updatePackageConfig(FlutterProject project) async {
final File packageConfigFile = project.packageConfigFile;
final PackageConfig packageConfig = await loadPackageConfigWithLogging(packageConfigFile, logger: _logger);
packageConfigFile.parent
.childFile('package_config_subset')
.writeAsStringSync(_computePackageConfigSubset(
packageConfig,
_fileSystem,
));
if (!project.manifest.generateSyntheticPackage) {
return;
}
if (packageConfig.packages.any((Package package) => package.name == 'flutter_gen')) {
return;
}
// TODO(jonahwillams): Using raw json manipulation here because
// savePackageConfig always writes to local io, and it changes absolute
// paths to relative on round trip.
// See: https://github.com/dart-lang/package_config/issues/99,
// and: https://github.com/dart-lang/package_config/issues/100.
// Because [loadPackageConfigWithLogging] succeeded [packageConfigFile]
// we can rely on the file to exist and be correctly formatted.
final Map<String, dynamic> jsonContents =
json.decode(packageConfigFile.readAsStringSync()) as Map<String, dynamic>;
(jsonContents['packages'] as List<dynamic>).add(<String, dynamic>{
'name': 'flutter_gen',
'rootUri': 'flutter_gen',
'languageVersion': '2.12',
});
packageConfigFile.writeAsStringSync(json.encode(jsonContents));
}
// Subset the package config file to only the parts that are relevant for
// rerunning the dart compiler.
String _computePackageConfigSubset(PackageConfig packageConfig, FileSystem fileSystem) {
final StringBuffer buffer = StringBuffer();
for (final Package package in packageConfig.packages) {
buffer.writeln(package.name);
buffer.writeln(package.languageVersion);
buffer.writeln(package.root);
buffer.writeln(package.packageUriRoot);
}
buffer.writeln(packageConfig.version);
return buffer.toString();
}
}