Unverified Commit 9184f4d6 authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Build iOS frameworks with relative output path (#47369)

parent beb86d2c
...@@ -45,19 +45,20 @@ Future<void> main() async { ...@@ -45,19 +45,20 @@ Future<void> main() async {
// This builds all build modes' frameworks by default // This builds all build modes' frameworks by default
section('Build frameworks'); section('Build frameworks');
const String outputDirectoryName = 'flutter-frameworks';
await inDirectory(projectDir, () async { await inDirectory(projectDir, () async {
await flutter( await flutter(
'build', 'build',
options: <String>['ios-framework'], options: <String>[
'ios-framework',
'--xcframework',
'--output=$outputDirectoryName'
],
); );
}); });
final String outputPath = path.join( final String outputPath = path.join(projectDir.path, outputDirectoryName);
projectDir.path,
'build',
'ios',
'framework',
);
section('Check debug build has Dart snapshot as asset'); section('Check debug build has Dart snapshot as asset');
...@@ -157,6 +158,22 @@ Future<void> main() async { ...@@ -157,6 +158,22 @@ Future<void> main() async {
'Flutter.framework', 'Flutter.framework',
'Flutter', 'Flutter',
)); ));
checkFileExists(path.join(
outputPath,
mode,
'Flutter.xcframework',
'ios-armv7_arm64',
'Flutter.framework',
'Flutter',
));
checkFileExists(path.join(
outputPath,
mode,
'Flutter.xcframework',
'ios-x86_64-simulator',
'Flutter.framework',
'Flutter',
));
} }
section("Check all modes' engine header"); section("Check all modes' engine header");
...@@ -177,6 +194,22 @@ Future<void> main() async { ...@@ -177,6 +194,22 @@ Future<void> main() async {
'device_info.framework', 'device_info.framework',
'device_info', 'device_info',
)); ));
checkFileExists(path.join(
outputPath,
mode,
'device_info.xcframework',
'ios-armv7_arm64',
'device_info.framework',
'device_info',
));
checkFileExists(path.join(
outputPath,
mode,
'device_info.xcframework',
'ios-x86_64-simulator',
'device_info.framework',
'device_info',
));
} }
section("Check all modes' have generated plugin registrant"); section("Check all modes' have generated plugin registrant");
...@@ -189,6 +222,24 @@ Future<void> main() async { ...@@ -189,6 +222,24 @@ Future<void> main() async {
'Headers', 'Headers',
'GeneratedPluginRegistrant.h', 'GeneratedPluginRegistrant.h',
)); ));
checkFileExists(path.join(
outputPath,
mode,
'FlutterPluginRegistrant.xcframework',
'ios-armv7_arm64',
'FlutterPluginRegistrant.framework',
'Headers',
'GeneratedPluginRegistrant.h',
));
checkFileExists(path.join(
outputPath,
mode,
'FlutterPluginRegistrant.xcframework',
'ios-x86_64-simulator',
'FlutterPluginRegistrant.framework',
'Headers',
'GeneratedPluginRegistrant.h',
));
} }
return TaskResult.success(null); return TaskResult.success(null);
......
...@@ -146,7 +146,7 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { ...@@ -146,7 +146,7 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
throwToolExit("Module's iOS folder missing"); throwToolExit("Module's iOS folder missing");
} }
final Directory outputDirectory = fs.directory(fs.path.normalize(outputArgument)); final Directory outputDirectory = fs.directory(fs.path.absolute(fs.path.normalize(outputArgument)));
aotBuilder ??= AotBuilder(); aotBuilder ??= AotBuilder();
bundleBuilder ??= BundleBuilder(); bundleBuilder ??= BundleBuilder();
...@@ -175,15 +175,18 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { ...@@ -175,15 +175,18 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
} }
final Status status = logger.startProgress(' └─Moving to ${fs.path.relative(modeDirectory.path)}', timeout: timeoutConfiguration.slowOperation); final Status status = logger.startProgress(' └─Moving to ${fs.path.relative(modeDirectory.path)}', timeout: timeoutConfiguration.slowOperation);
// Delete the intermediaries since they would have been copied into our try {
// output frameworks. // Delete the intermediaries since they would have been copied into our
if (iPhoneBuildOutput.existsSync()) { // output frameworks.
iPhoneBuildOutput.deleteSync(recursive: true); if (iPhoneBuildOutput.existsSync()) {
} iPhoneBuildOutput.deleteSync(recursive: true);
if (simulatorBuildOutput.existsSync()) { }
simulatorBuildOutput.deleteSync(recursive: true); if (simulatorBuildOutput.existsSync()) {
simulatorBuildOutput.deleteSync(recursive: true);
}
} finally {
status.stop();
} }
status.stop();
} }
printStatus('Frameworks written to ${outputDirectory.path}.'); printStatus('Frameworks written to ${outputDirectory.path}.');
...@@ -192,66 +195,81 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { ...@@ -192,66 +195,81 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
} }
Future<void> _produceFlutterFramework(Directory outputDirectory, BuildMode mode, Directory iPhoneBuildOutput, Directory simulatorBuildOutput, Directory modeDirectory) async { Future<void> _produceFlutterFramework(Directory outputDirectory, BuildMode mode, Directory iPhoneBuildOutput, Directory simulatorBuildOutput, Directory modeDirectory) async {
final Status status = logger.startProgress(' ├─Populating Flutter.framework...', timeout: timeoutConfiguration.fastOperation); final Status status = logger.startProgress(' ├─Populating Flutter.framework...', timeout: timeoutConfiguration.slowOperation);
final String engineCacheFlutterFrameworkDirectory = artifacts.getArtifactPath(Artifact.flutterFramework, platform: TargetPlatform.ios, mode: mode); try {
final String engineCacheFlutterFrameworkDirectory = artifacts.getArtifactPath(Artifact.flutterFramework, platform: TargetPlatform.ios, mode: mode);
// Copy universal engine cache framework to mode directory.
final String flutterFrameworkFileName = fs.path.basename(engineCacheFlutterFrameworkDirectory); // Copy universal engine cache framework to mode directory.
final Directory fatFlutterFrameworkCopy = modeDirectory.childDirectory(flutterFrameworkFileName); final String flutterFrameworkFileName = fs.path.basename(engineCacheFlutterFrameworkDirectory);
copyDirectorySync(fs.directory(engineCacheFlutterFrameworkDirectory), fatFlutterFrameworkCopy); final Directory fatFlutterFrameworkCopy = modeDirectory.childDirectory(flutterFrameworkFileName);
copyDirectorySync(fs.directory(engineCacheFlutterFrameworkDirectory), fatFlutterFrameworkCopy);
if (boolArg('xcframework')) {
// Copy universal framework to variant directory. if (boolArg('xcframework')) {
final Directory armFlutterFrameworkDirectory = iPhoneBuildOutput.childDirectory(flutterFrameworkFileName); // Copy universal framework to variant directory.
final File armFlutterFrameworkBinary = armFlutterFrameworkDirectory.childFile('Flutter'); final Directory armFlutterFrameworkDirectory = iPhoneBuildOutput.childDirectory(flutterFrameworkFileName);
final File fatFlutterFrameworkBinary = fatFlutterFrameworkCopy.childFile('Flutter'); final File armFlutterFrameworkBinary = armFlutterFrameworkDirectory.childFile('Flutter');
copyDirectorySync(fatFlutterFrameworkCopy, armFlutterFrameworkDirectory); final File fatFlutterFrameworkBinary = fatFlutterFrameworkCopy.childFile('Flutter');
copyDirectorySync(fatFlutterFrameworkCopy, armFlutterFrameworkDirectory);
// Create iOS framework.
List<String> lipoCommand = <String>['xcrun', 'lipo', fatFlutterFrameworkBinary.path, '-remove', 'x86_64', '-output', armFlutterFrameworkBinary.path]; // Create iOS framework.
List<String> lipoCommand = <String>['xcrun', 'lipo', fatFlutterFrameworkBinary.path, '-remove', 'x86_64', '-output', armFlutterFrameworkBinary.path];
await processUtils.run(
lipoCommand, RunResult lipoResult = processUtils.runSync(
workingDirectory: outputDirectory.path, lipoCommand,
allowReentrantFlutter: false, workingDirectory: outputDirectory.path,
); allowReentrantFlutter: false,
);
if (lipoResult.exitCode != 0) {
throwToolExit('Unable to create ARM engine framework: ${lipoResult.stderr}');
}
// Create simulator framework. // Create simulator framework.
final Directory simulatorFlutterFrameworkDirectory = simulatorBuildOutput.childDirectory(flutterFrameworkFileName); final Directory simulatorFlutterFrameworkDirectory = simulatorBuildOutput.childDirectory(flutterFrameworkFileName);
final File simulatorFlutterFrameworkBinary = simulatorFlutterFrameworkDirectory.childFile('Flutter'); final File simulatorFlutterFrameworkBinary = simulatorFlutterFrameworkDirectory.childFile('Flutter');
copyDirectorySync(fatFlutterFrameworkCopy, simulatorFlutterFrameworkDirectory); copyDirectorySync(fatFlutterFrameworkCopy, simulatorFlutterFrameworkDirectory);
lipoCommand = <String>['xcrun', 'lipo', fatFlutterFrameworkBinary.path, '-thin', 'x86_64', '-output', simulatorFlutterFrameworkBinary.path]; lipoCommand = <String>['xcrun', 'lipo', fatFlutterFrameworkBinary.path, '-thin', 'x86_64', '-output', simulatorFlutterFrameworkBinary.path];
await processUtils.run( lipoResult = processUtils.runSync(
lipoCommand, lipoCommand,
workingDirectory: outputDirectory.path, workingDirectory: outputDirectory.path,
allowReentrantFlutter: false, allowReentrantFlutter: false,
); );
// Create XCFramework from iOS and simulator frameworks. if (lipoResult.exitCode != 0) {
final List<String> xcframeworkCommand = <String>[ throwToolExit('Unable to create simulator engine framework: ${lipoResult.stderr}');
'xcrun', }
'xcodebuild',
'-create-xcframework',
'-framework', armFlutterFrameworkDirectory.path,
'-framework', simulatorFlutterFrameworkDirectory.path,
'-output', modeDirectory
.childFile('Flutter.xcframework')
.path
];
await processUtils.run( // Create XCFramework from iOS and simulator frameworks.
xcframeworkCommand, final List<String> xcframeworkCommand = <String>[
workingDirectory: outputDirectory.path, 'xcrun',
allowReentrantFlutter: false, 'xcodebuild',
); '-create-xcframework',
} '-framework', armFlutterFrameworkDirectory.path,
'-framework', simulatorFlutterFrameworkDirectory.path,
'-output', modeDirectory
.childFile('Flutter.xcframework')
.path
];
final RunResult xcframeworkResult = processUtils.runSync(
xcframeworkCommand,
workingDirectory: outputDirectory.path,
allowReentrantFlutter: false,
);
if (xcframeworkResult.exitCode != 0) {
throwToolExit('Unable to create engine XCFramework: ${xcframeworkResult.stderr}');
}
}
if (!boolArg('universal')) { if (!boolArg('universal')) {
fatFlutterFrameworkCopy.deleteSync(recursive: true); fatFlutterFrameworkCopy.deleteSync(recursive: true);
}
} finally {
status.stop();
} }
status.stop();
} }
Future<void> _produceAppFramework(BuildMode mode, Directory iPhoneBuildOutput, Directory simulatorBuildOutput, Directory modeDirectory) async { Future<void> _produceAppFramework(BuildMode mode, Directory iPhoneBuildOutput, Directory simulatorBuildOutput, Directory modeDirectory) async {
...@@ -261,8 +279,11 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { ...@@ -261,8 +279,11 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
if (mode == BuildMode.debug) { if (mode == BuildMode.debug) {
final Status status = logger.startProgress(' ├─Add placeholder App.framework for debug...', timeout: timeoutConfiguration.fastOperation); final Status status = logger.startProgress(' ├─Add placeholder App.framework for debug...', timeout: timeoutConfiguration.fastOperation);
await _produceStubAppFrameworkIfNeeded(mode, iPhoneBuildOutput, simulatorBuildOutput, destinationAppFrameworkDirectory); try {
status.stop(); await _produceStubAppFrameworkIfNeeded(mode, iPhoneBuildOutput, simulatorBuildOutput, destinationAppFrameworkDirectory);
} finally {
status.stop();
}
} else { } else {
await _produceAotAppFrameworkIfNeeded(mode, iPhoneBuildOutput, destinationAppFrameworkDirectory); await _produceAotAppFrameworkIfNeeded(mode, iPhoneBuildOutput, destinationAppFrameworkDirectory);
} }
...@@ -273,15 +294,18 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { ...@@ -273,15 +294,18 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
destinationInfoPlist.writeAsBytesSync(sourceInfoPlist.readAsBytesSync()); destinationInfoPlist.writeAsBytesSync(sourceInfoPlist.readAsBytesSync());
final Status status = logger.startProgress(' ├─Assembling Flutter resources for App.framework...', timeout: timeoutConfiguration.slowOperation); final Status status = logger.startProgress(' ├─Assembling Flutter resources for App.framework...', timeout: timeoutConfiguration.slowOperation);
await bundleBuilder.build( try {
platform: TargetPlatform.ios, await bundleBuilder.build(
buildMode: mode, platform: TargetPlatform.ios,
// Relative paths show noise in the compiler https://github.com/dart-lang/sdk/issues/37978. buildMode: mode,
mainPath: fs.path.absolute(targetFile), // Relative paths show noise in the compiler https://github.com/dart-lang/sdk/issues/37978.
assetDirPath: destinationAppFrameworkDirectory.childDirectory('flutter_assets').path, mainPath: fs.path.absolute(targetFile),
precompiledSnapshot: mode != BuildMode.debug, assetDirPath: destinationAppFrameworkDirectory.childDirectory('flutter_assets').path,
); precompiledSnapshot: mode != BuildMode.debug,
status.stop(); );
} finally {
status.stop();
}
} }
Future<void> _produceStubAppFrameworkIfNeeded(BuildMode mode, Directory iPhoneBuildOutput, Directory simulatorBuildOutput, Directory destinationAppFrameworkDirectory) async { Future<void> _produceStubAppFrameworkIfNeeded(BuildMode mode, Directory iPhoneBuildOutput, Directory simulatorBuildOutput, Directory destinationAppFrameworkDirectory) async {
...@@ -309,10 +333,14 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { ...@@ -309,10 +333,14 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
destinationAppFrameworkDirectory.childFile(binaryName).path destinationAppFrameworkDirectory.childFile(binaryName).path
]; ];
await processUtils.run( final RunResult lipoResult = processUtils.runSync(
lipoCommand, lipoCommand,
allowReentrantFlutter: false, allowReentrantFlutter: false,
); );
if (lipoResult.exitCode != 0) {
throwToolExit('Unable to create compiled dart universal framework: ${lipoResult.stderr}');
}
} }
Future<void> _produceAotAppFrameworkIfNeeded(BuildMode mode, Directory iPhoneBuildOutput, Directory destinationAppFrameworkDirectory) async { Future<void> _produceAotAppFrameworkIfNeeded(BuildMode mode, Directory iPhoneBuildOutput, Directory destinationAppFrameworkDirectory) async {
...@@ -320,22 +348,25 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { ...@@ -320,22 +348,25 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
return; return;
} }
final Status status = logger.startProgress(' ├─Building Dart AOT for App.framework...', timeout: timeoutConfiguration.slowOperation); final Status status = logger.startProgress(' ├─Building Dart AOT for App.framework...', timeout: timeoutConfiguration.slowOperation);
await aotBuilder.build( try {
platform: TargetPlatform.ios, await aotBuilder.build(
outputPath: iPhoneBuildOutput.path, platform: TargetPlatform.ios,
buildMode: mode, outputPath: iPhoneBuildOutput.path,
// Relative paths show noise in the compiler https://github.com/dart-lang/sdk/issues/37978. buildMode: mode,
mainDartFile: fs.path.absolute(targetFile), // Relative paths show noise in the compiler https://github.com/dart-lang/sdk/issues/37978.
quiet: true, mainDartFile: fs.path.absolute(targetFile),
bitcode: true, quiet: true,
reportTimings: false, bitcode: true,
iosBuildArchs: <DarwinArch>[DarwinArch.armv7, DarwinArch.arm64], reportTimings: false,
dartDefines: dartDefines, iosBuildArchs: <DarwinArch>[DarwinArch.armv7, DarwinArch.arm64],
); dartDefines: dartDefines,
);
const String appFrameworkName = 'App.framework'; const String appFrameworkName = 'App.framework';
copyDirectorySync(iPhoneBuildOutput.childDirectory(appFrameworkName), destinationAppFrameworkDirectory); copyDirectorySync(iPhoneBuildOutput.childDirectory(appFrameworkName), destinationAppFrameworkDirectory);
status.stop(); } finally {
status.stop();
}
} }
Future<void> _producePlugins( Future<void> _producePlugins(
...@@ -346,92 +377,111 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { ...@@ -346,92 +377,111 @@ class BuildIOSFrameworkCommand extends BuildSubCommand {
Directory outputDirectory, Directory outputDirectory,
) async { ) async {
final Status status = logger.startProgress(' ├─Building plugins...', timeout: timeoutConfiguration.slowOperation); final Status status = logger.startProgress(' ├─Building plugins...', timeout: timeoutConfiguration.slowOperation);
List<String> pluginsBuildCommand = <String>[ try {
'xcrun', List<String> pluginsBuildCommand = <String>[
'xcodebuild', 'xcrun',
'-alltargets', 'xcodebuild',
'-sdk', '-alltargets',
'iphoneos', '-sdk',
'-configuration', 'iphoneos',
xcodeBuildConfiguration, '-configuration',
'SYMROOT=${iPhoneBuildOutput.path}', xcodeBuildConfiguration,
'ONLY_ACTIVE_ARCH=NO' // No device targeted, so build all valid architectures. 'SYMROOT=${iPhoneBuildOutput.path}',
]; 'ONLY_ACTIVE_ARCH=NO' // No device targeted, so build all valid architectures.
];
await processUtils.run( RunResult buildPluginsResult = processUtils.runSync(
pluginsBuildCommand, pluginsBuildCommand,
workingDirectory: _project.ios.hostAppRoot.childDirectory('Pods').path, workingDirectory: _project.ios.hostAppRoot.childDirectory('Pods').path,
allowReentrantFlutter: false, allowReentrantFlutter: false,
); );
pluginsBuildCommand = <String>[ if (buildPluginsResult.exitCode != 0) {
'xcrun', throwToolExit('Unable to build plugin frameworks: ${buildPluginsResult.stderr}');
'xcodebuild', }
'-alltargets',
'-sdk',
'iphonesimulator',
'-configuration',
xcodeBuildConfiguration,
'SYMROOT=${simulatorBuildOutput.path}',
'ARCHS=x86_64',
'ONLY_ACTIVE_ARCH=NO' // No device targeted, so build all valid architectures.
];
await processUtils.run( pluginsBuildCommand = <String>[
pluginsBuildCommand, 'xcrun',
workingDirectory: _project.ios.hostAppRoot.childDirectory('Pods').path, 'xcodebuild',
allowReentrantFlutter: false, '-alltargets',
); '-sdk',
'iphonesimulator',
'-configuration',
xcodeBuildConfiguration,
'SYMROOT=${simulatorBuildOutput.path}',
'ARCHS=x86_64',
'ONLY_ACTIVE_ARCH=NO' // No device targeted, so build all valid architectures.
];
final Directory iPhoneBuildConfiguration = iPhoneBuildOutput.childDirectory('$xcodeBuildConfiguration-iphoneos'); buildPluginsResult = processUtils.runSync(
final Directory simulatorBuildConfiguration = simulatorBuildOutput.childDirectory('$xcodeBuildConfiguration-iphonesimulator'); pluginsBuildCommand,
workingDirectory: _project.ios.hostAppRoot.childDirectory('Pods').path,
for (Directory builtProduct in iPhoneBuildConfiguration.listSync(followLinks: false).whereType<Directory>()) { allowReentrantFlutter: false,
for (FileSystemEntity podProduct in builtProduct.listSync(followLinks: false)) { );
final String podFrameworkName = podProduct.basename;
if (fs.path.extension(podFrameworkName) == '.framework') {
final String binaryName = fs.path.basenameWithoutExtension(podFrameworkName);
if (boolArg('universal')) {
copyDirectorySync(podProduct as Directory, modeDirectory.childDirectory(podFrameworkName));
final List<String> lipoCommand = <String>[
'xcrun',
'lipo',
'-create',
fs.path.join(podProduct.path, binaryName),
simulatorBuildConfiguration.childDirectory(binaryName).childDirectory(podFrameworkName).childFile(binaryName).path,
'-output',
modeDirectory.childDirectory(podFrameworkName).childFile(binaryName).path
];
await processUtils.run(
lipoCommand,
workingDirectory: outputDirectory.path,
allowReentrantFlutter: false,
);
}
if (boolArg('xcframework')) { if (buildPluginsResult.exitCode != 0) {
final List<String> xcframeworkCommand = <String>[ throwToolExit('Unable to build plugin frameworks for simulator: ${buildPluginsResult.stderr}');
'xcrun', }
'xcodebuild',
'-create-xcframework', final Directory iPhoneBuildConfiguration = iPhoneBuildOutput.childDirectory('$xcodeBuildConfiguration-iphoneos');
'-framework', final Directory simulatorBuildConfiguration = simulatorBuildOutput.childDirectory('$xcodeBuildConfiguration-iphonesimulator');
podProduct.path,
'-framework', for (Directory builtProduct in iPhoneBuildConfiguration.listSync(followLinks: false).whereType<Directory>()) {
simulatorBuildConfiguration.childDirectory(binaryName).childDirectory(podFrameworkName).path, for (FileSystemEntity podProduct in builtProduct.listSync(followLinks: false)) {
'-output', final String podFrameworkName = podProduct.basename;
modeDirectory.childFile('$binaryName.xcframework').path if (fs.path.extension(podFrameworkName) == '.framework') {
]; final String binaryName = fs.path.basenameWithoutExtension(podFrameworkName);
if (boolArg('universal')) {
await processUtils.run( copyDirectorySync(podProduct as Directory, modeDirectory.childDirectory(podFrameworkName));
xcframeworkCommand, final List<String> lipoCommand = <String>[
workingDirectory: outputDirectory.path, 'xcrun',
allowReentrantFlutter: false, 'lipo',
); '-create',
fs.path.join(podProduct.path, binaryName),
simulatorBuildConfiguration.childDirectory(binaryName).childDirectory(podFrameworkName).childFile(binaryName).path,
'-output',
modeDirectory.childDirectory(podFrameworkName).childFile(binaryName).path
];
final RunResult pluginsLipoResult = processUtils.runSync(
lipoCommand,
workingDirectory: outputDirectory.path,
allowReentrantFlutter: false,
);
if (pluginsLipoResult.exitCode != 0) {
throwToolExit('Unable to create universal $binaryName.framework: ${buildPluginsResult.stderr}');
}
}
if (boolArg('xcframework')) {
final List<String> xcframeworkCommand = <String>[
'xcrun',
'xcodebuild',
'-create-xcframework',
'-framework',
podProduct.path,
'-framework',
simulatorBuildConfiguration.childDirectory(binaryName).childDirectory(podFrameworkName).path,
'-output',
modeDirectory.childFile('$binaryName.xcframework').path
];
final RunResult xcframeworkResult = processUtils.runSync(
xcframeworkCommand,
workingDirectory: outputDirectory.path,
allowReentrantFlutter: false,
);
if (xcframeworkResult.exitCode != 0) {
throwToolExit('Unable to create $binaryName.xcframework: ${xcframeworkResult.stderr}');
}
}
} }
} }
} }
} finally {
status.stop();
} }
status.stop();
} }
} }
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