feat(shorebird_cli): add XcodeBuild (#956)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user