blob: d3a7cffaf915ce3459bf0c9ce956b2cfbb0b6ad8 [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 '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:package_config/packages_file.dart' as packages_file;
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,
{FantasyRepoDependencies fantasyRepoDependencies}) buildGitRepoFrom;
final ResourceProvider resourceProvider;
final SubprocessLauncher launcher;
FantasyWorkspaceDependencies(
{ResourceProvider resourceProvider,
SubprocessLauncher launcher,
Future<FantasyRepo> Function(FantasyRepoSettings, String,
{FantasyRepoDependencies fantasyRepoDependencies})
buildGitRepoFrom})
: resourceProvider =
resourceProvider ?? PhysicalResourceProvider.INSTANCE,
launcher = launcher, // Pass through to FantasyRepoDependencies.
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'));
// TODO(jcollins-g): use package_config when pub package is updated, or
// implement writing for the analyzer version ourselves.
File get packageConfigJson => throw UnimplementedError();
/// 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);
Future<FantasySubPackage> addPackageToWorkspace(
FantasySubPackageSettings packageSettings) async {
FantasyRepo containingRepo =
await addRepoToWorkspace(packageSettings.repoSettings);
FantasySubPackage fantasySubPackage =
FantasySubPackage(packageSettings, containingRepo);
// TODO(jcollins-g): throw if double add
subPackages[packageSettings] = fantasySubPackage;
return fantasySubPackage;
}
static const _repoSubDir = '_repo';
/// Add one repository to the workspace.
///
/// The returned [Future] completes when the repository is synced and cloned.
Future<FantasyRepo> addRepoToWorkspace(FantasyRepoSettings repoSettings) {
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,
fantasyRepoDependencies:
FantasyRepoDependencies.fromWorkspaceDependencies(_external));
return _repos[repoSettings.name];
}
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 {
Map<String, Uri> uriMap = {};
if (packagesFile.exists) {
var uri = packagesFile.toUri();
var content = packagesFile.readAsBytesSync();
uriMap = packages_file.parse(content, uri);
}
uriMap[subPackage.name] =
subPackage.packageRoot.getChildAssumingFolder('lib').toUri();
StringBuffer buffer = StringBuffer();
packages_file.write(buffer, uriMap);
// TODO(jcollins-g): Consider accumulating rewrites rather than doing
// this once per package.
// TODO(jcollins-g): support package_config.json.
return packagesFile.writeAsStringSync(buffer.toString());
}
}
/// 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,
{FantasyWorkspaceDependencies workspaceDependencies}) async {
var workspace = FantasyWorkspaceTopLevelDevDepsImpl._(
topLevelPackage, workspaceRootPath,
workspaceDependencies: workspaceDependencies);
await Future.wait([
for (var n in [topLevelPackage, ...extraPackageNames])
workspace.addPackageNameToWorkspace(n)
]);
return workspace;
}
Future<FantasySubPackage> addPackageNameToWorkspace(
String packageName) async {
FantasySubPackageSettings packageSettings =
FantasySubPackageSettings.fromName(packageName);
return await addPackageToWorkspace(packageSettings);
}
@override
Future<FantasySubPackage> addPackageToWorkspace(
FantasySubPackageSettings packageSettings,
{Set<FantasySubPackageSettings> seenPackages}) async {
seenPackages ??= {};
if (seenPackages.contains(packageSettings)) return null;
seenPackages.add(packageSettings);
await _addPackageToWorkspace(packageSettings, seenPackages);
}
Future<FantasySubPackage> _addPackageToWorkspace(
FantasySubPackageSettings packageSettings,
Set<FantasySubPackageSettings> seenPackages) async {
FantasySubPackage fantasySubPackage =
await super.addPackageToWorkspace(packageSettings);
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, seenPackages: seenPackages)
]);
return fantasySubPackage;
}
}