blob: 4888a3fb3f7ffd85bcbea4949d358e792782ceb9 [file] [log] [blame]
// 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:convert';
import 'dart:io';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:nnbd_migration/src/fantasyland/fantasy_repo.dart';
import 'package:nnbd_migration/src/fantasyland/fantasy_repo_impl.dart';
import 'package:nnbd_migration/src/fantasyland/fantasy_sub_package.dart';
import 'package:nnbd_migration/src/fantasyland/fantasy_workspace.dart';
import 'package:nnbd_migration/src/utilities/multi_future_tracker.dart';
import 'package:nnbd_migration/src/utilities/subprocess_launcher.dart';
import 'package:path/path.dart' as path;
class FantasyWorkspaceError extends Error {
final String message;
FantasyWorkspaceError(this.message);
@override
String toString() => message;
}
// TODO(jcollins-g): consider refactor that makes resourceProvider required.
class FantasyWorkspaceDependencies {
final Future<FantasyRepo> Function(FantasyRepoSettings, String, bool,
{FantasyRepoDependencies fantasyRepoDependencies}) buildGitRepoFrom;
final ResourceProvider resourceProvider;
final SubprocessLauncher launcher;
FantasyWorkspaceDependencies(
{ResourceProvider resourceProvider,
SubprocessLauncher launcher,
Future<FantasyRepo> Function(FantasyRepoSettings, String, bool,
{FantasyRepoDependencies fantasyRepoDependencies})
buildGitRepoFrom,
List<String> dartdevExec})
: resourceProvider =
resourceProvider ?? PhysicalResourceProvider.INSTANCE,
launcher = launcher ?? SubprocessLauncher('fantasy-workspace'),
buildGitRepoFrom = buildGitRepoFrom ?? FantasyRepo.buildGitRepoFrom;
}
abstract class FantasyWorkspaceBase extends FantasyWorkspace {
final String workspaceRootPath;
final FantasyWorkspaceDependencies _external;
FantasyWorkspaceBase._(this.workspaceRootPath,
{FantasyWorkspaceDependencies workspaceDependencies})
: _external = workspaceDependencies ?? FantasyWorkspaceDependencies() {
if (!_external.resourceProvider.pathContext.isAbsolute(workspaceRootPath)) {
throw FantasyWorkspaceError('workspaceRootPath must be absolute');
}
}
MultiFutureTracker _packageConfigLock = MultiFutureTracker(1);
/// Repositories on which [addRepoToWorkspace] has been called.
Map<String, Future<FantasyRepo>> _repos = {};
/// Fully initialized subpackages.
///
/// This is complete once all [addPackageNameToWorkspace] futures are complete.
/// futures are complete. Packages may appear here early.
Map<FantasySubPackageSettings, FantasySubPackage> subPackages = {};
File _packagesFile;
File get packagesFile => _packagesFile ??= _external.resourceProvider.getFile(
_external.resourceProvider.pathContext
.join(workspaceRootPath, '.packages'));
File _packageConfigJson;
File get packageConfigJson => _packageConfigJson ??=
_external.resourceProvider.getFile(_external.resourceProvider.pathContext
.join(workspaceRootPath, '.dart_tool', 'package_config.json'));
File _migratedPackagesFile;
// TODO(jcollins-g): Remove this hack once a good way of determining whether
// a package is already migrated is available. (and our front-end implements it)
File get migratedPackagesFile => _migratedPackagesFile ??=
_external.resourceProvider.getFile(_external.resourceProvider.pathContext
.join(workspaceRootPath, '.steamroller_already_migrated'));
Set<String> _migratedPackagePaths;
Set<String> get migratedPackagePaths =>
_migratedPackagePaths ??= migratedPackagesFile.exists
? migratedPackagesFile.readAsStringSync().split('\n').toSet()
: Set();
/// Call this after migration has completed successfully for [packages].
void packagesMigrated(Iterable<FantasySubPackage> packages) {
// TODO(jcollins-g): Remove this hack once a reliable way of determining whether
// a package is already migrated is available (and our front-end implements it)
_migratedPackagePaths.addAll(packages.map((p) => p.packageRoot.path));
migratedPackagesFile.writeAsStringSync(_migratedPackagePaths.join('\n'));
}
/// The returned future should complete only when this package's repository
/// is:
///
/// cloned
/// up to date
/// added to the global .packages
/// symlinked into the workspace
/// has a [FantasySubPackage] assigned to its key in [subPackages].
///
/// Returns a list of [FantasySubPackageSettings] that needed to be added as
/// dependencies.
///
/// Which dependencies are automatically added is implementation dependent.
Future<void> addPackageNameToWorkspace(String packageName, bool allowUpdate);
Future<FantasySubPackage> addPackageToWorkspace(
FantasySubPackageSettings packageSettings, bool allowUpdate) async {
FantasyRepo containingRepo =
await addRepoToWorkspace(packageSettings.repoSettings, allowUpdate);
FantasySubPackage fantasySubPackage =
FantasySubPackage(packageSettings, containingRepo);
// TODO(jcollins-g): throw if double add
subPackages[packageSettings] = fantasySubPackage;
fantasySubPackage.cleanUp();
return fantasySubPackage;
}
@override
Future<bool> forceMigratePackages(
Iterable<FantasySubPackage> subPackages,
Iterable<FantasySubPackage> subPackagesLibOnly,
List<String> dartdevExec) async {
String dartdevBin = dartdevExec.first;
List<String> args = dartdevExec.sublist(1);
args.addAll(
['migrate', '--no-web-preview', '--apply-changes', '--ignore-errors']);
bool migrationNecessary = false;
// TODO(jcollins-g): consider using the package graph to break up and
// parallelize dartdev migrate runs
for (FantasySubPackage subPackage in subPackages) {
if (!migratedPackagePaths.contains(subPackage.packageRoot.path)) {
args.add(subPackage.packageRoot.path);
migrationNecessary = true;
}
}
for (FantasySubPackage subPackage in subPackagesLibOnly) {
if (!migratedPackagePaths.contains(subPackage.packageRoot.path)) {
args.add(subPackage.packageRoot.getChildAssumingFolder('lib').path);
migrationNecessary = true;
}
}
if (migrationNecessary) {
await _external.launcher
.runStreamed(dartdevBin, args, instance: 'dartdev');
}
// Update the file once we're sure it has completed successfully.
packagesMigrated(subPackages);
packagesMigrated(subPackagesLibOnly);
return migrationNecessary;
}
@override
Future<void> analyzePackages(
Iterable<FantasySubPackage> subPackages,
Iterable<FantasySubPackage> subPackagesLibOnly,
List<String> dartanalyzerExec) async {
var sdkPath = path.dirname(path.dirname(Platform.resolvedExecutable));
var analyzers = <Future>[];
String dartanalyzer_bin = dartanalyzerExec.first;
List<String> baseArgs = dartanalyzerExec.sublist(1);
baseArgs
.addAll(['--enable-experiment=non-nullable', '--dart-sdk=$sdkPath']);
Future<void> _spawn(
FantasySubPackage subPackage, List<String> allArgs) async {
return _external.launcher.runStreamed(dartanalyzer_bin, allArgs,
workingDirectory: subPackage.packageRoot.path,
instance: subPackage.name,
allowNonzeroExit: true);
}
for (FantasySubPackage subPackage in subPackages) {
List<String> allArgs = baseArgs.followedBy(['.']).toList();
analyzers.add(_spawn(subPackage, allArgs));
}
for (FantasySubPackage subPackage in subPackagesLibOnly) {
List<String> allArgs = baseArgs.followedBy(['lib']).toList();
analyzers.add(_spawn(subPackage, allArgs));
}
return Future.wait(analyzers);
}
static const _repoSubDir = '_repo';
/// Add one repository to the workspace.
///
/// If allowUpdate is true, the repository will be pulled before being
/// synced.
///
/// The returned [Future] completes when the repository is synced and cloned.
Future<FantasyRepo> addRepoToWorkspace(
FantasyRepoSettings repoSettings, bool allowUpdate) {
if (_repos.containsKey(repoSettings.name)) return _repos[repoSettings.name];
Folder repoRoot = _external.resourceProvider.getFolder(_external
.resourceProvider.pathContext
.canonicalize(_external.resourceProvider.pathContext
.join(workspaceRootPath, _repoSubDir, repoSettings.name)));
_repos[repoSettings.name] = _external.buildGitRepoFrom(
repoSettings, repoRoot.path, allowUpdate,
fantasyRepoDependencies:
FantasyRepoDependencies.fromWorkspaceDependencies(_external));
return _repos[repoSettings.name];
}
@override
Future<void> rewritePackageConfigWith(FantasySubPackage subPackage) async {
return _packageConfigLock.runFutureFromClosure(
() async => _rewritePackageConfigWith(subPackage));
}
// Only one [_rewritePackageConfigWith] should be running at a time
// per workspace.
Future<void> _rewritePackageConfigWith(FantasySubPackage subPackage) async {
if (packagesFile.exists) {
// A rogue .packages file can signal to tools the absence of a
// [FantasySubPackage.languageVersion]. This paradoxically will mean
// to our tools that all language features, including NNBD, are enabled,
// which is not necessarily what we want. It is safer to delete this to
// prevent it from being used accidentally.
packagesFile.delete();
}
Map<String, Object> packageConfigMap = {
"configVersion": 2,
"packages": <Map<String, String>>[],
};
if (packageConfigJson.exists) {
packageConfigMap = jsonDecode(packageConfigJson.readAsStringSync())
as Map<String, Object>;
}
packageConfigMap['generated'] = DateTime.now().toIso8601String();
packageConfigMap['generator'] = 'fantasyland';
// TODO(jcollins-g): analyzer seems to depend on this and ignore some versions
packageConfigMap['generatorVersion'] = "2.8.0-dev.9999.0";
var packages = packageConfigMap['packages'] as List;
var rewriteMe =
packages.firstWhere((p) => p['name'] == subPackage.name, orElse: () {
Map<String, String> newMap = {};
packages.add(newMap);
return newMap;
});
rewriteMe['name'] = subPackage.name;
String subPackageRootUriString =
subPackage.packageRoot.toUri().normalizePath().toString();
if (subPackageRootUriString.endsWith('/')) {
subPackageRootUriString = subPackageRootUriString.substring(
0, subPackageRootUriString.length - 1);
}
rewriteMe['rootUri'] = subPackageRootUriString;
// TODO(jcollins-g): is this ever anything different?
rewriteMe['packageUri'] = 'lib/';
if (subPackage.languageVersion != null) {
rewriteMe['languageVersion'] = subPackage.languageVersion;
} else {
rewriteMe.remove('languageVersion');
}
packageConfigJson.parent.create();
JsonEncoder encoder = new JsonEncoder.withIndent(" ");
packageConfigJson.writeAsStringSync(encoder.convert(packageConfigMap));
}
}
/// Represents a [FantasyWorkspaceBase] that only fetches dev_dependencies
/// for the top level package.
class FantasyWorkspaceTopLevelDevDepsImpl extends FantasyWorkspaceBase {
final String topLevelPackage;
FantasyWorkspaceTopLevelDevDepsImpl._(
this.topLevelPackage, String workspaceRootPath,
{FantasyWorkspaceDependencies workspaceDependencies})
: super._(workspaceRootPath,
workspaceDependencies: workspaceDependencies);
static Future<FantasyWorkspace> buildFor(
String topLevelPackage,
List<String> extraPackageNames,
String workspaceRootPath,
bool allowUpdate,
{FantasyWorkspaceDependencies workspaceDependencies}) async {
var workspace = FantasyWorkspaceTopLevelDevDepsImpl._(
topLevelPackage, workspaceRootPath,
workspaceDependencies: workspaceDependencies);
await Future.wait([
for (var n in [topLevelPackage, ...extraPackageNames])
workspace.addPackageNameToWorkspace(n, allowUpdate)
]);
return workspace;
}
Future<FantasySubPackage> addPackageNameToWorkspace(
String packageName, bool allowUpdate) async {
FantasySubPackageSettings packageSettings =
FantasySubPackageSettings.fromName(packageName);
return await addPackageToWorkspace(packageSettings, allowUpdate);
}
@override
Future<FantasySubPackage> addPackageToWorkspace(
FantasySubPackageSettings packageSettings, bool allowUpdate,
{Set<FantasySubPackageSettings> seenPackages}) async {
seenPackages ??= {};
if (seenPackages.contains(packageSettings)) return null;
seenPackages.add(packageSettings);
return await _addPackageToWorkspace(
packageSettings, allowUpdate, seenPackages);
}
Future<FantasySubPackage> _addPackageToWorkspace(
FantasySubPackageSettings packageSettings,
bool allowUpdate,
Set<FantasySubPackageSettings> seenPackages) async {
FantasySubPackage fantasySubPackage =
await super.addPackageToWorkspace(packageSettings, allowUpdate);
String packageName = packageSettings.name;
await rewritePackageConfigWith(fantasySubPackage);
List<FantasySubPackageSettings> dependencies = [];
if (packageName == topLevelPackage) {
dependencies = await fantasySubPackage.getPackageAllDependencies();
} else {
dependencies = await fantasySubPackage.getPackageDependencies();
}
await Future.wait([
for (var subPackageSettings in dependencies)
addPackageToWorkspace(subPackageSettings, allowUpdate,
seenPackages: seenPackages)
]);
return fantasySubPackage;
}
}