From 8a7103a46791717811e3a1f70e24a7f3d154f2ee Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Fri, 21 Oct 2022 15:20:54 +0200 Subject: [PATCH] Allow setting flutterSdkPath and dartSdkPath for the flutter and dart commands (#75) Co-authored-by: Giuseppe Cianci <39117631+Giuspepe@users.noreply.github.com> --- sidekick_core/lib/sidekick_core.dart | 54 +++++++++++++++- .../lib/src/commands/dart_command.dart | 3 +- .../lib/src/commands/flutter_command.dart | 22 ++++--- sidekick_core/lib/src/dart.dart | 63 ++++++++++++++++--- sidekick_core/lib/src/flutter.dart | 49 +++++++++++++++ sidekick_core/lib/src/flutterw.dart | 22 ++++--- sidekick_core/test/dart_command_test.dart | 54 ++++++++++++++++ sidekick_core/test/fake_sdk.dart | 38 +++++++++++ sidekick_core/test/flutter_command_test.dart | 38 +++++++++++ sidekick_core/test/init_test.dart | 3 + 10 files changed, 315 insertions(+), 31 deletions(-) create mode 100644 sidekick_core/lib/src/flutter.dart create mode 100644 sidekick_core/test/dart_command_test.dart create mode 100644 sidekick_core/test/fake_sdk.dart create mode 100644 sidekick_core/test/flutter_command_test.dart diff --git a/sidekick_core/lib/sidekick_core.dart b/sidekick_core/lib/sidekick_core.dart index dd01fa57..fafffdf7 100644 --- a/sidekick_core/lib/sidekick_core.dart +++ b/sidekick_core/lib/sidekick_core.dart @@ -24,6 +24,7 @@ export 'package:sidekick_core/src/dart.dart'; export 'package:sidekick_core/src/dart_package.dart'; export 'package:sidekick_core/src/dart_runtime.dart'; export 'package:sidekick_core/src/file_util.dart'; +export 'package:sidekick_core/src/flutter.dart'; export 'package:sidekick_core/src/flutterw.dart'; export 'package:sidekick_core/src/forward_command.dart'; export 'package:sidekick_core/src/git.dart'; @@ -34,13 +35,23 @@ export 'package:sidekick_core/src/sidekick_package.dart'; /// /// Set [name] to the name of your CLI entrypoint /// -/// [mainProjectPath], when set, links to the main package. For a flutter apps -/// it is the package that actually builds the flutter app. -/// Set [mainProjectPath] relative to the git repository root +/// [mainProjectPath] should be set when you have a package that you +/// consider the main package of the whole repository. +/// When your repository contains only one Flutter package in root set +/// `mainProjectPath = '.'`. +/// In a multi package repository you might use the same when the main package +/// is in root, or `mainProjectPath = 'packages/my_app'` when it is in a subfolder. +/// +/// Set [flutterSdkPath] when you bind a flutter sdk to this project. This SDK +/// enables the [flutter] and [dart] commands. +/// [dartSdkPath] is inherited from [flutterSdkPath]. Set it only for pure dart +/// projects. SidekickCommandRunner initializeSidekick({ required String name, String? description, String? mainProjectPath, + String? flutterSdkPath, + String? dartSdkPath, }) { DartPackage? mainProject; @@ -57,6 +68,8 @@ SidekickCommandRunner initializeSidekick({ repository: repo, mainProject: mainProject, workingDirectory: Directory.current, + flutterSdk: flutterSdkPath == null ? null : Directory(flutterSdkPath), + dartSdk: dartSdkPath == null ? null : Directory(dartSdkPath), ); return runner; } @@ -70,11 +83,15 @@ class SidekickCommandRunner extends CommandRunner { required this.repository, this.mainProject, required this.workingDirectory, + this.flutterSdk, + this.dartSdk, }) : super(cliName, description); final Repository repository; final DartPackage? mainProject; final Directory workingDirectory; + final Directory? flutterSdk; + final Directory? dartSdk; /// Mounts the sidekick related globals, returns a function to unmount them /// and restore the previous globals @@ -91,6 +108,9 @@ class SidekickCommandRunner extends CommandRunner { @override Future run(Iterable args) async { + // a new command gets executes, reset whatever exitCode the previous command has set + exitCode = 0; + final unmount = mount(); final result = await super.run(args); unmount(); @@ -149,3 +169,31 @@ DartPackage? get mainProject { } return _activeRunner?.mainProject; } + +/// Returns the path to he Flutter SDK sidekick should use for the [flutter] command +/// +/// This variable is usually set to a pinned version of the Flutter SDK per project, i.e. +/// - https://github.com/passsy/flutter_wrapper +/// - https://github.com/fluttertools/fvm +Directory? get flutterSdk { + if (_activeRunner == null) { + error( + 'You cannot access flutterSdk ' + 'outside of a Command executed with SidekickCommandRunner.', + ); + } + return _activeRunner?.flutterSdk; +} + +/// Returns the path to the Dart SDK sidekick should use for the [dart] command +/// +/// Usually inherited from [flutterSdk] which ships with an embedded Dart SDK +Directory? get dartSdk { + if (_activeRunner == null) { + error( + 'You cannot access dartSdk ' + 'outside of a Command executed with SidekickCommandRunner.', + ); + } + return _activeRunner?.dartSdk; +} diff --git a/sidekick_core/lib/src/commands/dart_command.dart b/sidekick_core/lib/src/commands/dart_command.dart index 3af8a5d3..6d7a8487 100644 --- a/sidekick_core/lib/src/commands/dart_command.dart +++ b/sidekick_core/lib/src/commands/dart_command.dart @@ -12,7 +12,6 @@ class DartCommand extends ForwardCommand { @override Future run() async { - final exitCode = dart(argResults!.arguments); - exit(exitCode); + exitCode = dart(argResults!.arguments); } } diff --git a/sidekick_core/lib/src/commands/flutter_command.dart b/sidekick_core/lib/src/commands/flutter_command.dart index c4e05834..08608444 100644 --- a/sidekick_core/lib/src/commands/flutter_command.dart +++ b/sidekick_core/lib/src/commands/flutter_command.dart @@ -14,15 +14,21 @@ class FlutterCommand extends ForwardCommand { @override Future run() async { - // TODO find pinned fvm flutter version + final args = argResults!.arguments; try { - final exitCode = flutterw(argResults!.arguments); - exit(exitCode); - } on FlutterWrapperNotFoundException catch (_) { - printerr( - 'Could not find a pinned flutter version associated with the project.\n' - 'Please install https://github.com/passsy/flutter_wrapper, to pin a flutter version', - ); + exitCode = flutter(args); + } on FlutterSdkNotSetException catch (original) { + // for backwards compatibility link to the previous required flutter_wrapper location + try { + // ignore: deprecated_member_use_from_same_package + exitCode = flutterw(args); + printerr("Sidekick Warning: ${original.message}"); + // success with flutterw, immediately return + return; + } on FlutterWrapperNotFoundException catch (_) { + // rethrow original below + } + rethrow /* original */; } } } diff --git a/sidekick_core/lib/src/dart.dart b/sidekick_core/lib/src/dart.dart index d9801627..327ca4cb 100644 --- a/sidekick_core/lib/src/dart.dart +++ b/sidekick_core/lib/src/dart.dart @@ -10,21 +10,52 @@ int dart( Directory? workingDirectory, dcli.Progress? progress, }) { - // TODO find pinned fvm flutter version - final binDir = repository.root.directory('.flutter/bin/cache/dart-sdk/bin/'); + bool flutterwLegacyMode = false; + + Directory? sdk = dartSdk; + if (sdk == null) { + if (flutterSdk != null) { + final embeddedSdk = flutterSdk!.directory('bin/cache/dart-sdk'); + if (!embeddedSdk.existsSync()) { + // Flutter SDK is not fully initialized, the Dart SDK not yet downloaded + // Execute flutter_tool to download the embedded dart runtime + flutter([], workingDirectory: workingDirectory); + } + if (embeddedSdk.existsSync()) { + sdk = embeddedSdk; + } + } + if (sdk == null) { + final flutterWrapperLocation = findFlutterwLocation(); + if (flutterWrapperLocation != null) { + // flutter_wrapper is installed, going into legacy mode for those which have not set the flutterSdkPath + final embeddedSdk = + repository.root.directory('.flutter/bin/cache/dart-sdk'); + if (!embeddedSdk.existsSync()) { + // Flutter SDK is not fully initialized, the Dart SDK not yet downloaded + // Execute flutter_tool to download the embedded dart runtime + // ignore: deprecated_member_use_from_same_package + flutterw([], workingDirectory: workingDirectory); + } + if (embeddedSdk.existsSync()) { + sdk = embeddedSdk; + flutterwLegacyMode = true; + } + } + } + } + if (sdk == null) { + throw DartSdkNotSetException(); + } + final dart = () { if (Platform.isWindows) { - return binDir.file('dart.exe'); + return sdk!.file('bin/dart.exe'); } else { - return binDir.file('dart'); + return sdk!.file('bin/dart'); } }(); - if (!dart.existsSync()) { - // run a flutterw command forcing flutter_tool to download the dart sdk - print("running flutterw to download dart"); - flutterw([], workingDirectory: mainProject!.root); - } final process = dcli.startFromArgs( dart.path, args, @@ -33,5 +64,19 @@ int dart( nothrow: true, terminal: progress == null, ); + + if (flutterwLegacyMode) { + printerr("Sidekick Warning: ${DartSdkNotSetException().message}"); + } return process.exitCode ?? -1; } + +/// The Dart SDK path is not set in [initializeSidekick] (param [dartSdk], neither is is the [flutterSdk]) +class DartSdkNotSetException implements Exception { + final String message = + "No Dart SDK set. Please set it in `initializeSidekick(dartSdkPath: 'path/to/sdk')`"; + @override + String toString() { + return "DartSdkNotSetException{message: $message}"; + } +} diff --git a/sidekick_core/lib/src/flutter.dart b/sidekick_core/lib/src/flutter.dart new file mode 100644 index 00000000..b4aa46ff --- /dev/null +++ b/sidekick_core/lib/src/flutter.dart @@ -0,0 +1,49 @@ +import 'package:dcli/dcli.dart' as dcli; +import 'package:sidekick_core/sidekick_core.dart'; + +/// Executes Flutter command from Flutter SDK set in [flutterSdk] +int flutter( + List args, { + Directory? workingDirectory, + dcli.Progress? progress, +}) { + final workingDir = + workingDirectory?.absolute ?? entryWorkingDirectory.absolute; + + final sdk = flutterSdk; + if (sdk == null) { + throw FlutterSdkNotSetException(); + } + + if (Platform.isWindows) { + final process = dcli.startFromArgs( + 'bash', + [sdk.file('bin/flutter.exe').path, ...args], + workingDirectory: workingDir.path, + nothrow: true, + progress: progress, + terminal: progress == null, + ); + return process.exitCode ?? -1; + } else { + final process = dcli.startFromArgs( + sdk.file('bin/flutter').path, + args, + workingDirectory: workingDir.path, + nothrow: true, + progress: progress, + terminal: progress == null, + ); + return process.exitCode ?? -1; + } +} + +/// The Flutter SDK path is not set in [initializeSidekick] (param [flutterSdk]) +class FlutterSdkNotSetException implements Exception { + final String message = + "No Flutter SDK set. Please set it in `initializeSidekick(flutterSdkPath: 'path/to/sdk')`"; + @override + String toString() { + return "FlutterSdkNotSetException{message: $message}"; + } +} diff --git a/sidekick_core/lib/src/flutterw.dart b/sidekick_core/lib/src/flutterw.dart index fed71015..ce0382a2 100644 --- a/sidekick_core/lib/src/flutterw.dart +++ b/sidekick_core/lib/src/flutterw.dart @@ -1,25 +1,29 @@ import 'package:dcli/dcli.dart' as dcli; import 'package:sidekick_core/sidekick_core.dart'; +/// Returns the closes `flutterw` file, searching upwards from [mainProject] or [Repository.cliPackageDir] or [repository] +File? findFlutterwLocation() { + final searchStart = + mainProject?.root ?? Repository.cliPackageDir ?? repository.root; + final flutterwParent = + searchStart.findParent((dir) => dir.file('flutterw').existsSync()); + final flutterw = flutterwParent?.file('flutterw'); + return flutterw; +} + /// Executes Flutter CLI (flutter_tool) via flutter_wrapper /// /// https://github.com/passsy/flutter_wrapper +@Deprecated('Use flutter() instead') int flutterw( List args, { Directory? workingDirectory, dcli.Progress? progress, }) { - // find closest flutterw - final searchStart = - mainProject?.root ?? Repository.cliPackageDir ?? repository.root; - final flutterwParent = - searchStart.findParent((dir) => dir.file('flutterw').existsSync()); - final flutterw = flutterwParent?.file('flutterw'); - - if (flutterw == null || !flutterw.existsSync()) { + final flutterw = findFlutterwLocation(); + if (flutterw == null) { throw FlutterWrapperNotFoundException(); } - final workingDir = workingDirectory?.absolute ?? entryWorkingDirectory.absolute; diff --git a/sidekick_core/test/dart_command_test.dart b/sidekick_core/test/dart_command_test.dart new file mode 100644 index 00000000..2d24a78f --- /dev/null +++ b/sidekick_core/test/dart_command_test.dart @@ -0,0 +1,54 @@ +import 'package:sidekick_core/sidekick_core.dart'; +import 'package:test/test.dart'; + +import 'fake_sdk.dart'; +import 'init_test.dart'; + +void main() { + test('dart command works when dartSdkPath is set', () async { + await insideFakeSidekickProject((dir) async { + final runner = initializeSidekick( + name: 'dash', + dartSdkPath: fakeDartSdk().path, + ); + runner.addCommand(DartCommand()); + await runner.run(['dart']); + }); + }); + + test('dart command fails when dartSdkPath is not set', () async { + await insideFakeSidekickProject((dir) async { + final runner = initializeSidekick( + name: 'dash', + // ignore: avoid_redundant_argument_values + dartSdkPath: null, + ); + runner.addCommand(DartCommand()); + expect( + () => runner.run(['dart']), + throwsA(isA()), + ); + }); + }); + + test('dart command links to embedded Dart SDK in Flutter SDK', () async { + await insideFakeSidekickProject((dir) async { + final runner = initializeSidekick( + name: 'dash', + flutterSdkPath: fakeFlutterSdk().path, + ); + runner.addCommand(DartCommand()); + await runner.run(['dart']); + }); + }); +} + +Future installFlutterWrapper(Directory directory) async { + writeAndRunShellScript( + r'sh -c "$(curl -fsSL https://raw.githubusercontent.com/passsy/flutter_wrapper/master/install.sh)"', + workingDirectory: directory, + ); + final exe = directory.file('flutterw'); + assert(exe.existsSync()); + return exe; +} diff --git a/sidekick_core/test/fake_sdk.dart b/sidekick_core/test/fake_sdk.dart new file mode 100644 index 00000000..8b2682da --- /dev/null +++ b/sidekick_core/test/fake_sdk.dart @@ -0,0 +1,38 @@ +import 'package:dcli/dcli.dart' as dcli; +import 'package:sidekick_core/sidekick_core.dart'; +import 'package:test/test.dart'; + +/// Creates a fake Flutter SDK in temp with a `flutter` executable that does +/// nothing besides "downloading" a fake Dart executable that does also nothing +Directory fakeFlutterSdk() { + final temp = Directory.systemTemp.createTempSync('fake_flutter'); + addTearDown(() => temp.deleteSync(recursive: true)); + + final flutterExe = temp.file('bin/flutter') + ..createSync(recursive: true) + ..writeAsStringSync(''' +#!/bin/bash +echo "fake Flutter executable" + +# Download dart SDK on execution +mkdir -p ${temp.absolute.path}/bin/cache/dart-sdk/bin +# write into file +printf "#!/bin/bash\\necho \\"fake embedded Dart executable\\"\\n" > ${temp.absolute.path}/bin/cache/dart-sdk/bin/dart +chmod 755 ${temp.absolute.path}/bin/cache/dart-sdk/bin/dart +'''); + dcli.run('chmod 755 ${flutterExe.path}'); + return temp; +} + +/// Creates a fake Dart SDK with a `dart` executable that does nothing +Directory fakeDartSdk() { + final temp = Directory.systemTemp.createTempSync('fake_dart'); + addTearDown(() => temp.deleteSync(recursive: true)); + + final exe = temp.file('bin/dart') + ..createSync(recursive: true) + ..writeAsStringSync('#!/bin/bash\necho "fake Dart executable"'); + dcli.run('chmod 755 ${exe.path}'); + + return temp; +} diff --git a/sidekick_core/test/flutter_command_test.dart b/sidekick_core/test/flutter_command_test.dart new file mode 100644 index 00000000..92ccc196 --- /dev/null +++ b/sidekick_core/test/flutter_command_test.dart @@ -0,0 +1,38 @@ +import 'package:sidekick_core/sidekick_core.dart'; +import 'package:test/test.dart'; + +import 'fake_sdk.dart'; +import 'init_test.dart'; + +void main() { + test( + 'flutter command works when flutterSdkPath is set', + () async { + await insideFakeSidekickProject((dir) async { + final runner = initializeSidekick( + name: 'dash', + flutterSdkPath: fakeFlutterSdk().path, + ); + runner.addCommand(FlutterCommand()); + await runner.run(['flutter']); + }); + }, + ); + + test('flutter command fails when flutterSdkPath is not set', () async { + await insideFakeSidekickProject((dir) async { + final runner = initializeSidekick( + name: 'dash', + // ignore: avoid_redundant_argument_values + flutterSdkPath: null, + ); + runner.addCommand(FlutterCommand()); + try { + await runner.run(['flutter']); + fail('did not throw'); + } catch (e) { + expect(e, isA()); + } + }); + }); +} diff --git a/sidekick_core/test/init_test.dart b/sidekick_core/test/init_test.dart index 1a897541..be920f22 100644 --- a/sidekick_core/test/init_test.dart +++ b/sidekick_core/test/init_test.dart @@ -175,8 +175,11 @@ R insideFakeSidekickProject(R Function(Directory projectDir) block) { ..writeAsStringSync('name: dash_sdk\n'); tempDir.directory('lib').createSync(); + env['SIDEKICK_PACKAGE_HOME'] = tempDir.absolute.path; + addTearDown(() { tempDir.deleteSync(recursive: true); + env['SIDEKICK_PACKAGE_HOME'] = null; }); return IOOverrides.runZoned(