Avoid raceconditions in `global activate`, `run` and `global run` (#3285)
diff --git a/analysis_options.yaml b/analysis_options.yaml
index f07a060..6177eba 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -31,6 +31,7 @@
- sort_pub_dependencies
- test_types_in_equals
- throw_in_finally
+ - unawaited_futures
- unnecessary_lambdas
- unnecessary_null_aware_assignments
- unnecessary_parenthesis
diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart
index d540978..328d935 100644
--- a/lib/src/command/add.dart
+++ b/lib/src/command/add.dart
@@ -162,9 +162,8 @@
/// to this new dependency.
final newRoot = Package.inMemory(updatedPubSpec);
- // TODO(jonasfj): Stop abusing Entrypoint.global for dry-run output
- await Entrypoint.global(newRoot, entrypoint.lockFile, cache,
- solveResult: solveResult)
+ await Entrypoint.inMemory(newRoot, cache,
+ solveResult: solveResult, lockFile: entrypoint.lockFile)
.acquireDependencies(SolveType.get,
dryRun: true,
precompile: argResults['precompile'],
diff --git a/lib/src/command/remove.dart b/lib/src/command/remove.dart
index af91d8c..f3b27bd 100644
--- a/lib/src/command/remove.dart
+++ b/lib/src/command/remove.dart
@@ -65,7 +65,7 @@
final newPubspec = _removePackagesFromPubspec(rootPubspec, packages);
final newRoot = Package.inMemory(newPubspec);
- await Entrypoint.global(newRoot, entrypoint.lockFile, cache)
+ await Entrypoint.inMemory(newRoot, cache, lockFile: entrypoint.lockFile)
.acquireDependencies(SolveType.get,
precompile: argResults['precompile'],
dryRun: true,
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart
index 34e061d..3063770 100644
--- a/lib/src/command/upgrade.dart
+++ b/lib/src/command/upgrade.dart
@@ -222,11 +222,10 @@
if (_dryRun) {
// Even if it is a dry run, run `acquireDependencies` so that the user
// gets a report on changes.
- // TODO(jonasfj): Stop abusing Entrypoint.global for dry-run output
- await Entrypoint.global(
+ await Entrypoint.inMemory(
Package.inMemory(resolvablePubspec),
- entrypoint.lockFile,
cache,
+ lockFile: entrypoint.lockFile,
solveResult: solveResult,
).acquireDependencies(
SolveType.upgrade,
@@ -317,10 +316,10 @@
// Even if it is a dry run, run `acquireDependencies` so that the user
// gets a report on changes.
// TODO(jonasfj): Stop abusing Entrypoint.global for dry-run output
- await Entrypoint.global(
+ await Entrypoint.inMemory(
Package.inMemory(nullsafetyPubspec),
- entrypoint.lockFile,
cache,
+ lockFile: entrypoint.lockFile,
solveResult: solveResult,
).acquireDependencies(
SolveType.upgrade,
diff --git a/lib/src/dart.dart b/lib/src/dart.dart
index 97219dc..f4f8686 100644
--- a/lib/src/dart.dart
+++ b/lib/src/dart.dart
@@ -146,11 +146,14 @@
String toString() => errors.join('\n');
}
-/// Precompiles the Dart executable at [executablePath] to a kernel file at
-/// [outputPath].
+/// Precompiles the Dart executable at [executablePath].
///
-/// This file is also cached at [incrementalDillOutputPath] which is used to
-/// initialize the compiler on future runs.
+/// If the compilation succeeds it is saved to a kernel file at [outputPath].
+///
+/// If compilation fails, the output is cached at [incrementalDillOutputPath].
+///
+/// Whichever of [incrementalDillOutputPath] and [outputPath] already exists is
+/// used to initialize the compiler run.
///
/// The [packageConfigPath] should point at the package config file to be used
/// for `package:` uri resolution.
@@ -158,39 +161,65 @@
/// The [name] is used to describe the executable in logs and error messages.
Future<void> precompile({
required String executablePath,
- required String incrementalDillOutputPath,
+ required String incrementalDillPath,
required String name,
required String outputPath,
required String packageConfigPath,
}) async {
ensureDir(p.dirname(outputPath));
- ensureDir(p.dirname(incrementalDillOutputPath));
+ ensureDir(p.dirname(incrementalDillPath));
+
const platformDill = 'lib/_internal/vm_platform_strong.dill';
final sdkRoot = p.relative(p.dirname(p.dirname(Platform.resolvedExecutable)));
- var client = await FrontendServerClient.start(
- executablePath,
- incrementalDillOutputPath,
- platformDill,
- sdkRoot: sdkRoot,
- packagesJson: packageConfigPath,
- printIncrementalDependencies: false,
- );
+ String? tempDir;
+ FrontendServerClient? client;
try {
- var result = await client.compile();
+ tempDir = createTempDir(p.dirname(incrementalDillPath), 'tmp');
+ // To avoid potential races we copy the incremental data to a temporary file
+ // for just this compilation.
+ final temporaryIncrementalDill =
+ p.join(tempDir, '${p.basename(incrementalDillPath)}.incremental.dill');
+ try {
+ if (fileExists(incrementalDillPath)) {
+ copyFile(incrementalDillPath, temporaryIncrementalDill);
+ } else if (fileExists(outputPath)) {
+ copyFile(outputPath, temporaryIncrementalDill);
+ }
+ } on FileSystemException {
+ // Not able to copy existing file, compilation will start from scratch.
+ }
+
+ client = await FrontendServerClient.start(
+ executablePath,
+ temporaryIncrementalDill,
+ platformDill,
+ sdkRoot: sdkRoot,
+ packagesJson: packageConfigPath,
+ printIncrementalDependencies: false,
+ );
+ final result = await client.compile();
final highlightedName = log.bold(name);
if (result?.errorCount == 0) {
log.message('Built $highlightedName.');
- await File(incrementalDillOutputPath).copy(outputPath);
+ // By using rename we ensure atomicity. An external observer will either
+ // see the old or the new snapshot.
+ renameFile(temporaryIncrementalDill, outputPath);
} else {
- // Don't leave partial results.
- deleteEntry(outputPath);
+ // By using rename we ensure atomicity. An external observer will either
+ // see the old or the new snapshot.
+ renameFile(temporaryIncrementalDill, incrementalDillPath);
+ // If compilation failed we don't want to leave an incorrect snapshot.
+ tryDeleteEntry(outputPath);
throw ApplicationException(
log.yellow('Failed to build $highlightedName:\n') +
(result?.compilerOutputLines.join('\n') ?? ''));
}
} finally {
- client.kill();
+ client?.kill();
+ if (tempDir != null) {
+ tryDeleteEntry(tempDir);
+ }
}
}
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index b54ec5e..26a8243 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -16,7 +16,6 @@
import 'dart.dart' as dart;
import 'exceptions.dart';
import 'executable.dart';
-import 'http.dart' as http;
import 'io.dart';
import 'language_version.dart';
import 'lock_file.dart';
@@ -73,8 +72,14 @@
/// but may be the entrypoint when you're running its tests.
class Entrypoint {
/// The root package this entrypoint is associated with.
+ ///
+ /// For a global package, this is the activated package.
final Package root;
+ /// For a global package, this is the directory that the package is installed
+ /// in. Non-global packages have null.
+ final String? globalDir;
+
/// The system-wide cache which caches packages that need to be fetched over
/// the network.
final SystemCache cache;
@@ -83,7 +88,8 @@
bool get isCached => !root.isInMemory && p.isWithin(cache.rootDir, root.dir);
/// Whether this is an entrypoint for a globally-activated package.
- final bool isGlobal;
+ // final bool isGlobal;
+ bool get isGlobal => globalDir != null;
/// The lockfile for the entrypoint.
///
@@ -123,8 +129,7 @@
///
/// Global packages (except those from path source)
/// store these in the global cache.
- String? get _configRoot =>
- isCached ? p.join(cache.rootDir, 'global_packages', root.name) : root.dir;
+ String? get _configRoot => isCached ? globalDir : root.dir;
/// The path to the entrypoint's ".packages" file.
///
@@ -153,11 +158,7 @@
/// but the configuration is stored at the package itself.
String get cachePath {
if (isGlobal) {
- return p.join(
- cache.rootDir,
- 'global_packages',
- root.name,
- );
+ return globalDir!;
} else {
var newPath = root.path('.dart_tool/pub');
var oldPath = root.path('.pub');
@@ -174,15 +175,25 @@
String get _incrementalDillsPath => p.join(cachePath, 'incremental');
/// Loads the entrypoint from a package at [rootDir].
- Entrypoint(String rootDir, this.cache)
- : root = Package.load(null, rootDir, cache.sources),
- isGlobal = false;
+ Entrypoint(
+ String rootDir,
+ this.cache,
+ ) : root = Package.load(null, rootDir, cache.sources),
+ globalDir = null;
+
+ Entrypoint.inMemory(this.root, this.cache,
+ {required LockFile? lockFile, SolveResult? solveResult})
+ : _lockFile = lockFile,
+ globalDir = null {
+ if (solveResult != null) {
+ _packageGraph = PackageGraph.fromSolveResult(this, solveResult);
+ }
+ }
/// Creates an entrypoint given package and lockfile objects.
/// If a SolveResult is already created it can be passed as an optimization.
- Entrypoint.global(this.root, this._lockFile, this.cache,
- {SolveResult? solveResult})
- : isGlobal = true {
+ Entrypoint.global(this.globalDir, this.root, this._lockFile, this.cache,
+ {SolveResult? solveResult}) {
if (solveResult != null) {
_packageGraph = PackageGraph.fromSolveResult(this, solveResult);
}
@@ -203,18 +214,20 @@
/// Writes .packages and .dart_tool/package_config.json
Future<void> writePackagesFiles() async {
+ final entrypointName = isGlobal ? null : root.name;
writeTextFile(
packagesFile,
lockFile.packagesFile(cache,
- entrypoint: root.name, relativeFrom: root.dir));
+ entrypoint: entrypointName,
+ relativeFrom: isGlobal ? null : root.dir));
ensureDir(p.dirname(packageConfigFile));
writeTextFile(
packageConfigFile,
await lockFile.packageConfigFile(cache,
- entrypoint: root.name,
+ entrypoint: entrypointName,
entrypointSdkConstraint:
root.pubspec.sdkConstraints[sdk.identifier],
- relativeFrom: root.dir));
+ relativeFrom: isGlobal ? null : root.dir));
}
/// Gets all dependencies of the [root] package.
@@ -294,7 +307,7 @@
await result.showReport(type, cache);
}
if (!dryRun) {
- await Future.wait(result.packages.map(_get));
+ await result.downloadCachedPackages(cache);
_saveLockFile(result);
}
if (onlyReportSuccessOrFailure) {
@@ -387,10 +400,11 @@
Future<void> _precompileExecutable(Executable executable) async {
final package = executable.package;
+
await dart.precompile(
executablePath: resolveExecutable(executable),
outputPath: pathOfExecutable(executable),
- incrementalDillOutputPath: incrementalDillPathOfExecutable(executable),
+ incrementalDillPath: incrementalDillPathOfExecutable(executable),
packageConfigPath: packageConfigFile,
name:
'$package:${p.basenameWithoutExtension(executable.relativePath)}');
@@ -470,21 +484,6 @@
}
}
- /// Makes sure the package at [id] is locally available.
- ///
- /// This automatically downloads the package to the system-wide cache as well
- /// if it requires network access to retrieve (specifically, if the package's
- /// source is a [CachedSource]).
- Future<void> _get(PackageId id) async {
- return await http.withDependencyType(root.dependencyType(id.name),
- () async {
- if (id.isRoot) return;
-
- var source = cache.source(id.source);
- if (source is CachedSource) await source.downloadToSystemCache(id);
- });
- }
-
/// Throws a [DataError] if the `.dart_tool/package_config.json` file doesn't
/// exist or if it's out-of-date relative to the lockfile or the pubspec.
///
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index a868b4b..773f2ec 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -12,7 +12,6 @@
import 'entrypoint.dart';
import 'exceptions.dart';
import 'executable.dart' as exec;
-import 'http.dart' as http;
import 'io.dart';
import 'lock_file.dart';
import 'log.dart' as log;
@@ -62,6 +61,8 @@
/// The directory where the lockfiles for activated packages are stored.
String get _directory => p.join(cache.rootDir, 'global_packages');
+ String _packageDir(String packageName) => p.join(_directory, packageName);
+
/// The directory where binstubs for global package executables are stored.
String get _binStubDir => p.join(cache.rootDir, 'bin');
@@ -151,26 +152,19 @@
// Get the package's dependencies.
await entrypoint.acquireDependencies(SolveType.get, analytics: analytics);
var name = entrypoint.root.name;
-
- try {
- var originalLockFile =
- LockFile.load(_getLockFilePath(name), cache.sources);
- // Call this just to log what the current active package is, if any.
- _describeActive(originalLockFile, name);
- } on IOException {
- // Couldn't read the lock file. It probably doesn't exist.
- }
+ _describeActive(name, cache);
// Write a lockfile that points to the local package.
var fullPath = canonicalize(entrypoint.root.dir);
var id = cache.path.source.idFor(name, entrypoint.root.version, fullPath);
+ final tempDir = cache.createTempDir();
// TODO(rnystrom): Look in "bin" and display list of binaries that
// user can run.
- _writeLockFile(name, LockFile([id]));
+ _writeLockFile(tempDir, LockFile([id]));
- var binDir = p.join(_directory, name, 'bin');
- if (dirExists(binDir)) deleteEntry(binDir);
+ tryDeleteEntry(_packageDir(name));
+ tryRenameDir(tempDir, _packageDir(name));
_updateBinStubs(entrypoint, entrypoint.root, executables,
overwriteBinStubs: overwriteBinStubs);
@@ -178,17 +172,12 @@
}
/// Installs the package [dep] and its dependencies into the system cache.
+ ///
+ /// If [silent] less logging will be printed.
Future<void> _installInCache(PackageRange dep, List<String>? executables,
- {required bool overwriteBinStubs}) async {
- LockFile? originalLockFile;
- try {
- originalLockFile =
- LockFile.load(_getLockFilePath(dep.name), cache.sources);
- // Call this just to log what the current active package is, if any.
- _describeActive(originalLockFile, dep.name);
- } on IOException {
- // Couldn't read the lock file. It probably doesn't exist.
- }
+ {required bool overwriteBinStubs, bool silent = false}) async {
+ final name = dep.name;
+ LockFile? originalLockFile = _describeActive(name, cache);
// Create a dummy package with just [dep] so we can do resolution on it.
var root = Package.inMemory(Pubspec('pub global activate',
@@ -200,103 +189,89 @@
// being available, report that as a [dataError].
SolveResult result;
try {
- result = await log.progress('Resolving dependencies',
- () => resolveVersions(SolveType.get, cache, root));
+ result = await log.spinner(
+ 'Resolving dependencies',
+ () => resolveVersions(SolveType.get, cache, root),
+ condition: !silent,
+ );
} on SolveFailure catch (error) {
for (var incompatibility
in error.incompatibility.externalIncompatibilities) {
if (incompatibility.cause != IncompatibilityCause.noVersions) continue;
- if (incompatibility.terms.single.package.name != dep.name) continue;
+ if (incompatibility.terms.single.package.name != name) continue;
dataError(error.toString());
}
rethrow;
}
+ // We want the entrypoint to be rooted at 'dep' not the dummy-package.
+ result.packages.removeWhere((id) => id.name == 'pub global activate');
final sameVersions = originalLockFile != null &&
originalLockFile.samePackageIds(result.lockFile);
+ final PackageId id = result.lockFile.packages[name]!;
if (sameVersions) {
log.message('''
-The package ${dep.name} is already activated at newest available version.
-To recompile executables, first run `$topLevelProgram pub global deactivate ${dep.name}`.
+The package $name is already activated at newest available version.
+To recompile executables, first run `$topLevelProgram pub global deactivate $name`.
''');
} else {
- await result.showReport(SolveType.get, cache);
+ // Only precompile binaries if we have a new resolution.
+ if (!silent) await result.showReport(SolveType.get, cache);
+
+ await result.downloadCachedPackages(cache);
+
+ final lockFile = result.lockFile;
+ final tempDir = cache.createTempDir();
+ _writeLockFile(tempDir, lockFile);
+
+ // Load the package graph from [result] so we don't need to re-parse all
+ // the pubspecs.
+ final entrypoint = Entrypoint.global(
+ tempDir,
+ cache.loadCached(id),
+ lockFile,
+ cache,
+ solveResult: result,
+ );
+
+ await entrypoint.writePackagesFiles();
+
+ await entrypoint.precompileExecutables();
+
+ tryDeleteEntry(_packageDir(name));
+ tryRenameDir(tempDir, _packageDir(name));
}
-
- // Make sure all of the dependencies are locally installed.
- await Future.wait(result.packages.map((id) {
- return http.withDependencyType(root.dependencyType(id.name), () async {
- if (id.isRoot) return;
-
- var source = cache.source(id.source);
- if (source is CachedSource) await source.downloadToSystemCache(id);
- });
- }));
-
- var lockFile = result.lockFile;
- _writeLockFile(dep.name, lockFile);
- await _writePackageConfigFiles(dep.name, lockFile);
-
- // We want the entrypoint to be rooted at 'dep' not the dummy-package.
- result.packages.removeWhere((id) => id.name == 'pub global activate');
-
- var id = lockFile.packages[dep.name]!;
- // Load the package graph from [result] so we don't need to re-parse all
- // the pubspecs.
final entrypoint = Entrypoint.global(
- Package(
- result.pubspecs[dep.name]!,
- (cache.source(dep.source) as CachedSource).getDirectoryInCache(id),
- ),
- lockFile,
+ _packageDir(id.name),
+ cache.loadCached(id),
+ result.lockFile,
cache,
solveResult: result,
);
- if (!sameVersions) {
- // Only precompile binaries if we have a new resolution.
- await entrypoint.precompileExecutables();
- }
-
_updateBinStubs(
entrypoint,
cache.load(entrypoint.lockFile.packages[dep.name]!),
executables,
overwriteBinStubs: overwriteBinStubs,
);
-
- log.message('Activated ${_formatPackage(id)}.');
- }
-
- Future<void> _writePackageConfigFiles(
- String package, LockFile lockFile) async {
- // TODO(sigurdm): Use [Entrypoint.writePackagesFiles] instead.
- final packagesFilePath = _getPackagesFilePath(package);
- final packageConfigFilePath = _getPackageConfigFilePath(package);
- final dir = p.dirname(packagesFilePath);
- writeTextFile(
- packagesFilePath, lockFile.packagesFile(cache, relativeFrom: dir));
- ensureDir(p.dirname(packageConfigFilePath));
- writeTextFile(packageConfigFilePath,
- await lockFile.packageConfigFile(cache, relativeFrom: dir));
+ if (!silent) log.message('Activated ${_formatPackage(id)}.');
}
/// Finishes activating package [package] by saving [lockFile] in the cache.
- void _writeLockFile(String package, LockFile lockFile) {
- ensureDir(p.join(_directory, package));
-
- // TODO(nweiz): This cleans up Dart 1.6's old lockfile location. Remove it
- // when Dart 1.6 is old enough that we don't think anyone will have these
- // lockfiles anymore (issue 20703).
- var oldPath = p.join(_directory, '$package.lock');
- if (fileExists(oldPath)) deleteEntry(oldPath);
-
- writeTextFile(_getLockFilePath(package),
- lockFile.serialize(p.join(_directory, package)));
+ void _writeLockFile(String dir, LockFile lockFile) {
+ writeTextFile(p.join(dir, 'pubspec.lock'), lockFile.serialize(null));
}
/// Shows the user the currently active package with [name], if any.
- void _describeActive(LockFile lockFile, String? name) {
+ LockFile? _describeActive(String name, SystemCache cache) {
+ late final LockFile lockFile;
+ try {
+ lockFile = LockFile.load(_getLockFilePath(name), cache.sources);
+ } on IOException {
+ // Couldn't read the lock file. It probably doesn't exist.
+ return null;
+ }
var id = lockFile.packages[name]!;
var source = id.source;
@@ -312,6 +287,7 @@
log.message('Package ${log.bold(name)} is currently active at version '
'${log.bold(id.version)}.');
}
+ return lockFile;
}
/// Deactivates a previously-activated package named [name].
@@ -341,22 +317,8 @@
try {
lockFile = LockFile.load(lockFilePath, cache.sources);
} on IOException {
- var oldLockFilePath = p.join(_directory, '$name.lock');
- try {
- // TODO(nweiz): This looks for Dart 1.6's old lockfile location.
- // Remove it when Dart 1.6 is old enough that we don't think anyone
- // will have these lockfiles anymore (issue 20703).
- lockFile = LockFile.load(oldLockFilePath, cache.sources);
- } on IOException {
- // If we couldn't read the lock file, it's not activated.
- dataError('No active package ${log.bold(name)}.');
- }
-
- // Move the old lockfile to its new location.
- ensureDir(p.dirname(lockFilePath));
- File(oldLockFilePath).renameSync(lockFilePath);
- // Just make sure these files are created as well.
- await _writePackageConfigFiles(name, lockFile);
+ // If we couldn't read the lock file, it's not activated.
+ dataError('No active package ${log.bold(name)}.');
}
// Remove the package itself from the lockfile. We put it in there so we
@@ -370,7 +332,8 @@
if (source is CachedSource) {
// For cached sources, the package itself is in the cache and the
// lockfile is the one we just loaded.
- entrypoint = Entrypoint.global(cache.loadCached(id), lockFile, cache);
+ entrypoint = Entrypoint.global(
+ _packageDir(id.name), cache.loadCached(id), lockFile, cache);
} else {
// For uncached sources (i.e. path), the ID just points to the real
// directory for the package.
@@ -446,16 +409,6 @@
String _getLockFilePath(String name) =>
p.join(_directory, name, 'pubspec.lock');
- /// Gets the path to the .packages file for an activated cached package with
- /// [name].
- String _getPackagesFilePath(String name) =>
- p.join(_directory, name, '.packages');
-
- /// Gets the path to the `package_config.json` file for an
- /// activated cached package with [name].
- String _getPackageConfigFilePath(String name) =>
- p.join(_directory, name, '.dart_tool', 'package_config.json');
-
/// Shows the user a formatted list of globally activated packages.
void listActivePackages() {
if (!dirExists(_directory)) return;
@@ -542,17 +495,24 @@
log.message('Reactivating ${log.bold(id.name)} ${id.version}...');
var entrypoint = await find(id.name);
+ final packageExecutables = executables.remove(id.name) ?? [];
- await _writePackageConfigFiles(id.name, entrypoint.lockFile);
- await entrypoint.precompileExecutables();
- var packageExecutables = executables.remove(id.name) ?? [];
- _updateBinStubs(
- entrypoint,
- cache.load(id),
- packageExecutables,
- overwriteBinStubs: true,
- suggestIfNotOnPath: false,
- );
+ if (entrypoint.isCached) {
+ deleteEntry(entrypoint.globalDir!);
+ await _installInCache(
+ id.toRange(),
+ packageExecutables,
+ overwriteBinStubs: true,
+ silent: true,
+ );
+ } else {
+ await activatePath(
+ entrypoint.root.dir,
+ packageExecutables,
+ overwriteBinStubs: true,
+ analytics: null,
+ );
+ }
successes.add(id.name);
} catch (error, stackTrace) {
var message = 'Failed to reactivate '
@@ -706,10 +666,7 @@
// Show errors for any missing scripts.
// TODO(rnystrom): This can print false positives since a script may be
// produced by a transformer. Do something better.
- var binFiles = package
- .listFiles(beneath: 'bin', recursive: false)
- .map(package.relative)
- .toList();
+ var binFiles = package.executablePaths;
for (var executable in installed) {
var script = package.pubspec.executables[executable];
var scriptPath = p.join('bin', '$script.dart');
@@ -761,6 +718,7 @@
// If the script was built to a snapshot, just try to invoke that
// directly and skip pub global run entirely.
String invocation;
+ late String binstub;
if (Platform.isWindows) {
if (fileExists(snapshot)) {
// We expect absolute paths from the precompiler since relative ones
@@ -786,7 +744,7 @@
} else {
invocation = 'dart pub global run ${package.name}:$script %*';
}
- var batch = '''
+ binstub = '''
@echo off
rem This file was created by pub v${sdk.version}.
rem Package: ${package.name}
@@ -795,7 +753,6 @@
rem Script: $script
$invocation
''';
- writeTextFile(binStubPath, batch);
} else {
if (fileExists(snapshot)) {
// We expect absolute paths from the precompiler since relative ones
@@ -818,7 +775,7 @@
} else {
invocation = 'dart pub global run ${package.name}:$script "\$@"';
}
- var bash = '''
+ binstub = '''
#!/usr/bin/env sh
# This file was created by pub v${sdk.version}.
# Package: ${package.name}
@@ -827,25 +784,31 @@
# Script: $script
$invocation
''';
+ }
- // Write this as the system encoding since the system is going to execute
- // it and it might contain non-ASCII characters in the pathnames.
- writeTextFile(binStubPath, bash, encoding: const SystemEncoding());
+ // Write the binstub to a temporary location, make it executable and move
+ // it into place afterwards to avoid races.
+ final tempDir = cache.createTempDir();
+ try {
+ final tmpPath = p.join(tempDir, binStubPath);
- // Make it executable.
- var result = Process.runSync('chmod', ['+x', binStubPath]);
- if (result.exitCode != 0) {
- // Couldn't make it executable so don't leave it laying around.
- try {
- deleteEntry(binStubPath);
- } on IOException catch (err) {
- // Do nothing. We're going to fail below anyway.
- log.fine('Could not delete binstub:\n$err');
+ // Write this as the system encoding since the system is going to
+ // execute it and it might contain non-ASCII characters in the
+ // pathnames.
+ writeTextFile(tmpPath, binstub, encoding: const SystemEncoding());
+
+ if (Platform.isLinux || Platform.isMacOS) {
+ // Make it executable.
+ var result = Process.runSync('chmod', ['+x', tmpPath]);
+ if (result.exitCode != 0) {
+ // Couldn't make it executable so don't leave it laying around.
+ fail('Could not make "$tmpPath" executable (exit code '
+ '${result.exitCode}):\n${result.stderr}');
}
-
- fail('Could not make "$binStubPath" executable (exit code '
- '${result.exitCode}):\n${result.stderr}');
}
+ File(tmpPath).renameSync(binStubPath);
+ } finally {
+ deleteEntry(tempDir);
}
return previousPackage;
diff --git a/lib/src/io.dart b/lib/src/io.dart
index 6d0bf83..280dcbd 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -383,7 +383,7 @@
}
// ERROR_DIR_NOT_EMPTY
- if (!ignoreEmptyDir && isDirectoryNotEmptyException(error)) {
+ if (!ignoreEmptyDir && _isDirectoryNotEmptyException(error)) {
return 'of dart-lang/sdk#25353';
}
@@ -457,7 +457,35 @@
}, ignoreEmptyDir: true);
}
-bool isDirectoryNotEmptyException(FileSystemException e) {
+/// Renames directory [from] to [to].
+/// If it fails with "destination not empty" we log and continue, assuming
+/// another process got there before us.
+void tryRenameDir(String from, String to) {
+ ensureDir(path.dirname(to));
+ try {
+ renameDir(from, to);
+ } on FileSystemException catch (e) {
+ tryDeleteEntry(from);
+ if (!_isDirectoryNotEmptyException(e)) {
+ rethrow;
+ }
+ log.fine('''
+Destination directory $to already existed.
+Assuming a concurrent pub invocation installed it.''');
+ }
+}
+
+void copyFile(String from, String to) {
+ log.io('Copying "$from" to "$to".');
+ File(from).copySync(to);
+}
+
+void renameFile(String from, String to) {
+ log.io('Renaming "$from" to "$to".');
+ File(from).renameSync(to);
+}
+
+bool _isDirectoryNotEmptyException(FileSystemException e) {
final errorCode = e.osError?.errorCode;
return
// On Linux rename will fail with ENOTEMPTY if directory exists:
diff --git a/lib/src/lock_file.dart b/lib/src/lock_file.dart
index e2723c7..8f563ab 100644
--- a/lib/src/lock_file.dart
+++ b/lib/src/lock_file.dart
@@ -218,7 +218,7 @@
String packagesFile(
SystemCache cache, {
String? entrypoint,
- required String relativeFrom,
+ String? relativeFrom,
}) {
var header = '''
This file is deprecated. Tools should instead consume
@@ -256,7 +256,7 @@
SystemCache cache, {
String? entrypoint,
VersionConstraint? entrypointSdkConstraint,
- required String relativeFrom,
+ String? relativeFrom,
}) async {
final entries = <PackageConfigEntry>[];
for (final name in ordered(packages.keys)) {
@@ -306,8 +306,9 @@
/// Returns the serialized YAML text of the lock file.
///
/// [packageDir] is the containing directory of the root package, used to
- /// properly serialize package descriptions.
- String serialize(String packageDir) {
+ /// serialize relative path package descriptions. If it is null, they will be
+ /// serialized as absolute.
+ String serialize(String? packageDir) {
// Convert the dependencies to a simple object.
var packageMap = {};
packages.forEach((name, package) {
diff --git a/lib/src/package.dart b/lib/src/package.dart
index 0bc6f89..d100157 100644
--- a/lib/src/package.dart
+++ b/lib/src/package.dart
@@ -83,10 +83,12 @@
..addAll(dependencyOverrides);
}
- /// Returns a list of asset ids for all Dart executables in this package's bin
+ /// Returns a list of paths to all Dart executables in this package's bin
/// directory.
List<String> get executablePaths {
- return ordered(listFiles(beneath: 'bin', recursive: false))
+ final binDir = p.join(dir, 'bin');
+ if (!dirExists(binDir)) return <String>[];
+ return ordered(listDir(p.join(dir, 'bin'), includeDirs: false))
.where((executable) => p.extension(executable) == '.dart')
.map((executable) => p.relative(executable, from: dir))
.toList();
diff --git a/lib/src/solver/result.dart b/lib/src/solver/result.dart
index b8ff0e5..bc01162 100644
--- a/lib/src/solver/result.dart
+++ b/lib/src/solver/result.dart
@@ -5,6 +5,7 @@
import 'package:collection/collection.dart';
import 'package:pub_semver/pub_semver.dart';
+import '../http.dart';
import '../io.dart';
import '../lock_file.dart';
import '../log.dart' as log;
@@ -12,6 +13,7 @@
import '../package_name.dart';
import '../pub_embeddable_command.dart';
import '../pubspec.dart';
+import '../source/cached.dart';
import '../source/hosted.dart';
import '../source_registry.dart';
import '../system_cache.dart';
@@ -78,6 +80,18 @@
final LockFile _previousLockFile;
+ /// Downloads all cached packages in [packages].
+ Future<void> downloadCachedPackages(SystemCache cache) async {
+ await Future.wait(packages.map((id) async {
+ if (id.source == null) return;
+ final source = cache.source(id.source);
+ if (source is! CachedSource) return;
+ return await withDependencyType(_root.dependencyType(id.name), () async {
+ await source.downloadToSystemCache(id);
+ });
+ }));
+ }
+
/// Returns the names of all packages that were changed.
///
/// This includes packages that were added or removed.
diff --git a/lib/src/source.dart b/lib/src/source.dart
index add55fa..ecbc5eb 100644
--- a/lib/src/source.dart
+++ b/lib/src/source.dart
@@ -116,7 +116,7 @@
/// [description] in the right format.
///
/// [containingPath] is the containing directory of the root package.
- dynamic serializeDescription(String containingPath, description) {
+ dynamic serializeDescription(String? containingPath, description) {
return description;
}
diff --git a/lib/src/source/git.dart b/lib/src/source/git.dart
index 0d98ae9..3be752c 100644
--- a/lib/src/source/git.dart
+++ b/lib/src/source/git.dart
@@ -108,10 +108,10 @@
/// For the descriptions where `relative` attribute is `true`, tries to make
/// `url` relative to the specified [containingPath].
@override
- dynamic serializeDescription(String containingPath, description) {
+ dynamic serializeDescription(String? containingPath, description) {
final copy = Map.from(description);
copy.remove('relative');
- if (description['relative'] == true) {
+ if (description['relative'] == true && containingPath != null) {
copy['url'] = p.url.relative(description['url'],
from: Uri.file(containingPath).toString());
}
diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart
index b3fa729..2cfc52a 100644
--- a/lib/src/source/hosted.dart
+++ b/lib/src/source/hosted.dart
@@ -159,7 +159,7 @@
}
@override
- dynamic serializeDescription(String containingPath, description) {
+ dynamic serializeDescription(String? containingPath, description) {
final desc = _asDescription(description);
return _serializedDescriptionFor(desc.packageName, desc.uri);
}
@@ -803,17 +803,7 @@
// If this fails with a "directory not empty" exception we assume that
// another pub process has installed the same package version while we
// downloaded.
- try {
- renameDir(tempDir, destPath);
- } on io.FileSystemException catch (e) {
- tryDeleteEntry(tempDir);
- if (!isDirectoryNotEmptyException(e)) {
- rethrow;
- }
- log.fine('''
-Destination directory $destPath already existed.
-Assuming a concurrent pub invocation installed it.''');
- }
+ tryRenameDir(tempDir, destPath);
});
}
diff --git a/lib/src/source/path.dart b/lib/src/source/path.dart
index 0401ebd..4679f98 100644
--- a/lib/src/source/path.dart
+++ b/lib/src/source/path.dart
@@ -130,9 +130,11 @@
///
/// For the descriptions where `relative` attribute is `true`, tries to make
/// `path` relative to the specified [containingPath].
+ ///
+ /// If [containingPath] is `null` they are serialized as absolute.
@override
- dynamic serializeDescription(String containingPath, description) {
- if (description['relative']) {
+ dynamic serializeDescription(String? containingPath, description) {
+ if (description['relative'] == true && containingPath != null) {
return {
'path': relativePathWithPosixSeparators(
p.relative(description['path'], from: containingPath)),
diff --git a/test/global/activate/activate_hosted_after_git_test.dart b/test/global/activate/activate_hosted_after_git_test.dart
index 58c65fb..1546280 100644
--- a/test/global/activate/activate_hosted_after_git_test.dart
+++ b/test/global/activate/activate_hosted_after_git_test.dart
@@ -2,6 +2,7 @@
// 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:path/path.dart' as p;
import 'package:test/test.dart';
import '../../descriptor.dart' as d;
@@ -21,8 +22,9 @@
await runPub(args: ['global', 'activate', '-sgit', '../foo.git']);
+ final locationUri = p.toUri(p.join(d.sandbox, 'foo.git'));
await runPub(args: ['global', 'activate', 'foo'], output: '''
- Package foo is currently active from Git repository "../foo.git".
+ Package foo is currently active from Git repository "$locationUri".
Resolving dependencies...
+ foo 2.0.0
Downloading foo 2.0.0...
diff --git a/test/global/activate/activate_hosted_twice_test.dart b/test/global/activate/activate_hosted_twice_test.dart
index 9baed5b..a4d1337 100644
--- a/test/global/activate/activate_hosted_twice_test.dart
+++ b/test/global/activate/activate_hosted_twice_test.dart
@@ -24,15 +24,7 @@
d.dir('lib', [d.file('bar.dart', 'final version = "1.0.0";')])
]);
- await runPub(args: ['global', 'activate', 'foo'], output: '''
-Resolving dependencies...
-+ bar 1.0.0
-+ foo 1.0.0
-Downloading foo 1.0.0...
-Downloading bar 1.0.0...
-Building package executables...
-Built foo:foo.
-Activated foo 1.0.0.''');
+ await runPub(args: ['global', 'activate', 'foo'], output: anything);
await runPub(args: ['global', 'activate', 'foo'], output: '''
Package foo is currently active at version 1.0.0.
diff --git a/test/global/activate/activate_path_after_hosted_test.dart b/test/global/activate/activate_path_after_hosted_test.dart
index cfc28fd..01d493d 100644
--- a/test/global/activate/activate_path_after_hosted_test.dart
+++ b/test/global/activate/activate_path_after_hosted_test.dart
@@ -10,7 +10,7 @@
import '../../test_pub.dart';
void main() {
- test('activating a hosted package deactivates the path one', () async {
+ test('activating a path package deactivates the hosted one', () async {
final server = await servePackages();
server.serve('foo', '1.0.0', contents: [
d.dir('bin', [d.file('foo.dart', "main(args) => print('hosted');")])
diff --git a/test/global/activate/reactivating_git_upgrades_test.dart b/test/global/activate/reactivating_git_upgrades_test.dart
index 100aaea..68102da 100644
--- a/test/global/activate/reactivating_git_upgrades_test.dart
+++ b/test/global/activate/reactivating_git_upgrades_test.dart
@@ -2,6 +2,7 @@
// 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:path/path.dart' as p;
import 'package:test/test.dart';
import '../../descriptor.dart' as d;
@@ -29,11 +30,12 @@
await d.git('foo.git', [d.libPubspec('foo', '1.0.1')]).commit();
// Activating it again pulls down the latest commit.
+ final locationUri = p.toUri(p.join(d.sandbox, 'foo.git'));
await runPub(
args: ['global', 'activate', '-sgit', '../foo.git'],
output: allOf(
startsWith('Package foo is currently active from Git repository '
- '"../foo.git".\n'
+ '"$locationUri".\n'
'Resolving dependencies...\n'
'+ foo 1.0.1 from git ../foo.git at '),
// Specific revision number goes here.
diff --git a/test/global/activate/removes_old_lockfile_test.dart b/test/global/activate/removes_old_lockfile_test.dart
deleted file mode 100644
index 3392aff..0000000
--- a/test/global/activate/removes_old_lockfile_test.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (c) 2014, 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:test/test.dart';
-
-import '../../descriptor.dart' as d;
-import '../../test_pub.dart';
-
-void main() {
- test('removes the 1.6-style lockfile', () async {
- final server = await servePackages();
- server.serve('foo', '1.0.0');
-
- await d.dir(cachePath, [
- d.dir('global_packages', [
- d.file(
- 'foo.lock',
- 'packages: {foo: {description: foo, source: hosted, '
- 'version: "1.0.0"}}}')
- ])
- ]).create();
-
- await runPub(args: ['global', 'activate', 'foo']);
-
- await d.dir(cachePath, [
- d.dir('global_packages', [
- d.nothing('foo.lock'),
- d.dir('foo', [d.file('pubspec.lock', contains('1.0.0'))])
- ])
- ]).validate();
- });
-}
diff --git a/test/global/deactivate/git_package_test.dart b/test/global/deactivate/git_package_test.dart
index f06ce53..7fc2855 100644
--- a/test/global/deactivate/git_package_test.dart
+++ b/test/global/deactivate/git_package_test.dart
@@ -2,6 +2,7 @@
// 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:path/path.dart' as p;
import 'package:test/test.dart';
import '../../descriptor.dart' as d;
@@ -18,9 +19,10 @@
await runPub(args: ['global', 'activate', '-sgit', '../foo.git']);
+ final locationUri = p.toUri(p.join(d.sandbox, 'foo.git'));
await runPub(
args: ['global', 'deactivate', 'foo'],
output:
- 'Deactivated package foo 1.0.0 from Git repository "../foo.git".');
+ 'Deactivated package foo 1.0.0 from Git repository "$locationUri".');
});
}
diff --git a/test/global/list_test.dart b/test/global/list_test.dart
index 8d4d02e..00ca22c 100644
--- a/test/global/list_test.dart
+++ b/test/global/list_test.dart
@@ -30,9 +30,10 @@
await runPub(args: ['global', 'activate', '-sgit', '../foo.git']);
+ final locationUri = p.toUri(p.join(d.sandbox, 'foo.git'));
await runPub(
args: ['global', 'list'],
- output: 'foo 1.0.0 from Git repository "../foo.git"');
+ output: 'foo 1.0.0 from Git repository "$locationUri"');
});
test('lists an activated Path package', () async {
diff --git a/test/global/run/uses_old_lockfile_test.dart b/test/global/run/uses_old_lockfile_test.dart
deleted file mode 100644
index 1afab81..0000000
--- a/test/global/run/uses_old_lockfile_test.dart
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (c) 2014, 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:test/test.dart';
-
-import '../../descriptor.dart' as d;
-import '../../test_pub.dart';
-
-void main() {
- test('uses the 1.6-style lockfile if necessary', () async {
- await servePackages()
- ..serve('bar', '1.0.0')
- ..serve('foo', '1.0.0', deps: {
- 'bar': 'any'
- }, contents: [
- d.dir('bin', [
- d.file('script.dart', """
- import 'package:bar/bar.dart' as bar;
-
- main(args) => print(bar.main());""")
- ])
- ]);
-
- await runPub(args: ['cache', 'add', 'foo']);
- await runPub(args: ['cache', 'add', 'bar']);
-
- await d.dir(cachePath, [
- d.dir('global_packages', [
- d.file('foo.lock', '''
-packages:
- foo:
- description: foo
- source: hosted
- version: "1.0.0"
- bar:
- description: bar
- source: hosted
- version: "1.0.0"''')
- ])
- ]).create();
-
- var pub = await pubRun(global: true, args: ['foo:script']);
- expect(pub.stdout, emitsThrough('bar 1.0.0'));
- await pub.shouldExit();
-
- await d.dir(cachePath, [
- d.dir('global_packages', [
- d.nothing('foo.lock'),
- d.dir('foo', [d.file('pubspec.lock', contains('1.0.0'))])
- ])
- ]).validate();
- });
-}
diff --git a/tool/test.dart b/tool/test.dart
index e7877a2..7d98156 100755
--- a/tool/test.dart
+++ b/tool/test.dart
@@ -29,7 +29,7 @@
await precompile(
executablePath: path.join('bin', 'pub.dart'),
outputPath: pubSnapshotFilename,
- incrementalDillOutputPath: pubSnapshotIncrementalFilename,
+ incrementalDillPath: pubSnapshotIncrementalFilename,
name: 'bin/pub.dart',
packageConfigPath: path.join('.dart_tool', 'package_config.json'));
testProcess = await Process.start(