feat: Download Shorebird-vended Flutter, reroute flutter commands to that instance of Flutter (#229)

This commit is contained in:
Bryan Oltman
2023-04-05 12:02:34 -04:00
committed by GitHub
parent e2b7965231
commit 6dc050c246
10 changed files with 213 additions and 145 deletions
+1 -12
View File
@@ -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
+15
View File
@@ -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
+3 -3
View File
@@ -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<int> {
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;
@@ -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<ProcessResult> Function(
String executable,
List<String> arguments, {
bool runInShell,
String? workingDirectory,
});
/// {@template shorebird_cli_command_runner}
/// A [CommandRunner] for the CLI.
///
@@ -30,9 +21,7 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner<int> {
/// {@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<int> {
void printUsage() => _logger.info(usage);
final Logger _logger;
final RunProcess _runProcess;
@override
Future<int> run(Iterable<String> args) async {
@@ -73,29 +61,6 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner<int> {
_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<String> _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;
}
}
@@ -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<ProcessResult> Function(
String executable,
List<String> arguments, {
bool runInShell,
});
/// {@template build_command}
///
/// `shorebird build`
@@ -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<Process> Function(
String executable,
List<String> arguments, {
bool runInShell,
});
/// {@template run_command}
/// `shorebird run`
/// Run the Flutter application.
@@ -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',
);
}
@@ -0,0 +1,67 @@
import 'dart:io';
import 'package:meta/meta.dart';
import 'package:shorebird_cli/src/shorebird_paths.dart';
typedef RunProcess = Future<ProcessResult> Function(
String executable,
List<String> arguments, {
bool runInShell,
String? workingDirectory,
});
typedef StartProcess = Future<Process> Function(
String executable,
List<String> arguments, {
bool runInShell,
});
/// A wrapper around [Process] that replaces executables to Shorebird-vended
/// versions.
abstract class ShorebirdProcess {
@visibleForTesting
static ProcessWrapper processWrapper = ProcessWrapper();
static Future<ProcessResult> run(
String executable,
List<String> arguments, {
bool runInShell = false,
String? workingDirectory,
}) {
return processWrapper.run(
_resolveExecutable(executable),
arguments,
runInShell: runInShell,
workingDirectory: workingDirectory,
);
}
static Future<Process> start(
String executable,
List<String> 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
@@ -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',
@@ -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);
});
});
});
}