feat(shorebird_cli): add XcodeBuild (#956)

This commit is contained in:
Felix Angelov
2023-07-31 14:33:46 -05:00
committed by GitHub
parent 188cd40bcb
commit 0be4fed904
5 changed files with 274 additions and 2 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ class MissingGradleWrapperException implements Exception {
String toString() {
return '''
Could not find $executablePath.
Make sure you have run "flutter build apk at least once.''';
Make sure you have run "flutter build apk" at least once.''';
}
}
@@ -0,0 +1,107 @@
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/process.dart';
/// {@template missing_ios_project_exception}
/// Thrown when the Flutter project does not have iOS configured as a platform.
/// {@endtemplate}
class MissingIOSProjectException implements Exception {
/// {@macro missing_ios_project_exception}
const MissingIOSProjectException(this.projectPath);
final String projectPath;
@override
String toString() {
return '''
Could not find an iOS project in $projectPath.
To add iOS, run "flutter create . --platforms ios"''';
}
}
/// {@template xcode_project_build_info}
/// Xcode project build information returned by `xcodebuild -list`
/// {@endtemplate}
class XcodeProjectBuildInfo {
/// {@macro xcode_project_build_info}
XcodeProjectBuildInfo({
this.targets = const {},
this.buildConfigurations = const {},
this.schemes = const {},
});
/// Set of targets configured for the project.
final Set<String> targets;
/// Set of build configurations configured for the project.
final Set<String> buildConfigurations;
/// Set of schemes configured for the project.
final Set<String> schemes;
}
/// A wrapper around the `xcodebuild` command.
class XcodeBuild {
/// Name of the executable.
static const executable = 'xcodebuild';
/// Return Xcode project build info returned by `xcodebuild -list`
/// for the app at [projectPath].
Future<XcodeProjectBuildInfo> list(String projectPath) async {
// Flutter apps have ios files in root/ios
// Flutter modules have ios files in root/.ios
final iosRoot = [
Directory(p.join(projectPath, 'ios')),
Directory(p.join(projectPath, '.ios')),
].firstWhereOrNull((dir) => dir.existsSync());
if (iosRoot == null) throw MissingIOSProjectException(projectPath);
const arguments = ['-list'];
final result = await process.run(
executable,
arguments,
workingDirectory: iosRoot.path,
);
if (result.exitCode != ExitCode.success.code) {
throw ProcessException(executable, arguments, '${result.stderr}');
}
final lines = '${result.stdout}'.split('\n');
final targets = <String>{};
final buildConfigurations = <String>{};
final schemes = <String>{};
Set<String>? bucket;
for (final line in lines) {
if (line.isEmpty) {
bucket = null;
continue;
}
if (line.endsWith('Targets:')) {
bucket = targets;
continue;
}
if (line.endsWith('Build Configurations:')) {
bucket = buildConfigurations;
continue;
}
if (line.endsWith('Schemes:')) {
bucket = schemes;
continue;
}
bucket?.add(line.trim());
}
if (schemes.isEmpty) schemes.add('Runner');
return XcodeProjectBuildInfo(
targets: targets,
buildConfigurations: buildConfigurations,
schemes: schemes,
);
}
}
@@ -0,0 +1,29 @@
Command line invocation:
/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild -list
User defaults from command line:
IDEPackageSupportUseBuiltinSCM = YES
Information about project "Runner":
Targets:
Runner
RunnerTests
Build Configurations:
Debug
Debug-stable
Debug-internal
Release
Release-stable
Release-internal
Profile
Profile-stable
Profile-internal
If no build configuration is specified and -scheme is not passed then "Release" is used.
Schemes:
internal
Runner
stable
@@ -67,7 +67,7 @@ void main() {
const MissingGradleWrapperException('test').toString(),
'''
Could not find test.
Make sure you have run "flutter build apk at least once.''',
Make sure you have run "flutter build apk" at least once.''',
);
});
});
@@ -0,0 +1,136 @@
import 'dart:io';
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as p;
import 'package:scoped/scoped.dart';
import 'package:shorebird_cli/src/process.dart';
import 'package:shorebird_cli/src/xcodebuild.dart';
import 'package:test/test.dart';
class _MockProcess extends Mock implements ShorebirdProcess {}
void main() {
group(XcodeBuild, () {
late ShorebirdProcess process;
late XcodeBuild xcodeBuild;
R runWithOverrides<R>(R Function() body) {
return runScoped(
body,
values: {
processRef.overrideWith(() => process),
},
);
}
Directory setUpAppTempDir() {
final tempDir = Directory.systemTemp.createTempSync();
Directory(p.join(tempDir.path, 'ios')).createSync(recursive: true);
return tempDir;
}
setUp(() {
process = _MockProcess();
xcodeBuild = runWithOverrides(XcodeBuild.new);
});
group(MissingIOSProjectException, () {
test('toString', () {
const exception = MissingIOSProjectException('test_project_path');
expect(
exception.toString(),
'''
Could not find an iOS project in test_project_path.
To add iOS, run "flutter create . --platforms ios"''',
);
});
});
group('list', () {
test('throws a MissingIOSProjectException if no iOS project is found',
() {
final tempDir = Directory.systemTemp.createTempSync();
expect(
() => xcodeBuild.list(tempDir.path),
throwsA(isA<MissingIOSProjectException>()),
);
verifyNever(
() => process.run(
any(),
any(),
workingDirectory: any(named: 'workingDirectory'),
),
);
});
test('throws ProcessException when xcodebuild fails', () async {
final tempDir = setUpAppTempDir();
const message = 'oops';
when(
() => process.run(
any(),
any(),
workingDirectory: any(named: 'workingDirectory'),
),
).thenAnswer(
(_) async => ShorebirdProcessResult(
exitCode: ExitCode.software.code,
stdout: '',
stderr: message,
),
);
expect(
() => runWithOverrides(() => xcodeBuild.list(tempDir.path)),
throwsA(isA<ProcessException>()),
);
});
test('returns correct XcodeProjectBuildInfo for an app', () async {
final tempDir = setUpAppTempDir();
when(
() => process.run(
any(),
any(),
workingDirectory: any(named: 'workingDirectory'),
),
).thenAnswer(
(_) async => ShorebirdProcessResult(
exitCode: ExitCode.success.code,
stdout:
File('test/fixtures/xcodebuild_list.txt').readAsStringSync(),
stderr: '',
),
);
final info = await runWithOverrides(
() => xcodeBuild.list(tempDir.path),
);
expect(info.targets, equals({'Runner', 'RunnerTests'}));
expect(
info.buildConfigurations,
equals(
{
'Debug',
'Debug-stable',
'Debug-internal',
'Release',
'Release-stable',
'Release-internal',
'Profile',
'Profile-stable',
'Profile-internal'
},
),
);
expect(info.schemes, equals({'Runner', 'stable', 'internal'}));
verify(
() => process.run(
'xcodebuild',
['-list'],
workingDirectory: p.join(tempDir.path, 'ios'),
),
).called(1);
});
});
});
}