Skip to content

Commit c0e3d5f

Browse files
committed
Make pub's binstubs resilient to changes in snapshot format.
This also makes "pub cache repair" repair activated packages, including updating the binstub format. [email protected] BUG=21463 Review URL: https://codereview.chromium.org//745153002 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@42055 260f80e4-7a28-3924-810f-c04153c831b5
1 parent 5aff08b commit c0e3d5f

File tree

55 files changed

+1647
-305
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1647
-305
lines changed

sdk/lib/_internal/pub/lib/src/command/cache_repair.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ class CacheRepairCommand extends PubCommand {
4242
log.message("Failed to reinstall ${log.red(failures)} $packages.");
4343
}
4444

45+
var results = await globals.repairActivatedPackages();
46+
if (results.first > 0) {
47+
var packages = pluralize("package", results.first);
48+
log.message("Reactivated ${log.green(results.first)} $packages.");
49+
}
50+
51+
if (results.last > 0) {
52+
var packages = pluralize("package", results.last);
53+
log.message("Failed to reactivate ${log.red(results.last)} $packages.");
54+
}
55+
4556
if (successes == 0 && failures == 0) {
4657
log.message("No packages in cache, so nothing to repair.");
4758
}

sdk/lib/_internal/pub/lib/src/entrypoint.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,10 @@ class Entrypoint {
348348
/// contains dependencies that are not in the lockfile or that don't match
349349
/// what's in there.
350350
bool _isLockFileUpToDate(LockFile lockFile) {
351+
/// If this is an entrypoint for an in-memory package, trust the in-memory
352+
/// lockfile provided for it.
353+
if (root.dir == null) return true;
354+
351355
return root.immediateDependencies.every((package) {
352356
var locked = lockFile.packages[package.name];
353357
if (locked == null) return false;

sdk/lib/_internal/pub/lib/src/global_packages.dart

Lines changed: 169 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:pub_semver/pub_semver.dart';
1313

1414
import 'barback/asset_environment.dart';
1515
import 'entrypoint.dart';
16+
import 'exceptions.dart';
1617
import 'executable.dart' as exe;
1718
import 'io.dart';
1819
import 'lock_file.dart';
@@ -27,10 +28,6 @@ import 'source/path.dart';
2728
import 'system_cache.dart';
2829
import 'utils.dart';
2930

30-
/// Matches the package name that a binstub was created for inside the contents
31-
/// of the shell script.
32-
final _binStubPackagePattern = new RegExp(r"Package: ([a-zA-Z0-9_-]+)");
33-
3431
/// Maintains the set of packages that have been globally activated.
3532
///
3633
/// These have been hand-chosen by the user to make their executables in bin/
@@ -377,29 +374,35 @@ class GlobalPackages {
377374
String _getLockFilePath(String name) =>
378375
p.join(_directory, name, "pubspec.lock");
379376

380-
/// Shows to the user formatted list of globally activated packages.
377+
/// Shows the user a formatted list of globally activated packages.
381378
void listActivePackages() {
382379
if (!dirExists(_directory)) return;
383380

384-
// Loads lock [file] and returns [PackageId] of the activated package.
385-
loadPackageId(file, name) {
386-
var lockFile = new LockFile.load(p.join(_directory, file), cache.sources);
387-
return lockFile.packages[name];
388-
}
389-
390-
var packages = listDir(_directory).map((entry) {
391-
if (fileExists(entry)) {
392-
return loadPackageId(entry, p.basenameWithoutExtension(entry));
393-
} else {
394-
return loadPackageId(p.join(entry, 'pubspec.lock'), p.basename(entry));
395-
}
396-
}).toList();
397-
398-
packages
381+
return listDir(_directory).map(_loadPackageId).toList()
399382
..sort((id1, id2) => id1.name.compareTo(id2.name))
400383
..forEach((id) => log.message(_formatPackage(id)));
401384
}
402385

386+
/// Returns the [PackageId] for the globally-activated package at [path].
387+
///
388+
/// [path] should be a path within [_directory]. It can either be an old-style
389+
/// path to a single lockfile or a new-style path to a directory containing a
390+
/// lockfile.
391+
PackageId _loadPackageId(String path) {
392+
var name = p.basenameWithoutExtension(path);
393+
if (!fileExists(path)) path = p.join(path, 'pubspec.lock');
394+
395+
var id = new LockFile.load(p.join(_directory, path), cache.sources)
396+
.packages[name];
397+
398+
if (id == null) {
399+
throw new FormatException("Pubspec for activated package $name didn't "
400+
"contain an entry for itself.");
401+
}
402+
403+
return id;
404+
}
405+
403406
/// Returns formatted string representing the package [id].
404407
String _formatPackage(PackageId id) {
405408
if (id.source == 'git') {
@@ -413,6 +416,92 @@ class GlobalPackages {
413416
}
414417
}
415418

419+
/// Repairs any corrupted globally-activated packages and their binstubs.
420+
///
421+
/// Returns a pair of two [int]s. The first indicates how many packages were
422+
/// successfully re-activated; the second indicates how many failed.
423+
Future<Pair<int, int>> repairActivatedPackages() async {
424+
var executables = {};
425+
if (dirExists(_binStubDir)) {
426+
for (var entry in listDir(_binStubDir)) {
427+
try {
428+
var binstub = readTextFile(entry);
429+
var package = _binStubProperty(binstub, "Package");
430+
if (package == null) {
431+
throw new ApplicationException("No 'Package' property.");
432+
}
433+
434+
var executable = _binStubProperty(binstub, "Executable");
435+
if (executable == null) {
436+
throw new ApplicationException("No 'Executable' property.");
437+
}
438+
439+
executables.putIfAbsent(package, () => []).add(executable);
440+
} catch (error, stackTrace) {
441+
log.error(
442+
"Error reading binstub for "
443+
"\"${p.basenameWithoutExtension(entry)}\"",
444+
error, stackTrace);
445+
446+
tryDeleteEntry(entry);
447+
}
448+
}
449+
}
450+
451+
var successes = 0;
452+
var failures = 0;
453+
if (dirExists(_directory)) {
454+
for (var entry in listDir(_directory)) {
455+
var id;
456+
try {
457+
id = _loadPackageId(entry);
458+
log.message("Reactivating ${log.bold(id.name)} ${id.version}...");
459+
460+
var entrypoint = await find(id.name);
461+
462+
var graph = await entrypoint.loadPackageGraph();
463+
var snapshots = await _precompileExecutables(entrypoint, id.name);
464+
var packageExecutables = executables.remove(id.name);
465+
if (packageExecutables == null) packageExecutables = [];
466+
_updateBinStubs(graph.packages[id.name], packageExecutables,
467+
overwriteBinStubs: true, snapshots: snapshots,
468+
suggestIfNotOnPath: false);
469+
successes++;
470+
} catch (error, stackTrace) {
471+
var message = "Failed to reactivate "
472+
"${log.bold(p.basenameWithoutExtension(entry))}";
473+
if (id != null) {
474+
message += " ${id.version}";
475+
if (id.source != "hosted") message += " from ${id.source}";
476+
}
477+
478+
log.error(message, error, stackTrace);
479+
failures++;
480+
481+
tryDeleteEntry(entry);
482+
}
483+
}
484+
}
485+
486+
if (executables.isNotEmpty) {
487+
var packages = pluralize("package", executables.length);
488+
var message = new StringBuffer("Binstubs exist for non-activated "
489+
"packages:\n");
490+
executables.forEach((package, executableNames) {
491+
// TODO(nweiz): Use a normal for loop here when
492+
// https://github.com/dart-lang/async_await/issues/68 is fixed.
493+
executableNames.forEach((executable) =>
494+
deleteEntry(p.join(_binStubDir, executable)));
495+
496+
message.writeln(" From ${log.bold(package)}: "
497+
"${toSentence(executableNames)}");
498+
});
499+
log.error(message);
500+
}
501+
502+
return new Pair(successes, failures);
503+
}
504+
416505
/// Updates the binstubs for [package].
417506
///
418507
/// A binstub is a little shell script in `PUB_CACHE/bin` that runs an
@@ -430,10 +519,14 @@ class GlobalPackages {
430519
/// Otherwise, the previous ones will be preserved.
431520
///
432521
/// If [snapshots] is given, it is a map of the names of executables whose
433-
/// snapshots that were precompiled to their paths. Binstubs for those will
434-
/// run the snapshot directly and skip pub entirely.
522+
/// snapshots were precompiled to the paths of those snapshots. Binstubs for
523+
/// those will run the snapshot directly and skip pub entirely.
524+
///
525+
/// If [suggestIfNotOnPath] is `true` (the default), this will warn the user if
526+
/// the bin directory isn't on their path.
435527
void _updateBinStubs(Package package, List<String> executables,
436-
{bool overwriteBinStubs, Map<String, String> snapshots}) {
528+
{bool overwriteBinStubs, Map<String, String> snapshots,
529+
bool suggestIfNotOnPath: true}) {
437530
if (snapshots == null) snapshots = const {};
438531

439532
// Remove any previously activated binstubs for this package, in case the
@@ -513,7 +606,9 @@ class GlobalPackages {
513606
}
514607
}
515608

516-
if (installed.isNotEmpty) _suggestIfNotOnPath(installed);
609+
if (suggestIfNotOnPath && installed.isNotEmpty) {
610+
_suggestIfNotOnPath(installed.first);
611+
}
517612
}
518613

519614
/// Creates a binstub named [executable] that runs [script] from [package].
@@ -537,12 +632,11 @@ class GlobalPackages {
537632
var previousPackage;
538633
if (fileExists(binStubPath)) {
539634
var contents = readTextFile(binStubPath);
540-
var match = _binStubPackagePattern.firstMatch(contents);
541-
if (match != null) {
542-
previousPackage = match[1];
543-
if (!overwrite) return previousPackage;
544-
} else {
635+
previousPackage = _binStubProperty(contents, "Package");
636+
if (previousPackage == null) {
545637
log.fine("Could not parse binstub $binStubPath:\n$contents");
638+
} else if (!overwrite) {
639+
return previousPackage;
546640
}
547641
}
548642

@@ -568,6 +662,20 @@ rem Executable: ${executable}
568662
rem Script: ${script}
569663
$invocation %*
570664
""";
665+
666+
if (snapshot != null) {
667+
batch += """
668+
669+
rem The VM exits with code 255 if the snapshot version is out-of-date.
670+
rem If it is, we need to delete it and run "pub global" manually.
671+
if not errorlevel 255 (
672+
exit /b %errorlevel%
673+
)
674+
675+
pub global run ${package.name}:$script %*
676+
""";
677+
}
678+
571679
writeTextFile(binStubPath, batch);
572680
} else {
573681
var bash = """
@@ -579,6 +687,21 @@ $invocation %*
579687
# Script: ${script}
580688
$invocation "\$@"
581689
""";
690+
691+
if (snapshot != null) {
692+
bash += """
693+
694+
# The VM exits with code 255 if the snapshot version is out-of-date.
695+
# If it is, we need to delete it and run "pub global" manually.
696+
exit_code=\$?
697+
if [[ \$exit_code != 255 ]]; then
698+
exit \$exit_code
699+
fi
700+
701+
pub global run ${package.name}:$script "\$@"
702+
""";
703+
}
704+
582705
writeTextFile(binStubPath, bash);
583706

584707
// Make it executable.
@@ -606,13 +729,13 @@ $invocation "\$@"
606729

607730
for (var file in listDir(_binStubDir, includeDirs: false)) {
608731
var contents = readTextFile(file);
609-
var match = _binStubPackagePattern.firstMatch(contents);
610-
if (match == null) {
732+
var binStubPackage = _binStubProperty(contents, "Package");
733+
if (binStubPackage == null) {
611734
log.fine("Could not parse binstub $file:\n$contents");
612735
continue;
613736
}
614737

615-
if (match[1] == package) {
738+
if (binStubPackage == package) {
616739
log.fine("Deleting old binstub $file");
617740
deleteEntry(file);
618741
}
@@ -621,11 +744,14 @@ $invocation "\$@"
621744

622745
/// Checks to see if the binstubs are on the user's PATH and, if not, suggests
623746
/// that the user add the directory to their PATH.
624-
void _suggestIfNotOnPath(List<String> installed) {
747+
///
748+
/// [installed] should be the name of an installed executable that can be used
749+
/// to test whether accessing it on the path works.
750+
void _suggestIfNotOnPath(String installed) {
625751
if (Platform.operatingSystem == "windows") {
626752
// See if the shell can find one of the binstubs.
627753
// "\q" means return exit code 0 if found or 1 if not.
628-
var result = runProcessSync("where", [r"\q", installed.first + ".bat"]);
754+
var result = runProcessSync("where", [r"\q", installed + ".bat"]);
629755
if (result.exitCode == 0) return;
630756

631757
log.warning(
@@ -636,7 +762,7 @@ $invocation "\$@"
636762
'A web search for "configure windows path" will show you how.');
637763
} else {
638764
// See if the shell can find one of the binstubs.
639-
var result = runProcessSync("which", [installed.first]);
765+
var result = runProcessSync("which", [installed]);
640766
if (result.exitCode == 0) return;
641767

642768
var binDir = _binStubDir;
@@ -655,4 +781,12 @@ $invocation "\$@"
655781
"\n");
656782
}
657783
}
784+
785+
/// Returns the value of the property named [name] in the bin stub script
786+
/// [source].
787+
String _binStubProperty(String source, String name) {
788+
var pattern = new RegExp(quoteRegExp(name) + r": ([a-zA-Z0-9_-]+)");
789+
var match = pattern.firstMatch(source);
790+
return match == null ? null : match[1];
791+
}
658792
}

sdk/lib/_internal/pub/lib/src/io.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,17 @@ void deleteEntry(String path) {
392392
});
393393
}
394394

395+
/// Attempts to delete whatever's at [path], but doesn't throw an exception if
396+
/// the deletion fails.
397+
void tryDeleteEntry(String path) {
398+
try {
399+
deleteEntry(path);
400+
} catch (error, stackTrace) {
401+
log.fine("Failed to delete $entry: $error\n"
402+
"${new Chain.forTrace(stackTrace)}");
403+
}
404+
}
405+
395406
/// "Cleans" [dir].
396407
///
397408
/// If that directory already exists, it is deleted. Then a new empty directory

sdk/lib/_internal/pub/lib/src/log.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,16 +182,16 @@ class Entry {
182182
}
183183

184184
/// Logs [message] at [Level.ERROR].
185-
void error(message, [error]) {
185+
///
186+
/// If [error] is passed, it's appended to [message]. If [trace] is passed, it's
187+
/// printed at log level fine.
188+
void error(message, [error, StackTrace trace]) {
186189
if (error != null) {
187190
message = "$message: $error";
188-
var trace;
189-
if (error is Error) trace = error.stackTrace;
190-
if (trace != null) {
191-
message = "$message\nStackTrace: $trace";
192-
}
191+
if (error is Error && trace == null) trace = error.stackTrace;
193192
}
194193
write(Level.ERROR, message);
194+
if (trace != null) write(Level.FINE, new Chain.forTrace(trace));
195195
}
196196

197197
/// Logs [message] at [Level.WARNING].

0 commit comments

Comments
 (0)