feat: Download Shorebird-vended Flutter, reroute flutter commands to that instance of Flutter (#229)
This commit is contained in:
+1
-12
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user