Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow invoking getExecutableForCommand everywhere in workspace #4257

Merged
merged 7 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
137 changes: 111 additions & 26 deletions lib/src/entrypoint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -740,22 +740,25 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without
///
/// If [onlyOutputWhenTerminal] is `true` (the default) there will be no
/// output if no terminal is attached.
static Future<PackageConfig> ensureUpToDate(
///
/// When succesfull returns the found/created `PackageConfig` and the
/// directory containing it.
static Future<({PackageConfig packageConfig, String rootDir})> ensureUpToDate(
String dir, {
required SystemCache cache,
bool summaryOnly = true,
bool onlyOutputWhenTerminal = true,
}) async {
final lockFilePath = p.normalize(p.join(dir, 'pubspec.lock'));
final packageConfigPath =
p.normalize(p.join(dir, '.dart_tool', 'package_config.json'));

/// Whether the lockfile is out of date with respect to the dependencies'
/// pubspecs.
///
/// If any mutable pubspec contains dependencies that are not in the lockfile
/// or that don't match what's in there, this will return `false`.
bool isLockFileUpToDate(LockFile lockFile, Package root) {
bool isLockFileUpToDate(
LockFile lockFile,
Package root, {
required String lockFilePath,
}) {
/// Returns whether the locked version of [dep] matches the dependency.
bool isDependencyUpToDate(PackageRange dep) {
if (dep.name == root.name) return true;
Expand Down Expand Up @@ -827,16 +830,20 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without
bool isPackageConfigUpToDate(
PackageConfig packageConfig,
LockFile lockFile,
Package root,
) {
Package root, {
required String packageConfigPath,
required String lockFilePath,
}) {
/// Determines if [lockFile] agrees with the given [packagePathsMapping].
///
/// The [packagePathsMapping] is a mapping from package names to paths where
/// the packages are located. (The library is located under
/// `lib/` relative to the path given).
bool isPackagePathsMappingUpToDateWithLockfile(
Map<String, String> packagePathsMapping,
) {
Map<String, String> packagePathsMapping, {
required String lockFilePath,
required String packageConfigPath,
}) {
// Check that [packagePathsMapping] does not contain more packages than what
// is required. This could lead to import statements working, when they are
// not supposed to work.
Expand Down Expand Up @@ -901,7 +908,11 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without
packagePathsMapping[pkg.name] =
root.path('.dart_tool', p.fromUri(pkg.rootUri));
}
if (!isPackagePathsMappingUpToDateWithLockfile(packagePathsMapping)) {
if (!isPackagePathsMappingUpToDateWithLockfile(
packagePathsMapping,
packageConfigPath: packageConfigPath,
lockFilePath: lockFilePath,
)) {
log.fine('The $lockFilePath file has changed since the '
'$packageConfigPath file '
'was generated, please run "$topLevelProgram pub get" again.');
Expand Down Expand Up @@ -952,8 +963,9 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without
}

/// The [PackageConfig] object representing `.dart_tool/package_config.json`
/// if it and `pubspec.lock` exist and are up to date with respect to
/// pubspec.yaml and its dependencies. Or `null` if it is outdate
/// along with the dir where it resides, if it and `pubspec.lock` exist and
/// are up to date with respect to pubspec.yaml and its dependencies. Or
/// `null` if it is outdated.
///
/// Always returns `null` if `.dart_tool/package_config.json` was generated
/// with a different PUB_CACHE location, a different $FLUTTER_ROOT or a
Expand Down Expand Up @@ -986,11 +998,80 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without
/// `touch pubspec.lock; touch .dart_tool/package_config.json`) - that is
/// hard to avoid, but also unlikely to happen by accident because
/// `.dart_tool/package_config.json` is not checked into version control.
PackageConfig? isResolutionUpToDate() {
(PackageConfig, String)? isResolutionUpToDate() {
FileStat? packageConfigStat;
late final String packageConfigPath;
late final String rootDir;
for (final parent in parentDirs(dir)) {
final potentialPackageConfigPath =
p.normalize(p.join(parent, '.dart_tool', 'package_config.json'));
packageConfigStat = tryStatFile(potentialPackageConfigPath);

if (packageConfigStat != null) {
packageConfigPath = potentialPackageConfigPath;
rootDir = parent;
break;
}
final potentialPubspacPath = p.join(parent, 'pubspec.yaml');
if (tryStatFile(potentialPubspacPath) == null) {
// No package at [parent] continue to next dir.
continue;
}

final potentialWorkspaceRefPath = p.normalize(
p.join(parent, '.dart_tool', 'pub', 'workspace_ref.json'),
);

final workspaceRefText = tryReadTextFile(potentialWorkspaceRefPath);
if (workspaceRefText == null) {
log.fine(
'`$potentialPubspacPath` exists without corresponding `$potentialPubspacPath` or `$potentialWorkspaceRefPath`.',
);
return null;
} else {
try {
switch (jsonDecode(workspaceRefText)) {
case {'workspaceRoot': final String path}:
final potentialPackageConfigPath2 = p.normalize(
p.join(
p.dirname(potentialWorkspaceRefPath),
workspaceRefText,
path,
'.dart_tool',
'package_config.json',
),
);
packageConfigStat = tryStatFile(potentialPackageConfigPath2);
if (packageConfigStat == null) {
log.fine(
'`$potentialWorkspaceRefPath` points to non-existing $potentialPackageConfigPath2',
);
return null;
}
case _:
log.fine(
'`$potentialWorkspaceRefPath` is missing "workspaceRoot" property',
);
}
} on FormatException catch (e) {
log.fine(
'`$potentialWorkspaceRefPath` not valid json: $e.',
);
return null;
}
}
}
if (packageConfigStat == null) {
log.fine(
'Found no .dart_tool/package_config.json - no existing resolution.',
);
return null;
}
final lockFilePath = p.normalize(p.join(rootDir, 'pubspec.lock'));
late final packageConfig = _loadPackageConfig(packageConfigPath);
if (p.isWithin(cache.rootDir, packageConfigPath)) {
// We always consider a global package (inside the cache) up-to-date.
return packageConfig;
return (packageConfig, rootDir);
}

/// Whether or not the `.dart_tool/package_config.json` file is was
Expand All @@ -1006,11 +1087,6 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without
return true;
}

final packageConfigStat = tryStatFile(packageConfigPath);
if (packageConfigStat == null) {
log.fine('No $packageConfigPath file found".\n');
return null;
}
final flutter = FlutterSdk();
// If Flutter has moved since last invocation, we want to have new
// sdk-packages, and therefore do a new resolution.
Expand Down Expand Up @@ -1097,7 +1173,7 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without
);

if (!lockfileNewerThanPubspecs) {
if (isLockFileUpToDate(lockFile, root)) {
if (isLockFileUpToDate(lockFile, root, lockFilePath: lockFilePath)) {
touch(lockFilePath);
touchedLockFile = true;
} else {
Expand All @@ -1108,13 +1184,19 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without
if (touchedLockFile ||
lockFileModified.isAfter(packageConfigStat.modified)) {
log.fine('`$lockFilePath` is newer than `$packageConfigPath`');
if (isPackageConfigUpToDate(packageConfig, lockFile, root)) {
if (isPackageConfigUpToDate(
packageConfig,
lockFile,
root,
packageConfigPath: packageConfigPath,
lockFilePath: lockFilePath,
)) {
touch(packageConfigPath);
} else {
return null;
}
}
return packageConfig;
return (packageConfig, rootDir);
}

switch (isResolutionUpToDate()) {
Expand All @@ -1137,10 +1219,13 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without
summaryOnly: summaryOnly,
);
}
return entrypoint.packageConfig;
case final PackageConfig packageConfig:
return (
packageConfig: entrypoint.packageConfig,
rootDir: entrypoint.workspaceRoot.dir
);
case (final PackageConfig packageConfig, final String rootDir):
log.fine('Package Config up to date.');
return packageConfig;
return (packageConfig: packageConfig, rootDir: rootDir);
}
}

Expand Down
91 changes: 55 additions & 36 deletions lib/src/executable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -240,19 +240,20 @@ final class DartExecutableWithPackageConfig {
///
/// [descriptor] is resolved as follows:
/// * If `<descriptor>` is an existing file (resolved relative to root, either
/// as a path or a file uri):
/// return that (without snapshotting).
/// as a path or a file uri): return that file with a `null` packageConfig.
///
/// * Otherwise if [root] contains no `pubspec.yaml`, throws a
/// [CommandResolutionFailedException].
/// * Otherwise if it looks like a file name (ends with '.dart' or contains a
/// '/' or a r'\') throw a [CommandResolutionFailedException]. (This is for
/// more clear error messages).
///
/// * Otherwise if the current package resolution is outdated do an implicit
/// `pub get`, if that fails, throw a [CommandResolutionFailedException].
/// * Otherwise call [Entrypoint.ensureUpToDate] in the current directory to
/// obtain a package config. If that fails, return a
/// [CommandResolutionFailedException].
///
/// * Otherwise let `<current>` be the name of the package at [root], and
/// interpret [descriptor] as `[<package>][:<command>]`.
/// * Otherwise let `<current>` be the name of the innermost package containing
/// [root], and interpret [descriptor] as `[<package>][:<command>]`.
///
/// * If `<package>` is empty: default to the package at [root].
/// * If `<package>` is empty: default to the package at [current].
/// * If `<command>` is empty, resolve it as `bin/<package>.dart` or
/// `bin/main.dart` to the first that exists.
///
Expand All @@ -270,8 +271,8 @@ final class DartExecutableWithPackageConfig {
/// the package is an immutable (non-path) dependency of [root].
///
/// If returning the path to a snapshot that doesn't already exist, the script
/// Will be built. And a message will be printed only if a terminal is
/// attached to stdout.
/// Will be built. And a message will be printed only if a terminal is attached
/// to stdout.
///
/// Throws an [CommandResolutionFailedException] if the command is not found or
/// if the entrypoint is not up to date (requires `pub get`) and a `pub get`.
Expand All @@ -280,8 +281,8 @@ final class DartExecutableWithPackageConfig {
/// additional source files into compilation even if they are not referenced
/// from the main library that [descriptor] resolves to.
///
/// The [nativeAssets], if provided, instructs the compiler to include
/// the native-assets mapping for @Native external functions.
/// The [nativeAssets], if provided, instructs the compiler to include the
/// native-assets mapping for @Native external functions.
Future<DartExecutableWithPackageConfig> getExecutableForCommand(
String descriptor, {
bool allowSnapshot = true,
Expand All @@ -305,19 +306,20 @@ Future<DartExecutableWithPackageConfig> getExecutableForCommand(
final asDirectFile = p.join(rootOrCurrent, asPath);
if (fileExists(asDirectFile)) {
return DartExecutableWithPackageConfig(
executable: p.relative(asDirectFile, from: rootOrCurrent),
executable: p.normalize(p.relative(asDirectFile, from: rootOrCurrent)),
packageConfig: null,
);
}
if (!fileExists(p.join(rootOrCurrent, 'pubspec.yaml'))) {
} else if (_looksLikeFile(asPath)) {
throw CommandResolutionFailedException._(
'Could not find file `$descriptor`',
CommandResolutionIssue.fileNotFound,
);
}
final PackageConfig packageConfig;
final String workspaceRootDir;
try {
packageConfig = await Entrypoint.ensureUpToDate(
(packageConfig: packageConfig, rootDir: workspaceRootDir) =
await Entrypoint.ensureUpToDate(
rootOrCurrent,
cache: SystemCache(rootDir: pubCacheDir),
);
Expand All @@ -327,20 +329,27 @@ Future<DartExecutableWithPackageConfig> getExecutableForCommand(
CommandResolutionIssue.pubGetFailed,
);
}
// TODO(https://github.com/dart-lang/pub/issues/4127): for workspaces: close
// the nearest enclosing package. That is the "current package" the one to
// default to.
late final rootPackageName = packageConfig.packages
.firstWhereOrNull(
(package) => p.equals(
p.join(rootOrCurrent, '.dart_tool', p.fromUri(package.rootUri)),
rootOrCurrent,
),
)
?.name;
// Find the first directory from [rootOrCurrent] to [workspaceRootDir] (both
// inclusive) that contains a package from the package config.
String? rootPackageName;
for (final parent in parentDirs(rootOrCurrent)) {
final rootPackage = packageConfig.packages.firstWhereOrNull(
(package) => p.equals(
p.join(workspaceRootDir, '.dart_tool', p.fromUri(package.rootUri)),
parent,
),
);
if (rootPackage != null) {
rootPackageName = rootPackage.name;
break;
}
if (p.equals(parent, workspaceRootDir)) {
break;
}
}
if (rootPackageName == null) {
throw CommandResolutionFailedException._(
'.dart_tool/package_config did not contain the root package',
'${p.join(workspaceRootDir, '.dart_tool', 'package_config.json')} did not contain its own root package',
CommandResolutionIssue.fileNotFound,
);
}
Expand Down Expand Up @@ -371,9 +380,13 @@ Future<DartExecutableWithPackageConfig> getExecutableForCommand(
}
final executable = Executable(package, p.join('bin', '$command.dart'));

final packageConfigPath = p.relative(
p.join(rootOrCurrent, '.dart_tool', 'package_config.json'),
from: rootOrCurrent,
final packageConfigPath = p.normalize(
p.join(
rootOrCurrent,
workspaceRootDir,
'.dart_tool',
'package_config.json',
),
);
final path = executable.resolve(packageConfig, packageConfigPath);
if (!fileExists(p.join(rootOrCurrent, path))) {
Expand All @@ -384,8 +397,8 @@ Future<DartExecutableWithPackageConfig> getExecutableForCommand(
}
if (!allowSnapshot) {
return DartExecutableWithPackageConfig(
executable: p.relative(path, from: rootOrCurrent),
packageConfig: packageConfigPath,
executable: p.normalize(p.relative(path, from: rootOrCurrent)),
packageConfig: p.relative(packageConfigPath, from: rootOrCurrent),
);
} else {
// TODO(sigurdm): attempt to decide on package mutability without looking at
Expand Down Expand Up @@ -419,12 +432,18 @@ Future<DartExecutableWithPackageConfig> getExecutableForCommand(
}
}
return DartExecutableWithPackageConfig(
executable: p.relative(snapshotPath, from: rootOrCurrent),
packageConfig: packageConfigPath,
executable: p.normalize(p.relative(snapshotPath, from: rootOrCurrent)),
packageConfig: p.relative(packageConfigPath, from: rootOrCurrent),
);
}
}

bool _looksLikeFile(String candidate) {
return candidate.contains('/') ||
candidate.endsWith('.dart') ||
candidate.endsWith('.snapshot');
}

/// Information on why no executable is returned.
enum CommandResolutionIssue {
/// The command string looked like a file (contained '.' '/' or '\\'), but no
Expand Down