1// Copyright 2015 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6 7import 'package:args/args.dart'; 8import 'package:args/command_runner.dart'; 9import 'package:completion/completion.dart'; 10import 'package:file/file.dart'; 11import 'package:platform/platform.dart'; 12import 'package:process/process.dart'; 13 14import '../artifacts.dart'; 15import '../base/common.dart'; 16import '../base/context.dart'; 17import '../base/file_system.dart'; 18import '../base/flags.dart'; 19import '../base/io.dart' as io; 20import '../base/logger.dart'; 21import '../base/os.dart'; 22import '../base/platform.dart'; 23import '../base/process.dart'; 24import '../base/process_manager.dart'; 25import '../base/terminal.dart'; 26import '../base/user_messages.dart'; 27import '../base/utils.dart'; 28import '../cache.dart'; 29import '../convert.dart'; 30import '../dart/package_map.dart'; 31import '../device.dart'; 32import '../globals.dart'; 33import '../reporting/reporting.dart'; 34import '../tester/flutter_tester.dart'; 35import '../version.dart'; 36import '../vmservice.dart'; 37 38const String kFlutterRootEnvironmentVariableName = 'FLUTTER_ROOT'; // should point to //flutter/ (root of flutter/flutter repo) 39const String kFlutterEngineEnvironmentVariableName = 'FLUTTER_ENGINE'; // should point to //engine/src/ (root of flutter/engine repo) 40const String kSnapshotFileName = 'flutter_tools.snapshot'; // in //flutter/bin/cache/ 41const String kFlutterToolsScriptFileName = 'flutter_tools.dart'; // in //flutter/packages/flutter_tools/bin/ 42const String kFlutterEnginePackageName = 'sky_engine'; 43 44class FlutterCommandRunner extends CommandRunner<void> { 45 FlutterCommandRunner({ bool verboseHelp = false }) : super( 46 'flutter', 47 'Manage your Flutter app development.\n' 48 '\n' 49 'Common commands:\n' 50 '\n' 51 ' flutter create <output directory>\n' 52 ' Create a new Flutter project in the specified directory.\n' 53 '\n' 54 ' flutter run [options]\n' 55 ' Run your Flutter application on an attached device or in an emulator.', 56 ) { 57 argParser.addFlag('verbose', 58 abbr: 'v', 59 negatable: false, 60 help: 'Noisy logging, including all shell commands executed.\n' 61 'If used with --help, shows hidden options.'); 62 argParser.addFlag('quiet', 63 negatable: false, 64 hide: !verboseHelp, 65 help: 'Reduce the amount of output from some commands.'); 66 argParser.addFlag('wrap', 67 negatable: true, 68 hide: !verboseHelp, 69 help: 'Toggles output word wrapping, regardless of whether or not the output is a terminal.', 70 defaultsTo: true); 71 argParser.addOption('wrap-column', 72 hide: !verboseHelp, 73 help: 'Sets the output wrap column. If not set, uses the width of the terminal. No ' 74 'wrapping occurs if not writing to a terminal. Use --no-wrap to turn off wrapping ' 75 'when connected to a terminal.', 76 defaultsTo: null); 77 argParser.addOption('device-id', 78 abbr: 'd', 79 help: 'Target device id or name (prefixes allowed).'); 80 argParser.addFlag('version', 81 negatable: false, 82 help: 'Reports the version of this tool.'); 83 argParser.addFlag('machine', 84 negatable: false, 85 hide: !verboseHelp, 86 help: 'When used with the --version flag, outputs the information using JSON.'); 87 argParser.addFlag('color', 88 negatable: true, 89 hide: !verboseHelp, 90 help: 'Whether to use terminal colors (requires support for ANSI escape sequences).', 91 defaultsTo: true); 92 argParser.addFlag('version-check', 93 negatable: true, 94 defaultsTo: true, 95 hide: !verboseHelp, 96 help: 'Allow Flutter to check for updates when this command runs.'); 97 argParser.addFlag('suppress-analytics', 98 negatable: false, 99 help: 'Suppress analytics reporting when this command runs.'); 100 argParser.addFlag('bug-report', 101 negatable: false, 102 help: 'Captures a bug report file to submit to the Flutter team.\n' 103 'Contains local paths, device identifiers, and log snippets.'); 104 105 String packagesHelp; 106 bool showPackagesCommand; 107 if (fs.isFileSync(kPackagesFileName)) { 108 packagesHelp = '(defaults to "$kPackagesFileName")'; 109 showPackagesCommand = verboseHelp; 110 } else { 111 packagesHelp = '(required, since the current directory does not contain a "$kPackagesFileName" file)'; 112 showPackagesCommand = true; 113 } 114 argParser.addOption('packages', 115 hide: !showPackagesCommand, 116 help: 'Path to your ".packages" file.\n$packagesHelp'); 117 118 argParser.addOption('flutter-root', 119 hide: !verboseHelp, 120 help: 'The root directory of the Flutter repository.\n' 121 'Defaults to \$$kFlutterRootEnvironmentVariableName if set, otherwise uses the parent ' 122 'of the directory that the "flutter" script itself is in.'); 123 124 if (verboseHelp) 125 argParser.addSeparator('Local build selection options (not normally required):'); 126 127 argParser.addOption('local-engine-src-path', 128 hide: !verboseHelp, 129 help: 'Path to your engine src directory, if you are building Flutter locally.\n' 130 'Defaults to \$$kFlutterEngineEnvironmentVariableName if set, otherwise defaults to ' 131 'the path given in your pubspec.yaml dependency_overrides for $kFlutterEnginePackageName, ' 132 'if any, or, failing that, tries to guess at the location based on the value of the ' 133 '--flutter-root option.'); 134 135 argParser.addOption('local-engine', 136 hide: !verboseHelp, 137 help: 'Name of a build output within the engine out directory, if you are building Flutter locally.\n' 138 'Use this to select a specific version of the engine if you have built multiple engine targets.\n' 139 'This path is relative to --local-engine-src-path/out.'); 140 141 if (verboseHelp) 142 argParser.addSeparator('Options for testing the "flutter" tool itself:'); 143 144 argParser.addOption('record-to', 145 hide: !verboseHelp, 146 help: 'Enables recording of process invocations (including stdout and stderr of all such invocations), ' 147 'and file system access (reads and writes).\n' 148 'Serializes that recording to a directory with the path specified in this flag. If the ' 149 'directory does not already exist, it will be created.'); 150 argParser.addOption('replay-from', 151 hide: !verboseHelp, 152 help: 'Enables mocking of process invocations by replaying their stdout, stderr, and exit code from ' 153 'the specified recording (obtained via --record-to). The path specified in this flag must refer ' 154 'to a directory that holds serialized process invocations structured according to the output of ' 155 '--record-to.'); 156 argParser.addFlag('show-test-device', 157 negatable: false, 158 hide: !verboseHelp, 159 help: 'List the special \'flutter-tester\' device in device listings. ' 160 'This headless device is used to\ntest Flutter tooling.'); 161 } 162 163 @override 164 ArgParser get argParser => _argParser; 165 final ArgParser _argParser = ArgParser( 166 allowTrailingOptions: false, 167 usageLineLength: outputPreferences.wrapText ? outputPreferences.wrapColumn : null, 168 ); 169 170 @override 171 String get usageFooter { 172 return wrapText('Run "flutter help -v" for verbose help output, including less commonly used options.'); 173 } 174 175 @override 176 String get usage { 177 final String usageWithoutDescription = super.usage.substring(description.length + 2); 178 return '${wrapText(description)}\n\n$usageWithoutDescription'; 179 } 180 181 static String get defaultFlutterRoot { 182 if (platform.environment.containsKey(kFlutterRootEnvironmentVariableName)) 183 return platform.environment[kFlutterRootEnvironmentVariableName]; 184 try { 185 if (platform.script.scheme == 'data') 186 return '../..'; // we're running as a test 187 188 if (platform.script.scheme == 'package') { 189 final String packageConfigPath = Uri.parse(platform.packageConfig).toFilePath(); 190 return fs.path.dirname(fs.path.dirname(fs.path.dirname(packageConfigPath))); 191 } 192 193 final String script = platform.script.toFilePath(); 194 if (fs.path.basename(script) == kSnapshotFileName) 195 return fs.path.dirname(fs.path.dirname(fs.path.dirname(script))); 196 if (fs.path.basename(script) == kFlutterToolsScriptFileName) 197 return fs.path.dirname(fs.path.dirname(fs.path.dirname(fs.path.dirname(script)))); 198 199 // If run from a bare script within the repo. 200 if (script.contains('flutter/packages/')) 201 return script.substring(0, script.indexOf('flutter/packages/') + 8); 202 if (script.contains('flutter/examples/')) 203 return script.substring(0, script.indexOf('flutter/examples/') + 8); 204 } catch (error) { 205 // we don't have a logger at the time this is run 206 // (which is why we don't use printTrace here) 207 print(userMessages.runnerNoRoot(error)); 208 } 209 return '.'; 210 } 211 212 @override 213 ArgResults parse(Iterable<String> args) { 214 try { 215 // This is where the CommandRunner would call argParser.parse(args). We 216 // override this function so we can call tryArgsCompletion instead, so the 217 // completion package can interrogate the argParser, and as part of that, 218 // it calls argParser.parse(args) itself and returns the result. 219 return tryArgsCompletion(args, argParser); 220 } on ArgParserException catch (error) { 221 if (error.commands.isEmpty) { 222 usageException(error.message); 223 } 224 225 Command<void> command = commands[error.commands.first]; 226 for (String commandName in error.commands.skip(1)) { 227 command = command.subcommands[commandName]; 228 } 229 230 command.usageException(error.message); 231 return null; 232 } 233 } 234 235 @override 236 Future<void> run(Iterable<String> args) { 237 // Have an invocation of 'build' print out it's sub-commands. 238 // TODO(ianh): Move this to the Build command itself somehow. 239 if (args.length == 1 && args.first == 'build') 240 args = <String>['build', '-h']; 241 242 return super.run(args); 243 } 244 245 @override 246 Future<void> runCommand(ArgResults topLevelResults) async { 247 final Map<Type, dynamic> contextOverrides = <Type, dynamic>{ 248 Flags: Flags(topLevelResults), 249 }; 250 251 // Check for verbose. 252 if (topLevelResults['verbose']) { 253 // Override the logger. 254 contextOverrides[Logger] = VerboseLogger(logger); 255 } 256 257 // Don't set wrapColumns unless the user said to: if it's set, then all 258 // wrapping will occur at this width explicitly, and won't adapt if the 259 // terminal size changes during a run. 260 int wrapColumn; 261 if (topLevelResults.wasParsed('wrap-column')) { 262 try { 263 wrapColumn = int.parse(topLevelResults['wrap-column']); 264 if (wrapColumn < 0) { 265 throwToolExit(userMessages.runnerWrapColumnInvalid(topLevelResults['wrap-column'])); 266 } 267 } on FormatException { 268 throwToolExit(userMessages.runnerWrapColumnParseError(topLevelResults['wrap-column'])); 269 } 270 } 271 272 // If we're not writing to a terminal with a defined width, then don't wrap 273 // anything, unless the user explicitly said to. 274 final bool useWrapping = topLevelResults.wasParsed('wrap') 275 ? topLevelResults['wrap'] 276 : io.stdio.terminalColumns == null ? false : topLevelResults['wrap']; 277 contextOverrides[OutputPreferences] = OutputPreferences( 278 wrapText: useWrapping, 279 showColor: topLevelResults['color'], 280 wrapColumn: wrapColumn, 281 ); 282 283 if (topLevelResults['show-test-device'] || 284 topLevelResults['device-id'] == FlutterTesterDevices.kTesterDeviceId) { 285 FlutterTesterDevices.showFlutterTesterDevice = true; 286 } 287 288 String recordTo = topLevelResults['record-to']; 289 String replayFrom = topLevelResults['replay-from']; 290 291 if (topLevelResults['bug-report']) { 292 // --bug-report implies --record-to=<tmp_path> 293 final Directory tempDir = const LocalFileSystem() 294 .systemTempDirectory 295 .createTempSync('flutter_tools_bug_report.'); 296 recordTo = tempDir.path; 297 298 // Record the arguments that were used to invoke this runner. 299 final File manifest = tempDir.childFile('MANIFEST.txt'); 300 final StringBuffer buffer = StringBuffer() 301 ..writeln('# arguments') 302 ..writeln(topLevelResults.arguments) 303 ..writeln() 304 ..writeln('# rest') 305 ..writeln(topLevelResults.rest); 306 await manifest.writeAsString(buffer.toString(), flush: true); 307 308 // ZIP the recording up once the recording has been serialized. 309 addShutdownHook(() async { 310 final File zipFile = getUniqueFile(fs.currentDirectory, 'bugreport', 'zip'); 311 os.zip(tempDir, zipFile); 312 printStatus(userMessages.runnerBugReportFinished(zipFile.basename)); 313 }, ShutdownStage.POST_PROCESS_RECORDING); 314 addShutdownHook(() => tempDir.delete(recursive: true), ShutdownStage.CLEANUP); 315 } 316 317 assert(recordTo == null || replayFrom == null); 318 319 if (recordTo != null) { 320 recordTo = recordTo.trim(); 321 if (recordTo.isEmpty) 322 throwToolExit(userMessages.runnerNoRecordTo); 323 contextOverrides.addAll(<Type, dynamic>{ 324 ProcessManager: getRecordingProcessManager(recordTo), 325 FileSystem: getRecordingFileSystem(recordTo), 326 Platform: await getRecordingPlatform(recordTo), 327 }); 328 VMService.enableRecordingConnection(recordTo); 329 } 330 331 if (replayFrom != null) { 332 replayFrom = replayFrom.trim(); 333 if (replayFrom.isEmpty) 334 throwToolExit(userMessages.runnerNoReplayFrom); 335 contextOverrides.addAll(<Type, dynamic>{ 336 ProcessManager: await getReplayProcessManager(replayFrom), 337 FileSystem: getReplayFileSystem(replayFrom), 338 Platform: await getReplayPlatform(replayFrom), 339 }); 340 VMService.enableReplayConnection(replayFrom); 341 } 342 343 // We must set Cache.flutterRoot early because other features use it (e.g. 344 // enginePath's initializer uses it). 345 final String flutterRoot = topLevelResults['flutter-root'] ?? defaultFlutterRoot; 346 Cache.flutterRoot = fs.path.normalize(fs.path.absolute(flutterRoot)); 347 348 // Set up the tooling configuration. 349 final String enginePath = _findEnginePath(topLevelResults); 350 if (enginePath != null) { 351 contextOverrides.addAll(<Type, dynamic>{ 352 Artifacts: Artifacts.getLocalEngine(enginePath, _findEngineBuildPath(topLevelResults, enginePath)), 353 }); 354 } 355 356 await context.run<void>( 357 overrides: contextOverrides.map<Type, Generator>((Type type, dynamic value) { 358 return MapEntry<Type, Generator>(type, () => value); 359 }), 360 body: () async { 361 logger.quiet = topLevelResults['quiet']; 362 363 if (platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true') 364 await Cache.lock(); 365 366 if (topLevelResults['suppress-analytics']) 367 flutterUsage.suppressAnalytics = true; 368 369 _checkFlutterCopy(); 370 try { 371 await FlutterVersion.instance.ensureVersionFile(); 372 } on FileSystemException catch (e) { 373 printError('Failed to write the version file to the artifact cache: "$e".'); 374 printError('Please ensure you have permissions in the artifact cache directory.'); 375 throwToolExit('Failed to write the version file'); 376 } 377 if (topLevelResults.command?.name != 'upgrade' && topLevelResults['version-check']) { 378 await FlutterVersion.instance.checkFlutterVersionFreshness(); 379 } 380 381 if (topLevelResults.wasParsed('packages')) 382 PackageMap.globalPackagesPath = fs.path.normalize(fs.path.absolute(topLevelResults['packages'])); 383 384 // See if the user specified a specific device. 385 deviceManager.specifiedDeviceId = topLevelResults['device-id']; 386 387 if (topLevelResults['version']) { 388 flutterUsage.sendCommand('version'); 389 String status; 390 if (topLevelResults['machine']) { 391 status = const JsonEncoder.withIndent(' ').convert(FlutterVersion.instance.toJson()); 392 } else { 393 status = FlutterVersion.instance.toString(); 394 } 395 printStatus(status); 396 return; 397 } 398 399 if (topLevelResults['machine']) { 400 throwToolExit('The --machine flag is only valid with the --version flag.', exitCode: 2); 401 } 402 await super.runCommand(topLevelResults); 403 }, 404 ); 405 } 406 407 String _tryEnginePath(String enginePath) { 408 if (fs.isDirectorySync(fs.path.join(enginePath, 'out'))) 409 return enginePath; 410 return null; 411 } 412 413 String _findEnginePath(ArgResults globalResults) { 414 String engineSourcePath = globalResults['local-engine-src-path'] ?? platform.environment[kFlutterEngineEnvironmentVariableName]; 415 416 if (engineSourcePath == null && globalResults['local-engine'] != null) { 417 try { 418 Uri engineUri = PackageMap(PackageMap.globalPackagesPath).map[kFlutterEnginePackageName]; 419 // Skip if sky_engine is the self-contained one. 420 if (engineUri != null && fs.identicalSync(fs.path.join(Cache.flutterRoot, 'bin', 'cache', 'pkg', kFlutterEnginePackageName, 'lib'), engineUri.path)) { 421 engineUri = null; 422 } 423 // If sky_engine is specified and the engineSourcePath not set, try to determine the engineSourcePath by sky_engine setting. 424 // A typical engineUri looks like: file://flutter-engine-local-path/src/out/host_debug_unopt/gen/dart-pkg/sky_engine/lib/ 425 if (engineUri?.path != null) { 426 engineSourcePath = fs.directory(engineUri.path)?.parent?.parent?.parent?.parent?.parent?.parent?.path; 427 if (engineSourcePath != null && (engineSourcePath == fs.path.dirname(engineSourcePath) || engineSourcePath.isEmpty)) { 428 engineSourcePath = null; 429 throwToolExit(userMessages.runnerNoEngineSrcDir(kFlutterEnginePackageName, kFlutterEngineEnvironmentVariableName), 430 exitCode: 2); 431 } 432 } 433 } on FileSystemException { 434 engineSourcePath = null; 435 } on FormatException { 436 engineSourcePath = null; 437 } 438 // If engineSourcePath is still not set, try to determine it by flutter root. 439 engineSourcePath ??= _tryEnginePath(fs.path.join(fs.directory(Cache.flutterRoot).parent.path, 'engine', 'src')); 440 } 441 442 if (engineSourcePath != null && _tryEnginePath(engineSourcePath) == null) { 443 throwToolExit(userMessages.runnerNoEngineBuildDirInPath(engineSourcePath), 444 exitCode: 2); 445 } 446 447 return engineSourcePath; 448 } 449 450 String _getHostEngineBasename(String localEngineBasename) { 451 // Determine the host engine directory associated with the local engine: 452 // Strip '_sim_' since there are no host simulator builds. 453 String tmpBasename = localEngineBasename.replaceFirst('_sim_', '_'); 454 tmpBasename = tmpBasename.substring(tmpBasename.indexOf('_') + 1); 455 // Strip suffix for various archs. 456 final List<String> suffixes = <String>['_arm', '_arm64', '_x86', '_x64']; 457 for (String suffix in suffixes) { 458 tmpBasename = tmpBasename.replaceFirst(RegExp('$suffix\$'), ''); 459 } 460 return 'host_' + tmpBasename; 461 } 462 463 EngineBuildPaths _findEngineBuildPath(ArgResults globalResults, String enginePath) { 464 String localEngine; 465 if (globalResults['local-engine'] != null) { 466 localEngine = globalResults['local-engine']; 467 } else { 468 throwToolExit(userMessages.runnerLocalEngineRequired, exitCode: 2); 469 } 470 471 final String engineBuildPath = fs.path.normalize(fs.path.join(enginePath, 'out', localEngine)); 472 if (!fs.isDirectorySync(engineBuildPath)) { 473 throwToolExit(userMessages.runnerNoEngineBuild(engineBuildPath), exitCode: 2); 474 } 475 476 final String basename = fs.path.basename(engineBuildPath); 477 final String hostBasename = _getHostEngineBasename(basename); 478 final String engineHostBuildPath = fs.path.normalize(fs.path.join(fs.path.dirname(engineBuildPath), hostBasename)); 479 if (!fs.isDirectorySync(engineHostBuildPath)) { 480 throwToolExit(userMessages.runnerNoEngineBuild(engineHostBuildPath), exitCode: 2); 481 } 482 483 return EngineBuildPaths(targetEngine: engineBuildPath, hostEngine: engineHostBuildPath); 484 } 485 486 static void initFlutterRoot() { 487 Cache.flutterRoot ??= defaultFlutterRoot; 488 } 489 490 /// Get the root directories of the repo - the directories containing Dart packages. 491 List<String> getRepoRoots() { 492 final String root = fs.path.absolute(Cache.flutterRoot); 493 // not bin, and not the root 494 return <String>['dev', 'examples', 'packages'].map<String>((String item) { 495 return fs.path.join(root, item); 496 }).toList(); 497 } 498 499 /// Get all pub packages in the Flutter repo. 500 List<Directory> getRepoPackages() { 501 return getRepoRoots() 502 .expand<String>((String root) => _gatherProjectPaths(root)) 503 .map<Directory>((String dir) => fs.directory(dir)) 504 .toList(); 505 } 506 507 static List<String> _gatherProjectPaths(String rootPath) { 508 if (fs.isFileSync(fs.path.join(rootPath, '.dartignore'))) 509 return <String>[]; 510 511 512 final List<String> projectPaths = fs.directory(rootPath) 513 .listSync(followLinks: false) 514 .expand((FileSystemEntity entity) { 515 if (entity is Directory && !fs.path.split(entity.path).contains('.dart_tool')) { 516 return _gatherProjectPaths(entity.path); 517 } 518 return <String>[]; 519 }) 520 .toList(); 521 522 if (fs.isFileSync(fs.path.join(rootPath, 'pubspec.yaml'))) 523 projectPaths.add(rootPath); 524 525 return projectPaths; 526 } 527 528 void _checkFlutterCopy() { 529 // If the current directory is contained by a flutter repo, check that it's 530 // the same flutter that is currently running. 531 String directory = fs.path.normalize(fs.path.absolute(fs.currentDirectory.path)); 532 533 // Check if the cwd is a flutter dir. 534 while (directory.isNotEmpty) { 535 if (_isDirectoryFlutterRepo(directory)) { 536 if (!_compareResolvedPaths(directory, Cache.flutterRoot)) { 537 printError(userMessages.runnerWrongFlutterInstance(Cache.flutterRoot, directory)); 538 } 539 540 break; 541 } 542 543 final String parent = fs.path.dirname(directory); 544 if (parent == directory) 545 break; 546 directory = parent; 547 } 548 549 // Check that the flutter running is that same as the one referenced in the pubspec. 550 if (fs.isFileSync(kPackagesFileName)) { 551 final PackageMap packageMap = PackageMap(kPackagesFileName); 552 Uri flutterUri; 553 try { 554 flutterUri = packageMap.map['flutter']; 555 } on FormatException { 556 // We're not quite sure why this can happen, perhaps the user 557 // accidentally edited the .packages file. Re-running pub should 558 // fix the issue, and we definitely shouldn't crash here. 559 printTrace('Failed to parse .packages file to check flutter dependency.'); 560 return; 561 } 562 563 if (flutterUri != null && (flutterUri.scheme == 'file' || flutterUri.scheme == '')) { 564 // .../flutter/packages/flutter/lib 565 final Uri rootUri = flutterUri.resolve('../../..'); 566 final String flutterPath = fs.path.normalize(fs.file(rootUri).absolute.path); 567 568 if (!fs.isDirectorySync(flutterPath)) { 569 printError(userMessages.runnerRemovedFlutterRepo(Cache.flutterRoot, flutterPath)); 570 } else if (!_compareResolvedPaths(flutterPath, Cache.flutterRoot)) { 571 printError(userMessages.runnerChangedFlutterRepo(Cache.flutterRoot, flutterPath)); 572 } 573 } 574 } 575 } 576 577 // Check if `bin/flutter` and `bin/cache/engine.stamp` exist. 578 bool _isDirectoryFlutterRepo(String directory) { 579 return 580 fs.isFileSync(fs.path.join(directory, 'bin/flutter')) && 581 fs.isFileSync(fs.path.join(directory, 'bin/cache/engine.stamp')); 582 } 583} 584 585bool _compareResolvedPaths(String path1, String path2) { 586 path1 = fs.directory(fs.path.absolute(path1)).resolveSymbolicLinksSync(); 587 path2 = fs.directory(fs.path.absolute(path2)).resolveSymbolicLinksSync(); 588 589 return path1 == path2; 590} 591