feat(shorebird_cli): Add OS wrapper for which/where.exe (#1403)
This commit is contained in:
@@ -11,6 +11,7 @@ import 'package:shorebird_cli/src/command_runner.dart';
|
||||
import 'package:shorebird_cli/src/doctor.dart';
|
||||
import 'package:shorebird_cli/src/executables/executables.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/os/os.dart';
|
||||
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
||||
import 'package:shorebird_cli/src/platform.dart';
|
||||
import 'package:shorebird_cli/src/process.dart';
|
||||
@@ -40,6 +41,7 @@ Future<void> main(List<String> args) async {
|
||||
iosDeployRef,
|
||||
javaRef,
|
||||
loggerRef,
|
||||
osInterfaceRef,
|
||||
patchDiffCheckerRef,
|
||||
platformRef,
|
||||
processRef,
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:scoped/scoped.dart';
|
||||
import 'package:shorebird_cli/src/platform.dart';
|
||||
import 'package:shorebird_cli/src/process.dart';
|
||||
|
||||
// TODO(bryanoltman): remove this once os is used.
|
||||
// coverage:ignore-start
|
||||
/// A reference to a [OperatingSystemInterface] instance.
|
||||
final osInterfaceRef = create(OperatingSystemInterface.new);
|
||||
|
||||
/// The [OperatingSystemInterface] instance available in the current zone.
|
||||
OperatingSystemInterface get osInterface => read(osInterfaceRef);
|
||||
// coverage:ignore-end
|
||||
|
||||
/// {@template operating_system_interface}
|
||||
/// A wrapper around operating system specific functionality.
|
||||
/// {@endtemplate}
|
||||
abstract class OperatingSystemInterface {
|
||||
/// {@macro operating_system_interface}
|
||||
factory OperatingSystemInterface() {
|
||||
if (platform.isWindows) {
|
||||
return _WindowsOperatingSystemInterface();
|
||||
} else if (platform.isMacOS || platform.isLinux) {
|
||||
return _PosixOperatingSystemInterface();
|
||||
}
|
||||
|
||||
throw UnsupportedError(
|
||||
'Unsupported operating system: ${Platform.operatingSystem}',
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the first instance of [executableName] found on the PATH.
|
||||
///
|
||||
/// This is the equivalent of the `which` command on Linux and macOS and
|
||||
/// `where.exe` on Windows.
|
||||
String? which(String executableName);
|
||||
}
|
||||
|
||||
class _PosixOperatingSystemInterface implements OperatingSystemInterface {
|
||||
@override
|
||||
String? which(String executableName) {
|
||||
final result = process.runSync('which', [executableName]);
|
||||
if (result.exitCode != ExitCode.success.code) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.stdout as String?;
|
||||
}
|
||||
}
|
||||
|
||||
class _WindowsOperatingSystemInterface implements OperatingSystemInterface {
|
||||
@override
|
||||
String? which(String executableName) {
|
||||
final result = process.runSync('where.exe', [executableName]);
|
||||
if (result.exitCode != ExitCode.success.code) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// By default, where.exe will list all matching executables on PATH. We want
|
||||
// to return the first one.
|
||||
return (result.stdout as String).split('\n').firstOrNull;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export 'operating_system_interface.dart';
|
||||
@@ -55,14 +55,11 @@ class ShorebirdProcess {
|
||||
String? workingDirectory,
|
||||
bool useVendedFlutter = true,
|
||||
}) {
|
||||
final resolvedEnvironment = environment ?? {};
|
||||
if (useVendedFlutter) {
|
||||
// Note: this will overwrite existing environment values.
|
||||
resolvedEnvironment.addAll(
|
||||
_environmentOverrides(executable: executable),
|
||||
);
|
||||
}
|
||||
|
||||
final resolvedEnvironment = _resolveEnvironment(
|
||||
environment,
|
||||
executable: executable,
|
||||
useVendedFlutter: useVendedFlutter,
|
||||
);
|
||||
final resolvedExecutable =
|
||||
useVendedFlutter ? _resolveExecutable(executable) : executable;
|
||||
final resolvedArguments =
|
||||
@@ -80,6 +77,36 @@ class ShorebirdProcess {
|
||||
);
|
||||
}
|
||||
|
||||
ShorebirdProcessResult runSync(
|
||||
String executable,
|
||||
List<String> arguments, {
|
||||
bool runInShell = false,
|
||||
Map<String, String>? environment,
|
||||
String? workingDirectory,
|
||||
bool useVendedFlutter = true,
|
||||
}) {
|
||||
final resolvedEnvironment = _resolveEnvironment(
|
||||
environment,
|
||||
executable: executable,
|
||||
useVendedFlutter: useVendedFlutter,
|
||||
);
|
||||
final resolvedExecutable =
|
||||
useVendedFlutter ? _resolveExecutable(executable) : executable;
|
||||
final resolvedArguments =
|
||||
useVendedFlutter ? _resolveArguments(executable, arguments) : arguments;
|
||||
logger.detail(
|
||||
'''[Process.runSync] $resolvedExecutable ${resolvedArguments.join(' ')}${workingDirectory == null ? '' : ' (in $workingDirectory)'}''',
|
||||
);
|
||||
|
||||
return processWrapper.runSync(
|
||||
resolvedExecutable,
|
||||
resolvedArguments,
|
||||
runInShell: runInShell,
|
||||
workingDirectory: workingDirectory,
|
||||
environment: resolvedEnvironment,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Process> start(
|
||||
String executable,
|
||||
List<String> arguments, {
|
||||
@@ -110,6 +137,22 @@ class ShorebirdProcess {
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, String> _resolveEnvironment(
|
||||
Map<String, String>? baseEnvironment, {
|
||||
required String executable,
|
||||
required bool useVendedFlutter,
|
||||
}) {
|
||||
final resolvedEnvironment = baseEnvironment ?? {};
|
||||
if (useVendedFlutter) {
|
||||
// Note: this will overwrite existing environment values.
|
||||
resolvedEnvironment.addAll(
|
||||
_environmentOverrides(executable: executable),
|
||||
);
|
||||
}
|
||||
|
||||
return resolvedEnvironment;
|
||||
}
|
||||
|
||||
String _resolveExecutable(String executable) {
|
||||
if (executable == 'flutter') return shorebirdEnv.flutterBinaryFile.path;
|
||||
return executable;
|
||||
@@ -178,6 +221,27 @@ class ProcessWrapper {
|
||||
);
|
||||
}
|
||||
|
||||
ShorebirdProcessResult runSync(
|
||||
String executable,
|
||||
List<String> arguments, {
|
||||
bool runInShell = false,
|
||||
Map<String, String>? environment,
|
||||
String? workingDirectory,
|
||||
}) {
|
||||
final result = Process.runSync(
|
||||
executable,
|
||||
arguments,
|
||||
environment: environment,
|
||||
runInShell: runInShell,
|
||||
workingDirectory: workingDirectory,
|
||||
);
|
||||
return ShorebirdProcessResult(
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Process> start(
|
||||
String executable,
|
||||
List<String> arguments, {
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:scoped/scoped.dart';
|
||||
import 'package:shorebird_cli/src/os/os.dart';
|
||||
import 'package:shorebird_cli/src/platform.dart';
|
||||
import 'package:shorebird_cli/src/process.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../mocks.dart';
|
||||
|
||||
void main() {
|
||||
group(OperatingSystemInterface, () {
|
||||
late Platform platform;
|
||||
late ShorebirdProcess process;
|
||||
late ShorebirdProcessResult processResult;
|
||||
late OperatingSystemInterface osInterface;
|
||||
|
||||
R runWithOverrides<R>(R Function() body) {
|
||||
return runScoped(
|
||||
() => body(),
|
||||
values: {
|
||||
platformRef.overrideWith(() => platform),
|
||||
processRef.overrideWith(() => process),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
platform = MockPlatform();
|
||||
process = MockShorebirdProcess();
|
||||
processResult = MockProcessResult();
|
||||
|
||||
when(() => platform.isLinux).thenReturn(false);
|
||||
when(() => platform.isMacOS).thenReturn(false);
|
||||
when(() => platform.isWindows).thenReturn(false);
|
||||
|
||||
when(() => process.runSync(any(), any())).thenReturn(processResult);
|
||||
when(() => processResult.exitCode).thenReturn(ExitCode.success.code);
|
||||
});
|
||||
|
||||
group('init', () {
|
||||
test('throws UnsupportedError when operating system is not supported',
|
||||
() {
|
||||
expect(
|
||||
() => runWithOverrides(OperatingSystemInterface.new),
|
||||
throwsUnsupportedError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('on macOS/Linux', () {
|
||||
setUp(() {
|
||||
when(() => platform.isMacOS).thenReturn(true);
|
||||
|
||||
osInterface = runWithOverrides(OperatingSystemInterface.new);
|
||||
});
|
||||
|
||||
group('which()', () {
|
||||
group('when no executable is found on PATH', () {
|
||||
setUp(() {
|
||||
when(() => processResult.exitCode).thenReturn(1);
|
||||
});
|
||||
|
||||
test('returns null', () {
|
||||
expect(
|
||||
runWithOverrides(() => osInterface.which('shorebird')),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when executable is found on PATH', () {
|
||||
const shorebirdPath = '/path/to/shorebird';
|
||||
setUp(() {
|
||||
when(() => processResult.stdout).thenReturn(shorebirdPath);
|
||||
});
|
||||
|
||||
test('returns path to executable', () {
|
||||
expect(
|
||||
runWithOverrides(() => osInterface.which('shorebird')),
|
||||
shorebirdPath,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('on Windows', () {
|
||||
setUp(() {
|
||||
when(() => platform.isWindows).thenReturn(true);
|
||||
osInterface = runWithOverrides(OperatingSystemInterface.new);
|
||||
});
|
||||
|
||||
group('which()', () {
|
||||
group('when no executable is found on PATH', () {
|
||||
setUp(() {
|
||||
when(() => processResult.exitCode).thenReturn(1);
|
||||
});
|
||||
|
||||
test('returns null', () {
|
||||
expect(
|
||||
runWithOverrides(() => osInterface.which('shorebird')), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('when executable is found on PATH', () {
|
||||
const shorebirdPath = r'C:\path\to\shorebird';
|
||||
setUp(() {
|
||||
when(() => processResult.stdout).thenReturn(shorebirdPath);
|
||||
});
|
||||
|
||||
test('returns path to executable', () {
|
||||
expect(
|
||||
runWithOverrides(() => osInterface.which('shorebird')),
|
||||
shorebirdPath,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when multiple executables are found on PATH', () {
|
||||
const shorebirdPath = r'C:\path\to\shorebird';
|
||||
const shorebirdPaths = r'''
|
||||
C:\path\to\shorebird
|
||||
C:\path\to\shorebird1
|
||||
C:\path\to\shorebird2
|
||||
C:\path\to\shorebird3''';
|
||||
|
||||
setUp(() {
|
||||
when(() => processResult.stdout).thenReturn(shorebirdPaths);
|
||||
});
|
||||
|
||||
test('returns first path to executable', () {
|
||||
expect(
|
||||
runWithOverrides(() => osInterface.which('shorebird')),
|
||||
shorebirdPath,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -42,24 +42,6 @@ void main() {
|
||||
when(
|
||||
() => shorebirdEnv.flutterBinaryFile,
|
||||
).thenReturn(File(p.join('bin', 'cache', 'flutter', 'bin', 'flutter')));
|
||||
when(
|
||||
() => processWrapper.run(
|
||||
any(),
|
||||
any(),
|
||||
runInShell: any(named: 'runInShell'),
|
||||
environment: any(named: 'environment'),
|
||||
workingDirectory: any(named: 'workingDirectory'),
|
||||
),
|
||||
).thenAnswer((_) async => runProcessResult);
|
||||
|
||||
when(
|
||||
() => processWrapper.start(
|
||||
any(),
|
||||
any(),
|
||||
environment: any(named: 'environment'),
|
||||
runInShell: any(named: 'runInShell'),
|
||||
),
|
||||
).thenAnswer((_) async => startProcess);
|
||||
});
|
||||
|
||||
test('ShorebirdProcessResult can be instantiated as a const', () {
|
||||
@@ -70,6 +52,18 @@ void main() {
|
||||
});
|
||||
|
||||
group('run', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => processWrapper.run(
|
||||
any(),
|
||||
any(),
|
||||
runInShell: any(named: 'runInShell'),
|
||||
environment: any(named: 'environment'),
|
||||
workingDirectory: any(named: 'workingDirectory'),
|
||||
),
|
||||
).thenAnswer((_) async => runProcessResult);
|
||||
});
|
||||
|
||||
test('forwards non-flutter executables to Process.run', () async {
|
||||
await shorebirdProcess.run(
|
||||
'git',
|
||||
@@ -180,35 +174,171 @@ void main() {
|
||||
).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
test('adds local-engine arguments if set', () async {
|
||||
final localEngineSrcPath = p.join('path', 'to', 'engine', 'src');
|
||||
shorebirdProcess = ShorebirdProcess(
|
||||
processWrapper: processWrapper,
|
||||
engineConfig: EngineConfig(
|
||||
localEngineSrcPath: localEngineSrcPath,
|
||||
localEngine: 'android_release_arm64',
|
||||
),
|
||||
);
|
||||
|
||||
await runWithOverrides(() => shorebirdProcess.run('flutter', []));
|
||||
|
||||
verify(
|
||||
() => processWrapper.run(
|
||||
any(),
|
||||
[
|
||||
'--local-engine-src-path=$localEngineSrcPath',
|
||||
'--local-engine=android_release_arm64',
|
||||
],
|
||||
runInShell: any(named: 'runInShell'),
|
||||
environment: any(named: 'environment'),
|
||||
workingDirectory: any(named: 'workingDirectory'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('adds local-engine arguments if set', () async {
|
||||
final localEngineSrcPath = p.join('path', 'to', 'engine', 'src');
|
||||
shorebirdProcess = ShorebirdProcess(
|
||||
processWrapper: processWrapper,
|
||||
engineConfig: EngineConfig(
|
||||
localEngineSrcPath: localEngineSrcPath,
|
||||
localEngine: 'android_release_arm64',
|
||||
),
|
||||
group('runSync', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => processWrapper.runSync(
|
||||
any(),
|
||||
any(),
|
||||
runInShell: any(named: 'runInShell'),
|
||||
environment: any(named: 'environment'),
|
||||
workingDirectory: any(named: 'workingDirectory'),
|
||||
),
|
||||
).thenReturn(runProcessResult);
|
||||
});
|
||||
|
||||
test('forwards non-flutter executables to Process.runSync', () async {
|
||||
shorebirdProcess.runSync(
|
||||
'git',
|
||||
['pull'],
|
||||
runInShell: true,
|
||||
workingDirectory: '~',
|
||||
);
|
||||
|
||||
verify(
|
||||
() => processWrapper.runSync(
|
||||
'git',
|
||||
['pull'],
|
||||
runInShell: true,
|
||||
environment: {},
|
||||
workingDirectory: '~',
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('replaces "flutter" with our local flutter', () {
|
||||
runWithOverrides(
|
||||
() => shorebirdProcess.runSync(
|
||||
'flutter',
|
||||
['--version'],
|
||||
runInShell: true,
|
||||
workingDirectory: '~',
|
||||
),
|
||||
);
|
||||
|
||||
verify(
|
||||
() => processWrapper.runSync(
|
||||
any(
|
||||
that: contains(
|
||||
p.join('bin', 'cache', 'flutter', 'bin', 'flutter'),
|
||||
),
|
||||
),
|
||||
['--version'],
|
||||
runInShell: true,
|
||||
environment: flutterStorageBaseUrlEnv,
|
||||
workingDirectory: '~',
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test(
|
||||
'''does not replace flutter with our local flutter if useVendedFlutter is false''',
|
||||
() {
|
||||
shorebirdProcess.runSync(
|
||||
'flutter',
|
||||
['--version'],
|
||||
runInShell: true,
|
||||
workingDirectory: '~',
|
||||
useVendedFlutter: false,
|
||||
);
|
||||
|
||||
verify(
|
||||
() => processWrapper.runSync(
|
||||
'flutter',
|
||||
['--version'],
|
||||
runInShell: true,
|
||||
environment: {},
|
||||
workingDirectory: '~',
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('Updates environment if useVendedFlutter is true', () {
|
||||
shorebirdProcess.runSync(
|
||||
'flutter',
|
||||
['--version'],
|
||||
runInShell: true,
|
||||
workingDirectory: '~',
|
||||
useVendedFlutter: false,
|
||||
environment: {'ENV_VAR': 'asdfasdf'},
|
||||
);
|
||||
|
||||
verify(
|
||||
() => processWrapper.runSync(
|
||||
'flutter',
|
||||
['--version'],
|
||||
runInShell: true,
|
||||
workingDirectory: '~',
|
||||
environment: {'ENV_VAR': 'asdfasdf'},
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test(
|
||||
'Makes no changes to environment if useVendedFlutter is false',
|
||||
() {
|
||||
shorebirdProcess.runSync(
|
||||
'flutter',
|
||||
['--version'],
|
||||
runInShell: true,
|
||||
workingDirectory: '~',
|
||||
useVendedFlutter: false,
|
||||
environment: {'ENV_VAR': 'asdfasdf'},
|
||||
);
|
||||
|
||||
verify(
|
||||
() => processWrapper.runSync(
|
||||
'flutter',
|
||||
['--version'],
|
||||
runInShell: true,
|
||||
workingDirectory: '~',
|
||||
environment: {'ENV_VAR': 'asdfasdf'},
|
||||
),
|
||||
).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
await runWithOverrides(() => shorebirdProcess.run('flutter', []));
|
||||
|
||||
verify(
|
||||
() => processWrapper.run(
|
||||
any(),
|
||||
[
|
||||
'--local-engine-src-path=$localEngineSrcPath',
|
||||
'--local-engine=android_release_arm64',
|
||||
],
|
||||
runInShell: any(named: 'runInShell'),
|
||||
environment: any(named: 'environment'),
|
||||
workingDirectory: any(named: 'workingDirectory'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
group('start', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => processWrapper.start(
|
||||
any(),
|
||||
any(),
|
||||
environment: any(named: 'environment'),
|
||||
runInShell: any(named: 'runInShell'),
|
||||
),
|
||||
).thenAnswer((_) async => startProcess);
|
||||
});
|
||||
|
||||
test('forwards non-flutter executables to Process.run', () async {
|
||||
await shorebirdProcess.start('git', ['pull'], runInShell: true);
|
||||
|
||||
@@ -260,55 +390,54 @@ void main() {
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('Updates environment if useVendedFlutter is true', () async {
|
||||
await runWithOverrides(
|
||||
() => shorebirdProcess.start(
|
||||
'flutter',
|
||||
['--version'],
|
||||
runInShell: true,
|
||||
environment: {'ENV_VAR': 'asdfasdf'},
|
||||
),
|
||||
);
|
||||
|
||||
verify(
|
||||
() => processWrapper.start(
|
||||
any(
|
||||
that: contains(
|
||||
p.join('bin', 'cache', 'flutter', 'bin', 'flutter'),
|
||||
),
|
||||
test('Updates environment if useVendedFlutter is true', () async {
|
||||
await runWithOverrides(
|
||||
() => shorebirdProcess.start(
|
||||
'flutter',
|
||||
['--version'],
|
||||
runInShell: true,
|
||||
environment: {'ENV_VAR': 'asdfasdf'},
|
||||
),
|
||||
['--version'],
|
||||
runInShell: true,
|
||||
environment: {
|
||||
'ENV_VAR': 'asdfasdf',
|
||||
...flutterStorageBaseUrlEnv,
|
||||
},
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test(
|
||||
'Makes no changes to environment if useVendedFlutter is false',
|
||||
() async {
|
||||
await shorebirdProcess.start(
|
||||
'flutter',
|
||||
['--version'],
|
||||
runInShell: true,
|
||||
useVendedFlutter: false,
|
||||
environment: {'hello': 'world'},
|
||||
);
|
||||
|
||||
verify(
|
||||
() => processWrapper.start(
|
||||
any(
|
||||
that: contains(
|
||||
p.join('bin', 'cache', 'flutter', 'bin', 'flutter'),
|
||||
),
|
||||
),
|
||||
['--version'],
|
||||
runInShell: true,
|
||||
environment: {
|
||||
'ENV_VAR': 'asdfasdf',
|
||||
...flutterStorageBaseUrlEnv,
|
||||
},
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test(
|
||||
'Makes no changes to environment if useVendedFlutter is false',
|
||||
() async {
|
||||
await shorebirdProcess.start(
|
||||
'flutter',
|
||||
['--version'],
|
||||
runInShell: true,
|
||||
useVendedFlutter: false,
|
||||
environment: {'hello': 'world'},
|
||||
),
|
||||
).called(1);
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
verify(
|
||||
() => processWrapper.start(
|
||||
'flutter',
|
||||
['--version'],
|
||||
runInShell: true,
|
||||
environment: {'hello': 'world'},
|
||||
),
|
||||
).called(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user