From 10ed08988ee33b7525018d842894bcd33e83e88f Mon Sep 17 00:00:00 2001 From: John Ryan Date: Tue, 28 Jul 2026 09:08:22 -0700 Subject: [PATCH 1/6] Fix SDK path resolution when Flutter on PATH is a symlink When running `dt update-flutter-sdk --update-on-path`, `FlutterSdk.findFromPathEnvironmentVariable` failed if `flutter` on PATH was a symlink (e.g. `/usr/local/bin/flutter`). It stripped `bin/flutter` from the unresolved symlink path, resulting in `/usr/local` being treated as the SDK root directory. This change: - Updates `FlutterSdk.findFromPath` to resolve symbolic links before determining the SDK root directory. - Updates `UpdateFlutterSdkCommand` to use the remote returned by `findRemote` instead of hardcoding 'upstream'. - Adds unit tests in `tool/test/model_test.dart` for `FlutterSdk.findFromPath`. --- tool/lib/commands/update_flutter_sdk.dart | 8 ++-- tool/lib/model.dart | 20 ++++++--- tool/lib/utils.dart | 2 +- tool/test/model_test.dart | 52 +++++++++++++++++++++++ 4 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 tool/test/model_test.dart diff --git a/tool/lib/commands/update_flutter_sdk.dart b/tool/lib/commands/update_flutter_sdk.dart index 73a95f5070c..9b6dfd28359 100644 --- a/tool/lib/commands/update_flutter_sdk.dart +++ b/tool/lib/commands/update_flutter_sdk.dart @@ -65,7 +65,7 @@ class UpdateFlutterSdkCommand extends Command { log.stdout('Updating Flutter from PATH at ${pathSdk.sdkPath}'); // Verify we have an upstream remote to pull from. - await findRemote( + final remote = await findRemote( processManager, remoteId: 'flutter/flutter.git', workingDirectory: pathSdk.sdkPath, @@ -74,9 +74,9 @@ class UpdateFlutterSdkCommand extends Command { await processManager.runAll( commands: [ CliCommand.git(['stash']), - CliCommand.git(['fetch', 'upstream']), - CliCommand.git(['checkout', 'upstream/master']), - CliCommand.git(['reset', '--hard', 'upstream/master']), + CliCommand.git(['fetch', remote]), + CliCommand.git(['checkout', '$remote/master']), + CliCommand.git(['reset', '--hard', '$remote/master']), CliCommand.git(['checkout', flutterVersion, '-f']), CliCommand.flutter(['--version']), ], diff --git a/tool/lib/model.dart b/tool/lib/model.dart index 2032b4c5ec7..e1eb8bfffc3 100644 --- a/tool/lib/model.dart +++ b/tool/lib/model.dart @@ -242,8 +242,19 @@ class FlutterSdk { } static FlutterSdk findFromPath(String sdkPath) { - if (path.basename(path.dirname(sdkPath)) == 'bin') { - return FlutterSdk._(path.dirname(path.dirname(sdkPath))); + var resolvedPath = sdkPath; + if (File(sdkPath).existsSync() || + Link(sdkPath).existsSync() || + Directory(sdkPath).existsSync()) { + resolvedPath = File(sdkPath).resolveSymbolicLinksSync(); + } + + if (path.basename(path.dirname(resolvedPath)) == 'bin') { + return FlutterSdk._(path.dirname(path.dirname(resolvedPath))); + } else if (path.basename(resolvedPath) == 'bin') { + return FlutterSdk._(path.dirname(resolvedPath)); + } else if (Directory(path.join(resolvedPath, 'bin')).existsSync()) { + return FlutterSdk._(resolvedPath); } throw Exception('Unable to locate the Flutter SDK at "$sdkPath"'); @@ -258,10 +269,7 @@ class FlutterSdk { final result = Process.runSync(whichCommand, ['flutter']); if (result.exitCode == 0) { final sdkPath = result.stdout.toString().split('\n').first.trim(); - // 'flutter/bin' - if (path.basename(path.dirname(sdkPath)) == 'bin') { - return FlutterSdk._(path.dirname(path.dirname(sdkPath))); - } + return findFromPath(sdkPath); } throw Exception('Unable to locate the Flutter SDK on PATH'); diff --git a/tool/lib/utils.dart b/tool/lib/utils.dart index 97e3b593e2b..51e51b1f550 100644 --- a/tool/lib/utils.dart +++ b/tool/lib/utils.dart @@ -268,7 +268,7 @@ Future findRemote( ); } final remoteUpstream = upstreamRemoteResult.namedGroup('remote')!; - print('Found upstream remote.'); + print('Found remote "$remoteUpstream".'); return remoteUpstream; } diff --git a/tool/test/model_test.dart b/tool/test/model_test.dart new file mode 100644 index 00000000000..91750bdc6a4 --- /dev/null +++ b/tool/test/model_test.dart @@ -0,0 +1,52 @@ +// Copyright 2026 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'dart:io'; + +import 'package:devtools_tool/model.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +void main() { + group('FlutterSdk.findFromPath', () { + late Directory tempDir; + + setUp(() { + tempDir = Directory.systemTemp.createTempSync('flutter_sdk_test_'); + }); + + tearDown(() { + tempDir.deleteSync(recursive: true); + }); + + test('finds SDK root from bin/flutter executable path', () { + final binDir = Directory(path.join(tempDir.path, 'bin'))..createSync(); + final flutterExe = File(path.join(binDir.path, 'flutter'))..createSync(); + + final sdk = FlutterSdk.findFromPath(flutterExe.path); + expect(sdk.sdkPath, equals(tempDir.resolveSymbolicLinksSync())); + }); + + test('resolves symbolic links to find actual SDK root', () { + final sdkDir = Directory(path.join(tempDir.path, 'real_sdk'))..createSync(); + final binDir = Directory(path.join(sdkDir.path, 'bin'))..createSync(); + final flutterExe = File(path.join(binDir.path, 'flutter'))..createSync(); + + final linkDir = Directory(path.join(tempDir.path, 'symlink_bin'))..createSync(); + final symlink = Link(path.join(linkDir.path, 'flutter')); + symlink.createSync(flutterExe.path); + + final sdk = FlutterSdk.findFromPath(symlink.path); + expect(sdk.sdkPath, equals(sdkDir.resolveSymbolicLinksSync())); + }); + + test('finds SDK root when given SDK directory path directly', () { + final binDir = Directory(path.join(tempDir.path, 'bin'))..createSync(); + File(path.join(binDir.path, 'flutter')).createSync(); + + final sdk = FlutterSdk.findFromPath(tempDir.path); + expect(sdk.sdkPath, equals(tempDir.resolveSymbolicLinksSync())); + }); + }); +} From 6e472e868a3222c38e2e44d7f7574195a1d88887 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Tue, 28 Jul 2026 13:45:27 -0700 Subject: [PATCH 2/6] Revert change to "findRemote" behavior --- tool/lib/commands/update_flutter_sdk.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tool/lib/commands/update_flutter_sdk.dart b/tool/lib/commands/update_flutter_sdk.dart index 9b6dfd28359..73a95f5070c 100644 --- a/tool/lib/commands/update_flutter_sdk.dart +++ b/tool/lib/commands/update_flutter_sdk.dart @@ -65,7 +65,7 @@ class UpdateFlutterSdkCommand extends Command { log.stdout('Updating Flutter from PATH at ${pathSdk.sdkPath}'); // Verify we have an upstream remote to pull from. - final remote = await findRemote( + await findRemote( processManager, remoteId: 'flutter/flutter.git', workingDirectory: pathSdk.sdkPath, @@ -74,9 +74,9 @@ class UpdateFlutterSdkCommand extends Command { await processManager.runAll( commands: [ CliCommand.git(['stash']), - CliCommand.git(['fetch', remote]), - CliCommand.git(['checkout', '$remote/master']), - CliCommand.git(['reset', '--hard', '$remote/master']), + CliCommand.git(['fetch', 'upstream']), + CliCommand.git(['checkout', 'upstream/master']), + CliCommand.git(['reset', '--hard', 'upstream/master']), CliCommand.git(['checkout', flutterVersion, '-f']), CliCommand.flutter(['--version']), ], From a59390c1cc03b576bf4ddc561ad715bc54680ed4 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Tue, 28 Jul 2026 13:49:20 -0700 Subject: [PATCH 3/6] Revert change to "findRemote" behavior --- tool/lib/utils.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tool/lib/utils.dart b/tool/lib/utils.dart index 51e51b1f550..97e3b593e2b 100644 --- a/tool/lib/utils.dart +++ b/tool/lib/utils.dart @@ -268,7 +268,7 @@ Future findRemote( ); } final remoteUpstream = upstreamRemoteResult.namedGroup('remote')!; - print('Found remote "$remoteUpstream".'); + print('Found upstream remote.'); return remoteUpstream; } From 710edad308d7e825a012acb1482d33fc1286a99d Mon Sep 17 00:00:00 2001 From: John Ryan Date: Tue, 28 Jul 2026 13:50:15 -0700 Subject: [PATCH 4/6] Simplify symlink resolution using try-catch fallback in FlutterSdk.findFromPath --- tool/lib/model.dart | 6 +++--- tool/test/model_test.dart | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/tool/lib/model.dart b/tool/lib/model.dart index e1eb8bfffc3..6e7b7ba8507 100644 --- a/tool/lib/model.dart +++ b/tool/lib/model.dart @@ -243,10 +243,10 @@ class FlutterSdk { static FlutterSdk findFromPath(String sdkPath) { var resolvedPath = sdkPath; - if (File(sdkPath).existsSync() || - Link(sdkPath).existsSync() || - Directory(sdkPath).existsSync()) { + try { resolvedPath = File(sdkPath).resolveSymbolicLinksSync(); + } catch (_) { + // Fallback to the unresolved path if resolution fails. } if (path.basename(path.dirname(resolvedPath)) == 'bin') { diff --git a/tool/test/model_test.dart b/tool/test/model_test.dart index 91750bdc6a4..e1f2e3a418c 100644 --- a/tool/test/model_test.dart +++ b/tool/test/model_test.dart @@ -48,5 +48,12 @@ void main() { final sdk = FlutterSdk.findFromPath(tempDir.path); expect(sdk.sdkPath, equals(tempDir.resolveSymbolicLinksSync())); }); + + test('throws Exception when unable to locate Flutter SDK', () { + expect( + () => FlutterSdk.findFromPath('/non/existent/path/to/flutter'), + throwsA(isA()), + ); + }); }); } From d35de9b3b53eaae32dd866298cc6d39554e0a48c Mon Sep 17 00:00:00 2001 From: John Ryan Date: Tue, 28 Jul 2026 13:54:24 -0700 Subject: [PATCH 5/6] Add release note for dt sync symlink fix --- packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index aa9373543f5..01269ede1d8 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -15,7 +15,7 @@ To learn more about DevTools, check out the ## General updates -TODO: Remove this section if there are not any updates. +* Fixed an issue with `dt sync` when Flutter on PATH is a symlink. [#9924](https://github.com/flutter/devtools/pull/9924) ## Inspector updates From ca912ee37afc3a668e31a097c13e451d3113e233 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Tue, 28 Jul 2026 14:04:11 -0700 Subject: [PATCH 6/6] format --- tool/test/model_test.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tool/test/model_test.dart b/tool/test/model_test.dart index e1f2e3a418c..5279086b286 100644 --- a/tool/test/model_test.dart +++ b/tool/test/model_test.dart @@ -29,11 +29,13 @@ void main() { }); test('resolves symbolic links to find actual SDK root', () { - final sdkDir = Directory(path.join(tempDir.path, 'real_sdk'))..createSync(); + final sdkDir = Directory(path.join(tempDir.path, 'real_sdk')) + ..createSync(); final binDir = Directory(path.join(sdkDir.path, 'bin'))..createSync(); final flutterExe = File(path.join(binDir.path, 'flutter'))..createSync(); - final linkDir = Directory(path.join(tempDir.path, 'symlink_bin'))..createSync(); + final linkDir = Directory(path.join(tempDir.path, 'symlink_bin')) + ..createSync(); final symlink = Link(path.join(linkDir.path, 'flutter')); symlink.createSync(flutterExe.path);