Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(shorebird_cli): create Gradlew #954

Merged
merged 2 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/shorebird_cli/bin/shorebird.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:shorebird_cli/src/cache.dart';
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
import 'package:shorebird_cli/src/command_runner.dart';
import 'package:shorebird_cli/src/doctor.dart';
import 'package:shorebird_cli/src/gradlew.dart';
import 'package:shorebird_cli/src/ios_deploy.dart';
import 'package:shorebird_cli/src/java.dart';
import 'package:shorebird_cli/src/logger.dart';
Expand All @@ -31,6 +32,7 @@ Future<void> main(List<String> args) async {
codePushClientWrapperRef,
doctorRef,
engineConfigRef,
gradlewRef,
iosDeployRef,
javaRef,
loggerRef,
Expand Down
7 changes: 3 additions & 4 deletions packages/shorebird_cli/lib/src/commands/init_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import 'dart:io';
import 'package:mason_logger/mason_logger.dart';
import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_cli/src/doctor.dart';
import 'package:shorebird_cli/src/gradlew.dart';
import 'package:shorebird_cli/src/logger.dart';
import 'package:shorebird_cli/src/shorebird_config_mixin.dart';
import 'package:shorebird_cli/src/shorebird_create_app_mixin.dart';
import 'package:shorebird_cli/src/shorebird_environment.dart';
import 'package:shorebird_cli/src/shorebird_flavor_mixin.dart';
import 'package:shorebird_cli/src/shorebird_validation_mixin.dart';

/// {@template init_command}
Expand All @@ -19,8 +19,7 @@ class InitCommand extends ShorebirdCommand
with
ShorebirdConfigMixin,
ShorebirdValidationMixin,
ShorebirdCreateAppMixin,
ShorebirdFlavorMixin {
ShorebirdCreateAppMixin {
/// {@macro init_command}
InitCommand({super.buildCodePushClient}) {
argParser.addFlag(
Expand Down Expand Up @@ -75,7 +74,7 @@ If you want to reinitialize Shorebird, please run "shorebird init --force".''');
var productFlavors = <String>{};
final detectFlavorsProgress = logger.progress('Detecting product flavors');
try {
productFlavors = await extractProductFlavors(Directory.current.path);
productFlavors = await gradlew.productFlavors(Directory.current.path);
detectFlavorsProgress.complete();
} catch (error) {
detectFlavorsProgress.fail();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'dart:io';

import 'package:collection/collection.dart';
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/command.dart';
import 'package:scoped/scoped.dart';
import 'package:shorebird_cli/src/java.dart';
import 'package:shorebird_cli/src/platform.dart';
import 'package:shorebird_cli/src/process.dart';
Expand All @@ -26,36 +26,45 @@ Make sure you have run "flutter build apk at least once.''';
}
}

/// Mixin on [ShorebirdCommand] which exposes methods for extracting
/// product flavors from the current app.
mixin ShorebirdFlavorMixin on ShorebirdCommand {
/// Return the set of product flavors configured for the app at [appRoot].
/// Returns an empty set for apps that do not use product flavors.
Future<Set<String>> extractProductFlavors(String appRoot) async {
/// A reference to a [Gradlew] instance.
final gradlewRef = create(Gradlew.new);

/// The [Gradlew] instance available in the current zone.
Gradlew get gradlew => read(gradlewRef);

/// A wrapper around the gradle wrapper (gradlew).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

class Gradlew {
String get executable => platform.isWindows ? 'gradlew.bat' : 'gradlew';

String executablePath(String projectPath) {
// Flutter apps have android files in root/android
// Flutter modules have android files in root/.android
final androidRoot = [
Directory(p.join(appRoot, 'android')),
Directory(p.join(appRoot, '.android')),
Directory(p.join(projectPath, 'android')),
Directory(p.join(projectPath, '.android')),
].firstWhereOrNull((dir) => dir.existsSync());

if (androidRoot == null) {
return {};
}
if (androidRoot == null) throw MissingGradleWrapperException(executable);

final executable = platform.isWindows ? 'gradlew.bat' : 'gradlew';
final executablePath = p.join(androidRoot.path, executable);
final executableFile = File(p.join(androidRoot.path, executable));

if (!File(executablePath).existsSync()) {
throw MissingGradleWrapperException(p.relative(executablePath));
if (!executableFile.existsSync()) {
throw MissingGradleWrapperException(p.relative(executableFile.path));
}

return executableFile.path;
}

/// Return the set of product flavors configured for the app at [projectPath].
/// Returns an empty set for apps that do not use product flavors.
Future<Set<String>> productFlavors(String projectPath) async {
final javaHome = java.home;
final gradlePath = executablePath(projectPath);
final result = await process.run(
executablePath,
executablePath(projectPath),
['app:tasks', '--all', '--console=auto'],
runInShell: true,
workingDirectory: androidRoot.path,
workingDirectory: p.dirname(gradlePath),
environment: {
if (javaHome != null) 'JAVA_HOME': javaHome,
},
Expand Down
174 changes: 18 additions & 156 deletions packages/shorebird_cli/test/src/commands/init_command_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,13 @@ import 'package:http/http.dart' as http;
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as p;
import 'package:platform/platform.dart';
import 'package:scoped/scoped.dart';
import 'package:shorebird_cli/src/auth/auth.dart';
import 'package:shorebird_cli/src/commands/init_command.dart';
import 'package:shorebird_cli/src/doctor.dart';
import 'package:shorebird_cli/src/java.dart';
import 'package:shorebird_cli/src/gradlew.dart';
import 'package:shorebird_cli/src/logger.dart';
import 'package:shorebird_cli/src/platform.dart';
import 'package:shorebird_cli/src/process.dart';
import 'package:shorebird_cli/src/shorebird_flavor_mixin.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
import 'package:test/test.dart';

Expand All @@ -28,18 +25,12 @@ class _MockCodePushClient extends Mock implements CodePushClient {}

class _MockDoctor extends Mock implements Doctor {}

class _MockJava extends Mock implements Java {}
class _MockGradlew extends Mock implements Gradlew {}

class _MockLogger extends Mock implements Logger {}

class _MockProgress extends Mock implements Progress {}

class _MockProcess extends Mock implements ShorebirdProcess {}

class _MockProcessResult extends Mock implements ShorebirdProcessResult {}

class _MockPlatform extends Mock implements Platform {}

void main() {
group(InitCommand, () {
const version = '1.2.3';
Expand All @@ -52,16 +43,12 @@ name: $appName
version: $version
environment:
sdk: ">=2.19.0 <3.0.0"''';
const javaHome = 'test_java_home';

late http.Client httpClient;
late ArgResults argResults;
late Auth auth;
late Doctor doctor;
late Java java;
late Platform platform;
late ShorebirdProcess process;
late ShorebirdProcessResult result;
late Gradlew gradlew;
late CodePushClient codePushClient;
late Logger logger;
late Progress progress;
Expand All @@ -73,9 +60,8 @@ environment:
values: {
authRef.overrideWith(() => auth),
doctorRef.overrideWith(() => doctor),
javaRef.overrideWith(() => java),
gradlewRef.overrideWith(() => gradlew),
loggerRef.overrideWith(() => logger),
platformRef.overrideWith(() => platform),
processRef.overrideWith(() => process),
},
);
Expand All @@ -87,21 +73,12 @@ environment:
return tempDir;
}

Directory setUpModuleTempDir() {
final tempDir = Directory.systemTemp.createTempSync();
Directory(p.join(tempDir.path, '.android')).createSync(recursive: true);
return tempDir;
}

setUp(() {
httpClient = _MockHttpClient();
argResults = _MockArgResults();
auth = _MockAuth();
doctor = _MockDoctor();
java = _MockJava();
platform = _MockPlatform();
process = _MockProcess();
result = _MockProcessResult();
gradlew = _MockGradlew();
codePushClient = _MockCodePushClient();
logger = _MockLogger();
progress = _MockProgress();
Expand All @@ -122,21 +99,7 @@ environment:
() => logger.prompt(any(), defaultValue: any(named: 'defaultValue')),
).thenReturn(appName);
when(() => logger.progress(any())).thenReturn(progress);
when(
() => process.run(
any(),
any(),
runInShell: any(named: 'runInShell'),
workingDirectory: any(named: 'workingDirectory'),
environment: any(named: 'environment'),
),
).thenAnswer((_) async => result);

when(() => result.exitCode).thenReturn(ExitCode.success.code);
when(() => result.stdout).thenReturn('');

when(() => java.home).thenReturn(javaHome);
when(() => platform.isWindows).thenReturn(false);
when(() => gradlew.productFlavors(any())).thenAnswer((_) async => {});

command = runWithOverrides(
() => InitCommand(
Expand All @@ -150,111 +113,6 @@ environment:
)..testArgResults = argResults;
});

group('extractProductFlavors', () {
test(
'throws MissingGradleWrapperException '
'when gradlew does not exist', () async {
when(() => platform.isLinux).thenReturn(true);
when(() => platform.isMacOS).thenReturn(false);
when(() => platform.isWindows).thenReturn(false);
final tempDir = setUpAppTempDir();
await expectLater(
command.extractProductFlavors(tempDir.path),
throwsA(isA<MissingGradleWrapperException>()),
);
verifyNever(
() => process.run(
p.join(tempDir.path, 'android', 'gradlew'),
['app:tasks', '--all', '--console=auto'],
runInShell: true,
workingDirectory: p.join(tempDir.path, 'android'),
environment: {'JAVA_HOME': javaHome},
),
);
});

test('uses existing JAVA_HOME when set', () async {
when(() => platform.isLinux).thenReturn(true);
when(() => platform.isMacOS).thenReturn(false);
when(() => platform.isWindows).thenReturn(false);
final tempDir = setUpAppTempDir();
File(
p.join(tempDir.path, 'android', 'gradlew'),
).createSync(recursive: true);
await expectLater(
runWithOverrides(() => command.extractProductFlavors(tempDir.path)),
completes,
);
verify(
() => process.run(
p.join(tempDir.path, 'android', 'gradlew'),
['app:tasks', '--all', '--console=auto'],
runInShell: true,
workingDirectory: p.join(tempDir.path, 'android'),
environment: {'JAVA_HOME': javaHome},
),
).called(1);
});

test(
'throws Exception '
'when process exits with non-zero code', () async {
when(() => platform.isLinux).thenReturn(true);
when(() => platform.isMacOS).thenReturn(false);
when(() => platform.isWindows).thenReturn(false);
final tempDir = setUpAppTempDir();
File(
p.join(tempDir.path, 'android', 'gradlew'),
).createSync(recursive: true);
when(() => result.exitCode).thenReturn(1);
when(() => result.stderr).thenReturn('test error');
await expectLater(
runWithOverrides(() => command.extractProductFlavors(tempDir.path)),
throwsA(
isA<Exception>().having(
(e) => '$e',
'message',
contains('test error'),
),
),
);
verify(
() => process.run(
p.join(tempDir.path, 'android', 'gradlew'),
['app:tasks', '--all', '--console=auto'],
runInShell: true,
workingDirectory: p.join(tempDir.path, 'android'),
environment: {'JAVA_HOME': javaHome},
),
).called(1);
});

test('extracts flavors from module directory structure', () async {
when(() => platform.isLinux).thenReturn(true);
when(() => platform.isMacOS).thenReturn(false);
when(() => platform.isWindows).thenReturn(false);
final tempDir = setUpModuleTempDir();
File(
p.join(tempDir.path, '.android', 'gradlew'),
).createSync(recursive: true);
const javaHome = 'test_java_home';
when(() => platform.environment).thenReturn({'JAVA_HOME': javaHome});
await expectLater(
runWithOverrides(() => command.extractProductFlavors(tempDir.path)),
completes,
);
verify(
() => process.run(
p.join(tempDir.path, '.android', 'gradlew'),
['app:tasks', '--all', '--console=auto'],
runInShell: true,
workingDirectory: p.join(tempDir.path, '.android'),
environment: {'JAVA_HOME': javaHome},
),
).called(1);
});
});

test('returns no user error when not logged in', () async {
when(() => auth.isAuthenticated).thenReturn(false);
final result = await runWithOverrides(command.run);
Expand Down Expand Up @@ -339,9 +197,8 @@ If you want to reinitialize Shorebird, please run "shorebird init --force".''',
});

test('fails when an error occurs while extracting flavors', () async {
when(() => result.exitCode).thenReturn(1);
when(() => result.stdout).thenReturn('error');
when(() => result.stderr).thenReturn('oops');
final exception = Exception('oops');
when(() => gradlew.productFlavors(any())).thenThrow(exception);
final tempDir = setUpAppTempDir();
File(
p.join(tempDir.path, 'pubspec.yaml'),
Expand Down Expand Up @@ -405,17 +262,22 @@ If you want to reinitialize Shorebird, please run "shorebird init --force".''',
'test-appId-6'
];
var index = 0;
when(() => gradlew.productFlavors(any())).thenAnswer(
(_) async => {
'development',
'developmentInternal',
'production',
'productionInternal',
'staging',
'stagingInternal',
},
);
when(
() => codePushClient.createApp(displayName: any(named: 'displayName')),
).thenAnswer((invocation) async {
final displayName = invocation.namedArguments[#displayName] as String;
return App(id: appIds[index++], displayName: displayName);
});
when(() => result.stdout).thenReturn(
File(
p.join('test', 'fixtures', 'gradle_app_tasks.txt'),
).readAsStringSync(),
);
final tempDir = setUpAppTempDir();
File(p.join(tempDir.path, 'android', 'gradlew')).createSync();
File(
Expand Down
Loading