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 diff --git a/tool/lib/model.dart b/tool/lib/model.dart index 2032b4c5ec7..6e7b7ba8507 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; + try { + resolvedPath = File(sdkPath).resolveSymbolicLinksSync(); + } catch (_) { + // Fallback to the unresolved path if resolution fails. + } + + 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/test/model_test.dart b/tool/test/model_test.dart new file mode 100644 index 00000000000..5279086b286 --- /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()), + ); + }); + }); +}