Unverified Commit b21e780a authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] refactor AotBuilder, removing globals and duplicated kernel compilation (#52896)

Reland of #52091
parent 11efa00f
...@@ -6,15 +6,18 @@ import 'dart:async'; ...@@ -6,15 +6,18 @@ import 'dart:async';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'base/build.dart';
import 'base/common.dart'; import 'base/common.dart';
import 'base/io.dart';
import 'base/logger.dart'; import 'base/logger.dart';
import 'base/process.dart';
import 'build_info.dart'; import 'build_info.dart';
import 'dart/package_map.dart'; import 'build_system/build_system.dart';
import 'build_system/targets/dart.dart';
import 'build_system/targets/icon_tree_shaker.dart';
import 'build_system/targets/ios.dart';
import 'cache.dart';
import 'convert.dart';
import 'globals.dart' as globals; import 'globals.dart' as globals;
import 'ios/bitcode.dart'; import 'ios/bitcode.dart';
import 'project.dart';
/// Builds AOT snapshots given a platform, build mode and a path to a Dart /// Builds AOT snapshots given a platform, build mode and a path to a Dart
/// library. /// library.
...@@ -26,18 +29,41 @@ class AotBuilder { ...@@ -26,18 +29,41 @@ class AotBuilder {
@required String mainDartFile, @required String mainDartFile,
bool bitcode = kBitcodeEnabledDefault, bool bitcode = kBitcodeEnabledDefault,
bool quiet = true, bool quiet = true,
bool reportTimings = false,
Iterable<DarwinArch> iosBuildArchs = defaultIOSArchs, Iterable<DarwinArch> iosBuildArchs = defaultIOSArchs,
}) async { }) async {
if (platform == null) { if (platform == null) {
throwToolExit('No AOT build platform specified'); throwToolExit('No AOT build platform specified');
} }
Target target;
if (bitcode) { bool expectSo = false;
if (platform != TargetPlatform.ios) { switch (platform) {
throwToolExit('Bitcode is only supported on iOS (TargetPlatform is $platform).'); case TargetPlatform.android:
} case TargetPlatform.darwin_x64:
await validateBitcode(buildInfo.mode, platform); case TargetPlatform.linux_x64:
case TargetPlatform.windows_x64:
case TargetPlatform.fuchsia_arm64:
case TargetPlatform.tester:
case TargetPlatform.web_javascript:
case TargetPlatform.android_x86:
throwToolExit('$platform is not supported in AOT.');
break;
case TargetPlatform.fuchsia_x64:
throwToolExit(
"To build release for fuchsia, use 'flutter build fuchsia --release'"
);
break;
case TargetPlatform.ios:
target = buildInfo.isRelease
? const AotAssemblyRelease()
: const AotAssemblyProfile();
break;
case TargetPlatform.android_arm:
case TargetPlatform.android_arm64:
case TargetPlatform.android_x64:
expectSo = true;
target = buildInfo.isRelease
? const AotElfRelease()
: const AotElfProfile();
} }
Status status; Status status;
...@@ -48,102 +74,46 @@ class AotBuilder { ...@@ -48,102 +74,46 @@ class AotBuilder {
timeout: timeoutConfiguration.slowOperation, timeout: timeoutConfiguration.slowOperation,
); );
} }
try {
final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: reportTimings);
// Compile to kernel. final Environment environment = Environment(
final String kernelOut = await snapshotter.compileKernel( projectDir: globals.fs.currentDirectory,
platform: platform, outputDir: globals.fs.directory(outputPath),
buildMode: buildInfo.mode, buildDir: FlutterProject.current().dartTool.childDirectory('flutter_build'),
mainPath: mainDartFile, cacheDir: null,
packagesPath: PackageMap.globalPackagesPath, flutterRootDir: globals.fs.directory(Cache.flutterRoot),
trackWidgetCreation: buildInfo.trackWidgetCreation, defines: <String, String>{
outputPath: outputPath, kTargetFile: mainDartFile ?? globals.fs.path.join('lib', 'main.dart'),
extraFrontEndOptions: buildInfo.extraFrontEndOptions, kBuildMode: getNameForBuildMode(buildInfo.mode),
dartDefines: buildInfo.dartDefines kTargetPlatform: getNameForTargetPlatform(platform),
); kIconTreeShakerFlag: buildInfo.treeShakeIcons.toString(),
if (kernelOut == null) { kDartDefines: jsonEncode(buildInfo.dartDefines),
throwToolExit('Compiler terminated unexpectedly.'); kBitcodeFlag: bitcode.toString(),
return; if (buildInfo?.extraGenSnapshotOptions?.isNotEmpty ?? false)
kExtraGenSnapshotOptions: buildInfo.extraGenSnapshotOptions.join(','),
if (buildInfo?.extraFrontEndOptions?.isNotEmpty ?? false)
kExtraFrontEndOptions: buildInfo.extraFrontEndOptions.join(','),
if (platform == TargetPlatform.ios)
kIosArchs: iosBuildArchs.map(getNameForDarwinArch).join(' ')
} }
);
final BuildResult result = await buildSystem.build(target, environment);
status?.stop();
// Build AOT snapshot. if (!result.success) {
if (platform == TargetPlatform.ios) { for (final ExceptionMeasurement measurement in result.exceptions.values) {
// Determine which iOS architectures to build for. globals.printError(measurement.exception.toString());
final Map<DarwinArch, String> iosBuilds = <DarwinArch, String>{};
for (final DarwinArch arch in iosBuildArchs) {
iosBuilds[arch] = globals.fs.path.join(outputPath, getNameForDarwinArch(arch));
}
// Generate AOT snapshot and compile to arch-specific App.framework.
final Map<DarwinArch, Future<int>> exitCodes = <DarwinArch, Future<int>>{};
iosBuilds.forEach((DarwinArch iosArch, String outputPath) {
exitCodes[iosArch] = snapshotter.build(
platform: platform,
darwinArch: iosArch,
buildMode: buildInfo.mode,
mainPath: kernelOut,
packagesPath: PackageMap.globalPackagesPath,
outputPath: outputPath,
extraGenSnapshotOptions: buildInfo.extraGenSnapshotOptions,
bitcode: bitcode,
quiet: quiet,
splitDebugInfo: null,
dartObfuscation: false,
).then<int>((int buildExitCode) {
return buildExitCode;
});
});
// Merge arch-specific App.frameworks into a multi-arch App.framework.
if ((await Future.wait<int>(exitCodes.values)).every((int buildExitCode) => buildExitCode == 0)) {
final Iterable<String> dylibs = iosBuilds.values.map<String>(
(String outputDir) => globals.fs.path.join(outputDir, 'App.framework', 'App'));
globals.fs.directory(globals.fs.path.join(outputPath, 'App.framework')).createSync();
await processUtils.run(
<String>[
'lipo',
...dylibs,
'-create',
'-output', globals.fs.path.join(outputPath, 'App.framework', 'App'),
],
throwOnError: true,
);
} else {
status?.cancel();
exitCodes.forEach((DarwinArch iosArch, Future<int> exitCodeFuture) async {
final int buildExitCode = await exitCodeFuture;
globals.printError('Snapshotting ($iosArch) exited with non-zero exit code: $buildExitCode');
});
}
} else {
// Android AOT snapshot.
final int snapshotExitCode = await snapshotter.build(
platform: platform,
buildMode: buildInfo.mode,
mainPath: kernelOut,
packagesPath: PackageMap.globalPackagesPath,
outputPath: outputPath,
extraGenSnapshotOptions: buildInfo.extraGenSnapshotOptions,
bitcode: false,
splitDebugInfo: null,
dartObfuscation: false,
);
if (snapshotExitCode != 0) {
status?.cancel();
throwToolExit('Snapshotting exited with non-zero exit code: $snapshotExitCode');
}
} }
} on ProcessException catch (error) { throwToolExit('The aot build failed.');
// Catch the String exceptions thrown from the `runSync` methods below.
status?.cancel();
globals.printError(error.toString());
return;
} }
status?.stop();
if (outputPath == null) { if (expectSo) {
throwToolExit(null); environment.buildDir.childFile('app.so')
.copySync(globals.fs.path.join(outputPath, 'app.so'));
} else {
globals.fs.directory(globals.fs.path.join(outputPath, 'App.framework'))
.createSync(recursive: true);
environment.buildDir.childDirectory('App.framework').childFile('App')
.copySync(globals.fs.path.join(outputPath, 'App.framework', 'App'));
} }
final String builtMessage = 'Built to $outputPath${globals.fs.path.separator}.'; final String builtMessage = 'Built to $outputPath${globals.fs.path.separator}.';
......
...@@ -697,7 +697,7 @@ abstract class Status { ...@@ -697,7 +697,7 @@ abstract class Status {
@required Duration timeout, @required Duration timeout,
@required TimeoutConfiguration timeoutConfiguration, @required TimeoutConfiguration timeoutConfiguration,
@required Stopwatch stopwatch, @required Stopwatch stopwatch,
@required AnsiTerminal terminal, @required Terminal terminal,
VoidCallback onFinish, VoidCallback onFinish,
SlowWarningCallback slowWarningCallback, SlowWarningCallback slowWarningCallback,
}) { }) {
...@@ -877,7 +877,7 @@ class AnsiSpinner extends Status { ...@@ -877,7 +877,7 @@ class AnsiSpinner extends Status {
@required Duration timeout, @required Duration timeout,
@required TimeoutConfiguration timeoutConfiguration, @required TimeoutConfiguration timeoutConfiguration,
@required Stopwatch stopwatch, @required Stopwatch stopwatch,
@required AnsiTerminal terminal, @required Terminal terminal,
VoidCallback onFinish, VoidCallback onFinish,
this.slowWarningCallback, this.slowWarningCallback,
Stdio stdio, Stdio stdio,
...@@ -893,7 +893,7 @@ class AnsiSpinner extends Status { ...@@ -893,7 +893,7 @@ class AnsiSpinner extends Status {
final String _backspaceChar = '\b'; final String _backspaceChar = '\b';
final String _clearChar = ' '; final String _clearChar = ' ';
final Stdio _stdio; final Stdio _stdio;
final AnsiTerminal _terminal; final Terminal _terminal;
bool timedOut = false; bool timedOut = false;
......
...@@ -210,7 +210,14 @@ class AndroidAot extends AotElfBase { ...@@ -210,7 +210,14 @@ class AndroidAot extends AotElfBase {
@override @override
Future<void> build(Environment environment) async { Future<void> build(Environment environment) async {
final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: false); final AOTSnapshotter snapshotter = AOTSnapshotter(
reportTimings: false,
fileSystem: globals.fs,
logger: globals.logger,
xcode: globals.xcode,
processManager: globals.processManager,
artifacts: globals.artifacts,
);
final Directory output = environment.buildDir.childDirectory(_androidAbiName); final Directory output = environment.buildDir.childDirectory(_androidAbiName);
final String splitDebugInfo = environment.defines[kSplitDebugInfo]; final String splitDebugInfo = environment.defines[kSplitDebugInfo];
if (environment.defines[kBuildMode] == null) { if (environment.defines[kBuildMode] == null) {
......
...@@ -204,9 +204,7 @@ class KernelSnapshot extends Target { ...@@ -204,9 +204,7 @@ class KernelSnapshot extends Target {
final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]); final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]);
// This configuration is all optional. // This configuration is all optional.
final List<String> extraFrontEndOptions = <String>[ final List<String> extraFrontEndOptions = environment.defines[kExtraFrontEndOptions]?.split(',');
...?environment.defines[kExtraFrontEndOptions]?.split(',')
];
final List<String> fileSystemRoots = environment.defines[kFileSystemRoots]?.split(','); final List<String> fileSystemRoots = environment.defines[kFileSystemRoots]?.split(',');
final String fileSystemScheme = environment.defines[kFileSystemScheme]; final String fileSystemScheme = environment.defines[kFileSystemScheme];
...@@ -261,7 +259,14 @@ abstract class AotElfBase extends Target { ...@@ -261,7 +259,14 @@ abstract class AotElfBase extends Target {
@override @override
Future<void> build(Environment environment) async { Future<void> build(Environment environment) async {
final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: false); final AOTSnapshotter snapshotter = AOTSnapshotter(
reportTimings: false,
fileSystem: globals.fs,
logger: globals.logger,
xcode: globals.xcode,
processManager: globals.processManager,
artifacts: globals.artifacts,
);
final String outputPath = environment.buildDir.path; final String outputPath = environment.buildDir.path;
if (environment.defines[kBuildMode] == null) { if (environment.defines[kBuildMode] == null) {
throw MissingDefineException(kBuildMode, 'aot_elf'); throw MissingDefineException(kBuildMode, 'aot_elf');
......
...@@ -28,7 +28,14 @@ abstract class AotAssemblyBase extends Target { ...@@ -28,7 +28,14 @@ abstract class AotAssemblyBase extends Target {
@override @override
Future<void> build(Environment environment) async { Future<void> build(Environment environment) async {
final AOTSnapshotter snapshotter = AOTSnapshotter(reportTimings: false); final AOTSnapshotter snapshotter = AOTSnapshotter(
reportTimings: false,
fileSystem: globals.fs,
logger: globals.logger,
xcode: globals.xcode,
artifacts: globals.artifacts,
processManager: globals.processManager,
);
final String buildOutputPath = environment.buildDir.path; final String buildOutputPath = environment.buildDir.path;
if (environment.defines[kBuildMode] == null) { if (environment.defines[kBuildMode] == null) {
throw MissingDefineException(kBuildMode, 'aot_assembly'); throw MissingDefineException(kBuildMode, 'aot_assembly');
...@@ -36,6 +43,8 @@ abstract class AotAssemblyBase extends Target { ...@@ -36,6 +43,8 @@ abstract class AotAssemblyBase extends Target {
if (environment.defines[kTargetPlatform] == null) { if (environment.defines[kTargetPlatform] == null) {
throw MissingDefineException(kTargetPlatform, 'aot_assembly'); throw MissingDefineException(kTargetPlatform, 'aot_assembly');
} }
final List<String> extraGenSnapshotOptions = environment
.defines[kExtraGenSnapshotOptions]?.split(',') ?? const <String>[];
final bool bitcode = environment.defines[kBitcodeFlag] == 'true'; final bool bitcode = environment.defines[kBitcodeFlag] == 'true';
final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]); final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]);
final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]); final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]);
...@@ -65,6 +74,7 @@ abstract class AotAssemblyBase extends Target { ...@@ -65,6 +74,7 @@ abstract class AotAssemblyBase extends Target {
quiet: true, quiet: true,
splitDebugInfo: splitDebugInfo, splitDebugInfo: splitDebugInfo,
dartObfuscation: dartObfuscation, dartObfuscation: dartObfuscation,
extraGenSnapshotOptions: extraGenSnapshotOptions,
)); ));
} }
final List<int> results = await Future.wait(pending); final List<int> results = await Future.wait(pending);
......
...@@ -202,7 +202,15 @@ class CompileMacOSFramework extends Target { ...@@ -202,7 +202,15 @@ class CompileMacOSFramework extends Target {
} }
final String splitDebugInfo = environment.defines[kSplitDebugInfo]; final String splitDebugInfo = environment.defines[kSplitDebugInfo];
final bool dartObfuscation = environment.defines[kDartObfuscation] == 'true'; final bool dartObfuscation = environment.defines[kDartObfuscation] == 'true';
final int result = await AOTSnapshotter(reportTimings: false).build( final AOTSnapshotter snapshotter = AOTSnapshotter(
reportTimings: false,
fileSystem: globals.fs,
logger: globals.logger,
xcode: globals.xcode,
artifacts: globals.artifacts,
processManager: globals.processManager
);
final int result = await snapshotter.build(
bitcode: false, bitcode: false,
buildMode: buildMode, buildMode: buildMode,
mainPath: environment.buildDir.childFile('app.dill').path, mainPath: environment.buildDir.childFile('app.dill').path,
......
...@@ -27,11 +27,6 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen ...@@ -27,11 +27,6 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen
allowed: <String>['android-arm', 'android-arm64', 'ios', 'android-x64'], allowed: <String>['android-arm', 'android-arm64', 'ios', 'android-x64'],
) )
..addFlag('quiet', defaultsTo: false) ..addFlag('quiet', defaultsTo: false)
..addFlag('report-timings',
negatable: false,
defaultsTo: false,
help: 'Report timing information about build steps in machine readable form,',
)
..addMultiOption('ios-arch', ..addMultiOption('ios-arch',
splitCommas: true, splitCommas: true,
defaultsTo: defaultIOSArchs.map<String>(getNameForDarwinArch), defaultsTo: defaultIOSArchs.map<String>(getNameForDarwinArch),
...@@ -80,7 +75,6 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen ...@@ -80,7 +75,6 @@ class BuildAotCommand extends BuildSubCommand with TargetPlatformBasedDevelopmen
mainDartFile: findMainDartFile(targetFile), mainDartFile: findMainDartFile(targetFile),
bitcode: boolArg('bitcode'), bitcode: boolArg('bitcode'),
quiet: boolArg('quiet'), quiet: boolArg('quiet'),
reportTimings: boolArg('report-timings'),
iosBuildArchs: stringsArg('ios-arch').map<DarwinArch>(getIOSArchForName), iosBuildArchs: stringsArg('ios-arch').map<DarwinArch>(getIOSArchForName),
); );
return FlutterCommandResult.success(); return FlutterCommandResult.success();
......
...@@ -436,7 +436,6 @@ end ...@@ -436,7 +436,6 @@ end
mainDartFile: globals.fs.path.absolute(targetFile), mainDartFile: globals.fs.path.absolute(targetFile),
quiet: true, quiet: true,
bitcode: true, bitcode: true,
reportTimings: false,
iosBuildArchs: <DarwinArch>[DarwinArch.armv7, DarwinArch.arm64], iosBuildArchs: <DarwinArch>[DarwinArch.armv7, DarwinArch.arm64],
); );
} finally { } finally {
......
...@@ -11,7 +11,6 @@ import 'android/gradle_utils.dart'; ...@@ -11,7 +11,6 @@ import 'android/gradle_utils.dart';
import 'application_package.dart'; import 'application_package.dart';
import 'artifacts.dart'; import 'artifacts.dart';
import 'asset.dart'; import 'asset.dart';
import 'base/build.dart';
import 'base/config.dart'; import 'base/config.dart';
import 'base/context.dart'; import 'base/context.dart';
import 'base/io.dart'; import 'base/io.dart';
...@@ -125,7 +124,6 @@ Future<T> runInContext<T>( ...@@ -125,7 +124,6 @@ Future<T> runInContext<T>(
FuchsiaDeviceTools: () => FuchsiaDeviceTools(), FuchsiaDeviceTools: () => FuchsiaDeviceTools(),
FuchsiaSdk: () => FuchsiaSdk(), FuchsiaSdk: () => FuchsiaSdk(),
FuchsiaWorkflow: () => FuchsiaWorkflow(), FuchsiaWorkflow: () => FuchsiaWorkflow(),
GenSnapshot: () => const GenSnapshot(),
GradleUtils: () => GradleUtils(), GradleUtils: () => GradleUtils(),
HotRunnerConfig: () => HotRunnerConfig(), HotRunnerConfig: () => HotRunnerConfig(),
IMobileDevice: () => IMobileDevice(), IMobileDevice: () => IMobileDevice(),
......
...@@ -249,7 +249,7 @@ class XcodeProjectInterpreter { ...@@ -249,7 +249,7 @@ class XcodeProjectInterpreter {
@required ProcessManager processManager, @required ProcessManager processManager,
@required Logger logger, @required Logger logger,
@required FileSystem fileSystem, @required FileSystem fileSystem,
@required AnsiTerminal terminal, @required Terminal terminal,
}) : _platform = platform, }) : _platform = platform,
_fileSystem = fileSystem, _fileSystem = fileSystem,
_terminal = terminal, _terminal = terminal,
...@@ -259,7 +259,7 @@ class XcodeProjectInterpreter { ...@@ -259,7 +259,7 @@ class XcodeProjectInterpreter {
final Platform _platform; final Platform _platform;
final FileSystem _fileSystem; final FileSystem _fileSystem;
final ProcessUtils _processUtils; final ProcessUtils _processUtils;
final AnsiTerminal _terminal; final Terminal _terminal;
final Logger _logger; final Logger _logger;
static const String _executable = '/usr/bin/xcodebuild'; static const String _executable = '/usr/bin/xcodebuild';
......
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
import 'package:flutter_tools/src/base/build.dart'; import 'package:flutter_tools/src/base/build.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/build_system/build_system.dart'; import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/dart.dart'; import 'package:flutter_tools/src/build_system/targets/dart.dart';
import 'package:flutter_tools/src/build_system/targets/macos.dart'; import 'package:flutter_tools/src/build_system/targets/macos.dart';
...@@ -49,7 +48,6 @@ void main() { ...@@ -49,7 +48,6 @@ void main() {
Testbed testbed; Testbed testbed;
Environment environment; Environment environment;
MockPlatform mockPlatform; MockPlatform mockPlatform;
MockXcode mockXcode;
setUpAll(() { setUpAll(() {
Cache.disableLocking(); Cache.disableLocking();
...@@ -57,7 +55,6 @@ void main() { ...@@ -57,7 +55,6 @@ void main() {
}); });
setUp(() { setUp(() {
mockXcode = MockXcode();
mockPlatform = MockPlatform(); mockPlatform = MockPlatform();
when(mockPlatform.isWindows).thenReturn(false); when(mockPlatform.isWindows).thenReturn(false);
when(mockPlatform.isMacOS).thenReturn(true); when(mockPlatform.isMacOS).thenReturn(true);
...@@ -193,35 +190,6 @@ void main() { ...@@ -193,35 +190,6 @@ void main() {
expect(outputFramework.readAsStringSync(), 'DEF'); expect(outputFramework.readAsStringSync(), 'DEF');
})); }));
test('release/profile macOS compilation uses correct gen_snapshot', () => testbed.run(() async {
when(genSnapshot.run(
snapshotType: anyNamed('snapshotType'),
additionalArgs: anyNamed('additionalArgs'),
darwinArch: anyNamed('darwinArch'),
)).thenAnswer((Invocation invocation) {
environment.buildDir.childFile('snapshot_assembly.o').createSync();
environment.buildDir.childFile('snapshot_assembly.S').createSync();
return Future<int>.value(0);
});
when(mockXcode.cc(any)).thenAnswer((Invocation invocation) {
return Future<RunResult>.value(RunResult(FakeProcessResult()..exitCode = 0, <String>['test']));
});
when(mockXcode.clang(any)).thenAnswer((Invocation invocation) {
return Future<RunResult>.value(RunResult(FakeProcessResult()..exitCode = 0, <String>['test']));
});
environment.buildDir.childFile('app.dill').createSync(recursive: true);
globals.fs.file('.packages')
..createSync()
..writeAsStringSync('''
# Generated
sky_engine:file:///bin/cache/pkg/sky_engine/lib/
flutter_tools:lib/''');
await const CompileMacOSFramework().build(environment..defines[kBuildMode] = 'release');
}, overrides: <Type, Generator>{
GenSnapshot: () => MockGenSnapshot(),
Xcode: () => mockXcode,
}));
} }
class MockPlatform extends Mock implements Platform {} class MockPlatform extends Mock implements Platform {}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment