Skip to content

Allow invoking getExecutableForCommand everywhere in workspace #4257

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

Merged
merged 7 commits into from
May 13, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 159 additions & 46 deletions lib/src/entrypoint.dart
Original file line number Diff line number Diff line change
@@ -740,22 +740,29 @@ 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'));
late final wasRelative = p.isRelative(dir);
String relativeIfNeeded(String path) =>
wasRelative ? p.relative(path) : path;

/// 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;
@@ -827,16 +834,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.
@@ -901,7 +912,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.');
@@ -952,8 +967,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
@@ -971,7 +987,8 @@ To update `$lockFilePath` run `$topLevelProgram pub get`$suffix without
/// where version control or other processes mess up the timestamp order.
///
/// If the resolution is still valid, the timestamps are updated and this
/// returns `true`. Otherwise this returns `false`.
/// returns the package configuration and the root dir. Otherwise this
/// returns `null`.
///
/// This check is on the fast-path of `dart run` and should do as little
/// work as possible. Specifically we avoid parsing any yaml when the
@@ -986,11 +1003,100 @@ 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 {
if (jsonDecode(workspaceRefText)
case {'workspaceRoot': final String path}) {
final potentialPackageConfigPath2 = relativeIfNeeded(
p.normalize(
p.absolute(
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;
} else {
packageConfigPath = potentialPackageConfigPath2;
rootDir = relativeIfNeeded(
p.normalize(
p.absolute(
p.join(
p.dirname(potentialWorkspaceRefPath),
workspaceRefText,
path,
),
),
),
);

break;
}
} else {
log.fine(
'`$potentialWorkspaceRefPath` is missing "workspaceRoot" property',
);
return null;
}
} 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
@@ -1006,11 +1112,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.
@@ -1097,7 +1198,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 {
@@ -1108,40 +1209,52 @@ 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()) {
case null:
final entrypoint = Entrypoint(
dir, cache,
// [ensureUpToDate] is also used for entries in 'global_packages/'
checkInCache: false,
if (isResolutionUpToDate()
case (final PackageConfig packageConfig, final String rootDir)) {
log.fine('Package Config up to date.');
return (packageConfig: packageConfig, rootDir: rootDir);
}
final entrypoint = Entrypoint(
dir, cache,
// [ensureUpToDate] is also used for entries in 'global_packages/'
checkInCache: false,
);
if (onlyOutputWhenTerminal) {
await log.errorsOnlyUnlessTerminal(() async {
await entrypoint.acquireDependencies(
SolveType.get,
summaryOnly: summaryOnly,
);
if (onlyOutputWhenTerminal) {
await log.errorsOnlyUnlessTerminal(() async {
await entrypoint.acquireDependencies(
SolveType.get,
summaryOnly: summaryOnly,
);
});
} else {
await entrypoint.acquireDependencies(
SolveType.get,
summaryOnly: summaryOnly,
);
}
return entrypoint.packageConfig;
case final PackageConfig packageConfig:
log.fine('Package Config up to date.');
return packageConfig;
});
} else {
await entrypoint.acquireDependencies(
SolveType.get,
summaryOnly: summaryOnly,
);
}
return (
packageConfig: entrypoint.packageConfig,
rootDir: relativeIfNeeded(
p.normalize(
p.absolute(entrypoint.workspaceRoot.dir),
),
)
);
}

/// We require an SDK constraint lower-bound as of Dart 2.12.0
96 changes: 59 additions & 37 deletions lib/src/executable.dart
Original file line number Diff line number Diff line change
@@ -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.
///
@@ -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`.
@@ -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,
@@ -305,42 +306,53 @@ 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(
final String workspaceRootRelativeToCwd;
(packageConfig: packageConfig, rootDir: workspaceRootRelativeToCwd) =
await Entrypoint.ensureUpToDate(
rootOrCurrent,
cache: SystemCache(rootDir: pubCacheDir),
);
workspaceRootDir = p.absolute(workspaceRootRelativeToCwd);
} on ApplicationException catch (e) {
throw CommandResolutionFailedException._(
e.toString(),
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.
final packageConfigDir =
p.join(workspaceRootDir, '.dart_tool', 'package_config.json');

final rootPackageName = maxBy<(String, String), int>(
packageConfig.packages.map((package) {
final packageRootDir =
p.canonicalize(package.resolvedRootDir(packageConfigDir));
if (p.equals(packageRootDir, rootOrCurrent) ||
p.isWithin(packageRootDir, rootOrCurrent)) {
return (package.name, packageRootDir);
} else {
return null;
}
}).whereNotNull(),
(tuple) => tuple.$2.length,
)?.$1;

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,
);
}
@@ -370,10 +382,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))) {
@@ -384,8 +399,8 @@ Future<DartExecutableWithPackageConfig> getExecutableForCommand(
}
if (!allowSnapshot) {
return DartExecutableWithPackageConfig(
executable: p.relative(path, from: rootOrCurrent),
packageConfig: packageConfigPath,
executable: p.normalize(path),
packageConfig: p.relative(packageConfigPath, from: rootOrCurrent),
);
} else {
// TODO(sigurdm): attempt to decide on package mutability without looking at
@@ -419,12 +434,19 @@ 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('/') ||
(Platform.isWindows && candidate.contains(r'\')) ||
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
15 changes: 13 additions & 2 deletions lib/src/io.dart
Original file line number Diff line number Diff line change
@@ -197,8 +197,19 @@ String _resolveLink(String link) {
return link;
}

/// Reads the contents of the text file [file].
String readTextFile(String file) => File(file).readAsStringSync();
/// Reads the contents of the text file at [path].
String readTextFile(String path) => File(path).readAsStringSync();

/// Reads the contents of the text file at [path].
/// Returns `null` if the operation fails.
String? tryReadTextFile(String path) {
try {
return readTextFile(path);
} on FileSystemException {
// TODO: Consider handlind file-not-found differently from other exceptions.
return null;
}
}

/// Reads the contents of the text file [file].
Future<String> readTextFileAsync(String file) {
2 changes: 1 addition & 1 deletion test/descriptor.dart
Original file line number Diff line number Diff line change
@@ -117,7 +117,7 @@ FileDescriptor libPubspec(
}) {
final map = packageMap(name, version, deps, devDeps);
if (resolutionWorkspace && sdk == null) {
sdk = '3.5.0';
sdk = '^3.5.0-0';
}
if (sdk != null) {
map['environment'] = {'sdk': sdk};
2 changes: 1 addition & 1 deletion test/embedding/ensure_pubspec_resolved.dart
Original file line number Diff line number Diff line change
@@ -80,7 +80,7 @@ void testEnsurePubspecResolved() {
.deleteSync();

await _implicitPubGet(
'No .dart_tool/package_config.json file found',
'`./pubspec.yaml` exists without corresponding `./pubspec.yaml` or `.dart_tool/pub/workspace_ref.json`.',
);
});

171 changes: 164 additions & 7 deletions test/embedding/get_executable_for_command_test.dart
Original file line number Diff line number Diff line change
@@ -138,7 +138,7 @@ Future<void> main() async {
);
});

test('Does `pub get` if there is a pubspec.yaml', () async {
test('Reports file not found if the path looks like a file', () async {
await d.dir(appPath, [
d.pubspec({
'name': 'myapp',
@@ -156,8 +156,8 @@ Future<void> main() async {
await testGetExecutable(
'bar/m.dart',
d.path(appPath),
errorMessage: matches(r'version\s+solving\s+failed'),
issue: CommandResolutionIssue.pubGetFailed,
errorMessage: matches(r'Could not find file `bar/m.dart`'),
issue: CommandResolutionIssue.fileNotFound,
);
});

@@ -206,6 +206,9 @@ Future<void> main() async {
pubspec: {
'environment': {'sdk': '^$_currentVersion'},
},
deps: {
'transitive': {'hosted': globalServer.url},
},
contents: [
d.dir('bin', [
d.file('foo.dart', 'main() {print(42);}'),
@@ -214,16 +217,24 @@ Future<void> main() async {
],
);

server.serve(
'transitive',
'1.0.0',
pubspec: {
'environment': {'sdk': '^$_currentVersion'},
},
contents: [
d.dir('bin', [d.file('transitive.dart', 'main() {print(42);}')]),
],
);

await d.dir(appPath, [
d.pubspec({
'name': 'myapp',
'environment': {'sdk': '^$_currentVersion'},
'dependencies': {
'foo': {
'hosted': {
'name': 'foo',
'url': globalServer.url,
},
'hosted': globalServer.url,
'version': '^1.0.0',
},
},
@@ -331,6 +342,152 @@ Future<void> main() async {
'Could not find package `unknownTool` or file `unknownTool`',
issue: CommandResolutionIssue.packageNotFound,
);
await testGetExecutable(
'transitive',
dir,
executable: p.join(
d.sandbox,
d.hostedCachePath(port: globalServer.port),
'transitive-1.0.0',
'bin',
'transitive.dart',
),
allowSnapshot: false,
packageConfig: p.join('.dart_tool', 'package_config.json'),
);
});

test('works with workspace', () async {
final server = await servePackages();
server.serve(
'foo',
'1.0.0',
contents: [
d.dir('bin', [
d.file('foo.dart', 'main() {print(42);}'),
d.file('tool.dart', 'main() {print(42);}'),
]),
],
);

await d.dir(appPath, [
d.libPubspec(
'myapp',
'1.2.3',
deps: {
'a': 'any',
'foo': {
'hosted': globalServer.url,
'version': '^1.0.0',
},
},
extras: {
'workspace': ['pkgs/a', 'pkgs/b'],
},
sdk: '^3.5.0-0',
),
d.dir('bin', [
d.file('myapp.dart', 'main() {print(42);}'),
d.file('tool.dart', 'main() {print(42);}'),
]),
d.dir('pkgs', [
d.dir('a', [
d.libPubspec(
'a',
'1.0.0',
resolutionWorkspace: true,
),
d.dir('bin', [
d.file('a.dart', 'main() {print(42);}'),
d.file('tool.dart', 'main() {print(42);}'),
]),
]),
d.dir('b', [
d.libPubspec(
'b',
'1.0.0',
resolutionWorkspace: true,
),
d.dir('bin', [
d.file('b.dart', 'main() {print(42);}'),
d.file('tool.dart', 'main() {print(42);}'),
]),
]),
]),
]).create();
await pubGet(
environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
);

await testGetExecutable(
'myapp',
p.join(d.sandbox, appPath, 'pkgs', 'a'),
executable: p.join(
'..',
'..',
'.dart_tool',
'pub',
'bin',
'myapp',
'myapp.dart-$_currentVersion.snapshot',
),
packageConfig: p.join('..', '..', '.dart_tool', 'package_config.json'),
);
await testGetExecutable(
'a',
p.join(d.sandbox, appPath, 'pkgs'),
executable: p.join(
d.sandbox,
appPath,
'pkgs',
'a',
'bin',
'a.dart',
),
allowSnapshot: false,
packageConfig: p.join('..', '.dart_tool', 'package_config.json'),
);
await testGetExecutable(
'b:tool',
p.join(d.sandbox, appPath),
allowSnapshot: false,
executable: p.join(
d.sandbox,
appPath,
'pkgs',
'b',
'bin',
'tool.dart',
),
packageConfig: p.join('.dart_tool', 'package_config.json'),
);
await testGetExecutable(
'foo',
p.join(d.sandbox, appPath),
allowSnapshot: false,
executable: p.join(
d.sandbox,
d.hostedCachePath(),
'foo-1.0.0',
'bin',
'foo.dart',
),
packageConfig: p.join('.dart_tool', 'package_config.json'),
);
await testGetExecutable(
':tool',
p.join(d.sandbox, appPath, 'pkgs', 'a'),
allowSnapshot: false,
executable: p.join(
d.sandbox,
appPath,
'pkgs',
'a',
'bin',
'tool.dart',
),
packageConfig: p.join('..', '..', '.dart_tool', 'package_config.json'),
);
});
}