Allow flutter SDK binary to be a symlink (#9924)
* 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`.
* Revert change to "findRemote" behavior
* Revert change to "findRemote" behavior
* Simplify symlink resolution using try-catch fallback in FlutterSdk.findFromPath
* Add release note for dt sync symlink fix
* format
* Revert changes to release notes
* extract repeated calls to path.dirname
* Avoid explicit variable type declaration.
* Remove ignore for DCM
diff --git a/tool/lib/model.dart b/tool/lib/model.dart
index 2032b4c..12df455 100644
--- a/tool/lib/model.dart
+++ b/tool/lib/model.dart
@@ -242,8 +242,20 @@
}
static FlutterSdk findFromPath(String sdkPath) {
- if (path.basename(path.dirname(sdkPath)) == 'bin') {
- return FlutterSdk._(path.dirname(path.dirname(sdkPath)));
+ var resolvedPath = sdkPath;
+ try {
+ resolvedPath = File(sdkPath).resolveSymbolicLinksSync();
+ } catch (_) {
+ // Fallback to the unresolved path if resolution fails.
+ }
+
+ final resolvedPathParent = path.dirname(resolvedPath);
+ if (path.basename(resolvedPathParent) == 'bin') {
+ return FlutterSdk._(path.dirname(resolvedPathParent));
+ } else if (path.basename(resolvedPath) == 'bin') {
+ return FlutterSdk._(resolvedPathParent);
+ } else if (Directory(path.join(resolvedPath, 'bin')).existsSync()) {
+ return FlutterSdk._(resolvedPath);
}
throw Exception('Unable to locate the Flutter SDK at "$sdkPath"');
@@ -258,10 +270,7 @@
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/test/model_test.dart b/tool/test/model_test.dart
new file mode 100644
index 0000000..5279086
--- /dev/null
+++ b/tool/test/model_test.dart
@@ -0,0 +1,61 @@
+// 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()));
+ });
+
+ test('throws Exception when unable to locate Flutter SDK', () {
+ expect(
+ () => FlutterSdk.findFromPath('/non/existent/path/to/flutter'),
+ throwsA(isA<Exception>()),
+ );
+ });
+ });
+}