From 6dc050c2468a08e874a9411cb100cd9d0b409ec1 Mon Sep 17 00:00:00 2001 From: Bryan Oltman Date: Wed, 5 Apr 2023 12:02:34 -0400 Subject: [PATCH] feat: Download Shorebird-vended Flutter, reroute flutter commands to that instance of Flutter (#229) --- TRUSTED_TESTERS.md | 13 +-- bin/internal/shared.sh | 15 +++ packages/shorebird_cli/lib/src/command.dart | 6 +- .../shorebird_cli/lib/src/command_runner.dart | 52 --------- .../lib/src/commands/build_command.dart | 6 - .../lib/src/commands/run_command.dart | 7 -- .../lib/src/shorebird_paths.dart | 21 ++++ .../lib/src/shorebird_process.dart | 67 +++++++++++ .../test/src/command_runner_test.dart | 66 +---------- .../test/src/shorebird_process_test.dart | 105 ++++++++++++++++++ 10 files changed, 213 insertions(+), 145 deletions(-) create mode 100644 packages/shorebird_cli/lib/src/shorebird_paths.dart create mode 100644 packages/shorebird_cli/lib/src/shorebird_process.dart create mode 100644 packages/shorebird_cli/test/src/shorebird_process_test.dart diff --git a/TRUSTED_TESTERS.md b/TRUSTED_TESTERS.md index e486e145..95372a53 100644 --- a/TRUSTED_TESTERS.md +++ b/TRUSTED_TESTERS.md @@ -74,18 +74,7 @@ No support for: ## Installing Shorebird command line -These instructions assume you already have Flutter installed on the machine -and `flutter` and `dart` in your path: -https://docs.flutter.dev/get-started/install - -Shorebird also currently only works with the latest Flutter stable version: - -``` -flutter channel stable -flutter upgrade -``` - -Once you have Flutter installed, the next is to install the `shorebird` command-line tool. +Install the `shorebird` command-line tool by running the following command: ```bash curl --proto '=https' --tlsv1.2 https://raw.githubusercontent.com/shorebirdtech/install/main/install.sh -sSf | sh diff --git a/bin/internal/shared.sh b/bin/internal/shared.sh index a9de6345..bffaac53 100755 --- a/bin/internal/shared.sh +++ b/bin/internal/shared.sh @@ -8,6 +8,17 @@ set -e # Needed because if it is set, cd may print the path it changed to. unset CDPATH +# Either clones or pulls the Shorebird Flutter repository, depending on whether FLUTTER_PATH exists. +function update_flutter { + # TODO(bryanoltman): add doctor check that we're on stable + # TODO(bryanoltman): add doctor check for modified files + if [[ -d "$FLUTTER_PATH" ]]; then + git --git-dir="$FLUTTER_PATH/.git" pull + else + git clone --filter=tree:0 https://github.com/shorebirdtech/flutter.git -b stable "$FLUTTER_PATH" + fi +} + function pub_upgrade_with_retry { local total_tries="10" local remaining_tries=$((total_tries - 1)) @@ -126,6 +137,9 @@ function upgrade_shorebird () ( exit $? fi + >&2 echo Updating Flutter... + update_flutter + >&2 echo Building Shorebird... # Prepare packages... @@ -170,6 +184,7 @@ function shared::execute() { SNAPSHOT_PATH="$SHOREBIRD_ROOT/bin/cache/shorebird.snapshot" STAMP_PATH="$SHOREBIRD_ROOT/bin/cache/shorebird.stamp" SCRIPT_PATH="$SHOREBIRD_CLI_DIR/bin/shorebird.dart" + FLUTTER_PATH="$SHOREBIRD_ROOT/bin/cache/flutter" # Test if running as superuser – but don't warn if running within Docker or CI. if [[ "$EUID" == "0" && ! -f /.dockerenv && "$CI" != "true" && "$BOT" != "true" && "$CONTINUOUS_INTEGRATION" != "true" ]]; then diff --git a/packages/shorebird_cli/lib/src/command.dart b/packages/shorebird_cli/lib/src/command.dart index f396e33d..ee96954a 100644 --- a/packages/shorebird_cli/lib/src/command.dart +++ b/packages/shorebird_cli/lib/src/command.dart @@ -6,7 +6,7 @@ import 'package:http/http.dart' as http; import 'package:mason_logger/mason_logger.dart'; import 'package:meta/meta.dart'; import 'package:shorebird_cli/src/auth/auth.dart'; -import 'package:shorebird_cli/src/command_runner.dart'; +import 'package:shorebird_cli/src/shorebird_process.dart'; import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; /// Signature for a function which takes a list of bytes and returns a hash. @@ -32,8 +32,8 @@ abstract class ShorebirdCommand extends Command { StartProcess? startProcess, }) : auth = auth ?? Auth(), buildCodePushClient = buildCodePushClient ?? CodePushClient.new, - runProcess = runProcess ?? Process.run, - startProcess = startProcess ?? Process.start; + runProcess = runProcess ?? ShorebirdProcess.run, + startProcess = startProcess ?? ShorebirdProcess.start; final Auth auth; final CodePushClientBuilder buildCodePushClient; diff --git a/packages/shorebird_cli/lib/src/command_runner.dart b/packages/shorebird_cli/lib/src/command_runner.dart index 4f2a986f..25671a30 100644 --- a/packages/shorebird_cli/lib/src/command_runner.dart +++ b/packages/shorebird_cli/lib/src/command_runner.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:cli_completion/cli_completion.dart'; @@ -12,13 +10,6 @@ const executableName = 'shorebird'; const packageName = 'shorebird_cli'; const description = 'The shorebird command-line tool'; -typedef RunProcess = Future Function( - String executable, - List arguments, { - bool runInShell, - String? workingDirectory, -}); - /// {@template shorebird_cli_command_runner} /// A [CommandRunner] for the CLI. /// @@ -30,9 +21,7 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner { /// {@macro shorebird_cli_command_runner} ShorebirdCliCommandRunner({ Logger? logger, - RunProcess? runProcess, }) : _logger = logger ?? Logger(), - _runProcess = runProcess ?? Process.run, super(executableName, description) { argParser ..addFlag( @@ -63,7 +52,6 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner { void printUsage() => _logger.info(usage); final Logger _logger; - final RunProcess _runProcess; @override Future run(Iterable args) async { @@ -73,29 +61,6 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner { _logger.level = Level.verbose; } - try { - final flutterEngineRevision = await _getFlutterEngineRevision(); - if (flutterEngineRevision != requiredFlutterEngineRevision) { - _logger.err( - ''' -Shorebird only works with the latest stable channel at this time. - -To use the latest stable channel, run: - flutter channel stable - flutter upgrade - -If you believe you're already on the latest stable channel, please ask on Discord, we're happy to help! - -Required engine revision: "$requiredFlutterEngineRevision" -Detected engine revision: "$flutterEngineRevision"''', - ); - return ExitCode.software.code; - } - } catch (error) { - _logger.err('Failed to get Flutter engine revision.\n$error'); - return ExitCode.software.code; - } - return await runCommand(topLevelResults) ?? ExitCode.success.code; } on FormatException catch (e, stackTrace) { // On format errors, show the commands error message, root usage and @@ -141,21 +106,4 @@ Shorebird Engine • revision $shorebirdEngineRevision''', return exitCode; } - - Future _getFlutterEngineRevision() async { - final result = await _runProcess( - 'flutter', - ['--version'], - runInShell: true, - ); - if (result.exitCode != 0) throw Exception('${result.stderr}'); - - final output = result.stdout as String; - final regexp = RegExp(r'Engine • revision (.*?$)', multiLine: true); - final flutterEngineRevision = regexp.firstMatch(output)?.group(1); - if (flutterEngineRevision == null) { - throw Exception('Unable to determine the Flutter engine revision.'); - } - return flutterEngineRevision; - } } diff --git a/packages/shorebird_cli/lib/src/commands/build_command.dart b/packages/shorebird_cli/lib/src/commands/build_command.dart index 543dc9e5..4d768796 100644 --- a/packages/shorebird_cli/lib/src/commands/build_command.dart +++ b/packages/shorebird_cli/lib/src/commands/build_command.dart @@ -6,12 +6,6 @@ import 'package:shorebird_cli/src/shorebird_build_mixin.dart'; import 'package:shorebird_cli/src/shorebird_config_mixin.dart'; import 'package:shorebird_cli/src/shorebird_engine_mixin.dart'; -typedef RunProcess = Future Function( - String executable, - List arguments, { - bool runInShell, -}); - /// {@template build_command} /// /// `shorebird build` diff --git a/packages/shorebird_cli/lib/src/commands/run_command.dart b/packages/shorebird_cli/lib/src/commands/run_command.dart index c64ca94d..562a4259 100644 --- a/packages/shorebird_cli/lib/src/commands/run_command.dart +++ b/packages/shorebird_cli/lib/src/commands/run_command.dart @@ -1,17 +1,10 @@ import 'dart:convert'; -import 'dart:io'; import 'package:mason_logger/mason_logger.dart'; import 'package:shorebird_cli/src/command.dart'; import 'package:shorebird_cli/src/shorebird_config_mixin.dart'; import 'package:shorebird_cli/src/shorebird_engine_mixin.dart'; -typedef StartProcess = Future Function( - String executable, - List arguments, { - bool runInShell, -}); - /// {@template run_command} /// `shorebird run` /// Run the Flutter application. diff --git a/packages/shorebird_cli/lib/src/shorebird_paths.dart b/packages/shorebird_cli/lib/src/shorebird_paths.dart new file mode 100644 index 00000000..db71ba05 --- /dev/null +++ b/packages/shorebird_cli/lib/src/shorebird_paths.dart @@ -0,0 +1,21 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; + +abstract class ShorebirdPaths { + /// The root directory of the Shorebird install. + /// + /// Assumes we are running from $ROOT/bin/cache. + static Directory shorebirdRoot = + File(Platform.script.toFilePath()).parent.parent.parent; + + /// Path to the Shorebird-vended Flutter binary. + static String get flutterBinaryPath => p.join( + shorebirdRoot.path, + 'bin', + 'cache', + 'flutter', + 'bin', + 'flutter', + ); +} diff --git a/packages/shorebird_cli/lib/src/shorebird_process.dart b/packages/shorebird_cli/lib/src/shorebird_process.dart new file mode 100644 index 00000000..386ee497 --- /dev/null +++ b/packages/shorebird_cli/lib/src/shorebird_process.dart @@ -0,0 +1,67 @@ +import 'dart:io'; + +import 'package:meta/meta.dart'; +import 'package:shorebird_cli/src/shorebird_paths.dart'; + +typedef RunProcess = Future Function( + String executable, + List arguments, { + bool runInShell, + String? workingDirectory, +}); + +typedef StartProcess = Future Function( + String executable, + List arguments, { + bool runInShell, +}); + +/// A wrapper around [Process] that replaces executables to Shorebird-vended +/// versions. +abstract class ShorebirdProcess { + @visibleForTesting + static ProcessWrapper processWrapper = ProcessWrapper(); + + static Future run( + String executable, + List arguments, { + bool runInShell = false, + String? workingDirectory, + }) { + return processWrapper.run( + _resolveExecutable(executable), + arguments, + runInShell: runInShell, + workingDirectory: workingDirectory, + ); + } + + static Future start( + String executable, + List argument, { + bool runInShell = false, + }) { + return processWrapper.start( + _resolveExecutable(executable), + argument, + runInShell: runInShell, + ); + } + + static String _resolveExecutable(String executable) { + if (executable == 'flutter') { + return ShorebirdPaths.flutterBinaryPath; + } + + return executable; + } +} + +// coverage:ignore-start +@visibleForTesting +class ProcessWrapper { + RunProcess get run => Process.run; + + StartProcess get start => Process.start; +} +// coverage:ignore-end diff --git a/packages/shorebird_cli/test/src/command_runner_test.dart b/packages/shorebird_cli/test/src/command_runner_test.dart index 88d89cd6..0084e8c7 100644 --- a/packages/shorebird_cli/test/src/command_runner_test.dart +++ b/packages/shorebird_cli/test/src/command_runner_test.dart @@ -28,71 +28,7 @@ void main() { 'Engine • revision $requiredFlutterEngineRevision', ); - commandRunner = ShorebirdCliCommandRunner( - logger: logger, - runProcess: ( - executable, - arguments, { - bool runInShell = false, - String? workingDirectory, - }) async { - return processResult; - }, - ); - }); - - test('exits when Flutter is not installed', () async { - const error = 'oops something went wrong'; - when(() => processResult.exitCode).thenReturn(1); - when(() => processResult.stderr).thenReturn(error); - - final result = await commandRunner.run(['--version']); - expect(result, equals(ExitCode.software.code)); - verify(() => logger.err(any(that: contains(error)))).called(1); - }); - - test('exits when unable to detect the Flutter engine revision', () async { - when(() => processResult.exitCode).thenReturn(0); - when(() => processResult.stdout).thenReturn( - ''' -Flutter 3.7.7 • channel stable • -Framework • revision 2ad6cd72c0 (12 days ago) • 2023-03-08 09:41:59 -0800 -Tools • Dart 2.19.4 • DevTools 2.20.1 -''', - ); - - final result = await commandRunner.run(['--version']); - expect(result, equals(ExitCode.software.code)); - verify( - () => logger.err( - any( - that: contains('Unable to determine the Flutter engine revision.'), - ), - ), - ).called(1); - }); - - test('exits when there is an incompatible Flutter engine', () async { - when(() => processResult.stdout).thenReturn( - ''' -Flutter 3.7.7 • channel stable • -Framework • revision 2ad6cd72c0 (12 days ago) • 2023-03-08 09:41:59 -0800 -Engine • revision 639e313f99 -Tools • Dart 2.19.4 • DevTools 2.20.1 -''', - ); - - final result = await commandRunner.run(['--version']); - expect(result, equals(ExitCode.software.code)); - verify( - () => logger.err( - any( - that: contains( - '''Shorebird only works with the latest stable channel at this time.''', - ), - ), - ), - ).called(1); + commandRunner = ShorebirdCliCommandRunner(logger: logger); }); test('can be instantiated without an explicit analytics/logger instance', diff --git a/packages/shorebird_cli/test/src/shorebird_process_test.dart b/packages/shorebird_cli/test/src/shorebird_process_test.dart new file mode 100644 index 00000000..faaeb19b --- /dev/null +++ b/packages/shorebird_cli/test/src/shorebird_process_test.dart @@ -0,0 +1,105 @@ +import 'dart:io'; + +import 'package:mocktail/mocktail.dart'; +import 'package:shorebird_cli/src/shorebird_process.dart'; +import 'package:test/test.dart'; + +class _MockProcess extends Mock implements Process {} + +class _MockProcessResult extends Mock implements ProcessResult {} + +class _MockProcessWrapper extends Mock implements ProcessWrapper {} + +void main() { + group('ShorebirdProcess', () { + late ProcessWrapper processWrapper; + late Process startProcess; + late ProcessResult runProcessResult; + + setUp(() { + processWrapper = _MockProcessWrapper(); + runProcessResult = _MockProcessResult(); + startProcess = _MockProcess(); + + ShorebirdProcess.processWrapper = processWrapper; + + when(() => processWrapper.run).thenReturn( + ( + executable, + arguments, { + bool runInShell = false, + String? workingDirectory, + }) async { + return runProcessResult; + }, + ); + + when(() => processWrapper.start).thenReturn( + ( + executable, + arguments, { + bool runInShell = false, + }) async { + return startProcess; + }, + ); + }); + + group('run', () { + test('forwards non-flutter executables to Process.run', () async { + await ShorebirdProcess.run( + 'git', + ['pull'], + runInShell: true, + workingDirectory: '~', + ); + + verify( + () => processWrapper.run( + 'git', + ['pull'], + runInShell: true, + workingDirectory: '~', + ), + ).called(1); + }); + + test('replaces "flutter" with our local flutter', () async { + await ShorebirdProcess.run( + 'flutter', + ['--version'], + runInShell: true, + workingDirectory: '~', + ); + + verify( + () => processWrapper.run( + 'flutter/bin/flutter', + ['--version'], + runInShell: true, + workingDirectory: '~', + ), + ).called(1); + }); + }); + + group('start', () { + test('forwards non-flutter executables to Process.run', () async { + await ShorebirdProcess.start('git', ['pull'], runInShell: true); + + verify(() => processWrapper.start('git', ['pull'], runInShell: true)) + .called(1); + }); + + test('replaces "flutter" with our local flutter', () async { + await ShorebirdProcess.start('flutter', ['run'], runInShell: true); + + verify(() => processWrapper.start( + 'flutter/bin/flutter', + ['run'], + runInShell: true, + )).called(1); + }); + }); + }); +}