[tools]build ipa validate app icon size (#115594)
* [tools]build ipa validate icon size * add more checks in case apple change the format, and also add device lab tests * do not depend on collection package
This commit is contained in:
parent
900b395451
commit
e438a12057
@ -17,6 +17,20 @@ Future<void> main() async {
|
|||||||
section('Archive');
|
section('Archive');
|
||||||
|
|
||||||
await inDirectory(flutterProject.rootPath, () async {
|
await inDirectory(flutterProject.rootPath, () async {
|
||||||
|
final File appIconFile = File(path.join(
|
||||||
|
flutterProject.rootPath,
|
||||||
|
'ios',
|
||||||
|
'Runner',
|
||||||
|
'Assets.xcassets',
|
||||||
|
'AppIcon.appiconset',
|
||||||
|
'Icon-App-20x20@1x.png',
|
||||||
|
));
|
||||||
|
// Resizes app icon to 123x456 (it is supposed to be 20x20).
|
||||||
|
appIconFile.writeAsBytesSync(appIconFile.readAsBytesSync()
|
||||||
|
..buffer.asByteData().setInt32(16, 123)
|
||||||
|
..buffer.asByteData().setInt32(20, 456)
|
||||||
|
);
|
||||||
|
|
||||||
final String output = await evalFlutter('build', options: <String>[
|
final String output = await evalFlutter('build', options: <String>[
|
||||||
'xcarchive',
|
'xcarchive',
|
||||||
'-v',
|
'-v',
|
||||||
@ -27,6 +41,15 @@ Future<void> main() async {
|
|||||||
if (!output.contains('Sending archive event if usage enabled')) {
|
if (!output.contains('Sending archive event if usage enabled')) {
|
||||||
throw TaskResult.failure('Usage archive event not sent');
|
throw TaskResult.failure('Usage archive event not sent');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!output.contains('Warning: App icon is using the wrong size (e.g. Icon-App-20x20@1x.png).')) {
|
||||||
|
throw TaskResult.failure('Must validate incorrect app icon image size.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The project is still using Flutter template icon.
|
||||||
|
if (!output.contains('Warning: App icon is set to the default placeholder icon. Replace with unique icons.')) {
|
||||||
|
throw TaskResult.failure('Must validate template app icon.');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
final String archivePath = path.join(
|
final String archivePath = path.join(
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
@ -55,6 +57,32 @@ class BuildIOSCommand extends _BuildIOSSubCommand {
|
|||||||
Directory _outputAppDirectory(String xcodeResultOutput) => globals.fs.directory(xcodeResultOutput).parent;
|
Directory _outputAppDirectory(String xcodeResultOutput) => globals.fs.directory(xcodeResultOutput).parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The key that uniquely identifies an image file in an app icon asset.
|
||||||
|
/// It consists of (idiom, size, scale).
|
||||||
|
@immutable
|
||||||
|
class _AppIconImageFileKey {
|
||||||
|
const _AppIconImageFileKey(this.idiom, this.size, this.scale);
|
||||||
|
|
||||||
|
/// The idiom (iphone or ipad).
|
||||||
|
final String idiom;
|
||||||
|
/// The logical size in point (e.g. 83.5).
|
||||||
|
final double size;
|
||||||
|
/// The scale factor (e.g. 2).
|
||||||
|
final int scale;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(idiom, size, scale);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => other is _AppIconImageFileKey
|
||||||
|
&& other.idiom == idiom
|
||||||
|
&& other.size == size
|
||||||
|
&& other.scale == scale;
|
||||||
|
|
||||||
|
/// The pixel size.
|
||||||
|
int get pixelSize => (size * scale).toInt(); // pixel size must be an int.
|
||||||
|
}
|
||||||
|
|
||||||
/// Builds an .xcarchive and optionally .ipa for an iOS app to be generated for
|
/// Builds an .xcarchive and optionally .ipa for an iOS app to be generated for
|
||||||
/// App Store submission.
|
/// App Store submission.
|
||||||
///
|
///
|
||||||
@ -131,18 +159,22 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
|
|||||||
return super.validateCommand();
|
return super.validateCommand();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses Contents.json into a map, with the key to be the combination of (idiom, size, scale), and value to be the icon image file name.
|
// Parses Contents.json into a map, with the key to be _AppIconImageFileKey, and value to be the icon image file name.
|
||||||
Map<String, String> _parseIconContentsJson(String contentsJsonDirName) {
|
Map<_AppIconImageFileKey, String> _parseIconContentsJson(String contentsJsonDirName) {
|
||||||
final Directory contentsJsonDirectory = globals.fs.directory(contentsJsonDirName);
|
final Directory contentsJsonDirectory = globals.fs.directory(contentsJsonDirName);
|
||||||
if (!contentsJsonDirectory.existsSync()) {
|
if (!contentsJsonDirectory.existsSync()) {
|
||||||
return <String, String>{};
|
return <_AppIconImageFileKey, String>{};
|
||||||
}
|
}
|
||||||
final File contentsJsonFile = contentsJsonDirectory.childFile('Contents.json');
|
final File contentsJsonFile = contentsJsonDirectory.childFile('Contents.json');
|
||||||
final Map<String, dynamic> content = json.decode(contentsJsonFile.readAsStringSync()) as Map<String, dynamic>;
|
final Map<String, dynamic> contents = json.decode(contentsJsonFile.readAsStringSync()) as Map<String, dynamic>? ?? <String, dynamic>{};
|
||||||
final List<dynamic> images = content['images'] as List<dynamic>? ?? <dynamic>[];
|
final List<dynamic> images = contents['images'] as List<dynamic>? ?? <dynamic>[];
|
||||||
|
final Map<String, dynamic> info = contents['info'] as Map<String, dynamic>? ?? <String, dynamic>{};
|
||||||
final Map<String, String> iconInfo = <String, String>{};
|
if ((info['version'] as int?) != 1) {
|
||||||
|
// Skips validation for unknown format.
|
||||||
|
return <_AppIconImageFileKey, String>{};
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<_AppIconImageFileKey, String> iconInfo = <_AppIconImageFileKey, String>{};
|
||||||
for (final dynamic image in images) {
|
for (final dynamic image in images) {
|
||||||
final Map<String, dynamic> imageMap = image as Map<String, dynamic>;
|
final Map<String, dynamic> imageMap = image as Map<String, dynamic>;
|
||||||
final String? idiom = imageMap['idiom'] as String?;
|
final String? idiom = imageMap['idiom'] as String?;
|
||||||
@ -150,9 +182,29 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
|
|||||||
final String? scale = imageMap['scale'] as String?;
|
final String? scale = imageMap['scale'] as String?;
|
||||||
final String? fileName = imageMap['filename'] as String?;
|
final String? fileName = imageMap['filename'] as String?;
|
||||||
|
|
||||||
if (size != null && idiom != null && scale != null && fileName != null) {
|
if (size == null || idiom == null || scale == null || fileName == null) {
|
||||||
iconInfo['$idiom $size $scale'] = fileName;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for example, "64x64". Parse the width since it is a square.
|
||||||
|
final Iterable<double> parsedSizes = size.split('x')
|
||||||
|
.map((String element) => double.tryParse(element))
|
||||||
|
.whereType<double>();
|
||||||
|
if (parsedSizes.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final double parsedSize = parsedSizes.first;
|
||||||
|
|
||||||
|
// for example, "3x".
|
||||||
|
final Iterable<int> parsedScales = scale.split('x')
|
||||||
|
.map((String element) => int.tryParse(element))
|
||||||
|
.whereType<int>();
|
||||||
|
if (parsedScales.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final int parsedScale = parsedScales.first;
|
||||||
|
|
||||||
|
iconInfo[_AppIconImageFileKey(idiom, parsedSize, parsedScale)] = fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return iconInfo;
|
return iconInfo;
|
||||||
@ -162,29 +214,51 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
|
|||||||
final BuildableIOSApp app = await buildableIOSApp;
|
final BuildableIOSApp app = await buildableIOSApp;
|
||||||
final String templateIconImageDirName = await app.templateAppIconDirNameForImages;
|
final String templateIconImageDirName = await app.templateAppIconDirNameForImages;
|
||||||
|
|
||||||
final Map<String, String> templateIconMap = _parseIconContentsJson(app.templateAppIconDirNameForContentsJson);
|
final Map<_AppIconImageFileKey, String> templateIconMap = _parseIconContentsJson(app.templateAppIconDirNameForContentsJson);
|
||||||
final Map<String, String> projectIconMap = _parseIconContentsJson(app.projectAppIconDirName);
|
final Map<_AppIconImageFileKey, String> projectIconMap = _parseIconContentsJson(app.projectAppIconDirName);
|
||||||
|
|
||||||
// find if any of the project icons conflict with template icons
|
// validate each of the project icon images.
|
||||||
final bool hasConflict = projectIconMap.entries
|
final List<String> filesWithTemplateIcon = <String>[];
|
||||||
.where((MapEntry<String, String> entry) {
|
final List<String> filesWithWrongSize = <String>[];
|
||||||
|
for (final MapEntry<_AppIconImageFileKey, String> entry in projectIconMap.entries) {
|
||||||
final String projectIconFileName = entry.value;
|
final String projectIconFileName = entry.value;
|
||||||
final String? templateIconFileName = templateIconMap[entry.key];
|
final String? templateIconFileName = templateIconMap[entry.key];
|
||||||
if (templateIconFileName == null) {
|
final File projectIconFile = globals.fs.file(globals.fs.path.join(app.projectAppIconDirName, projectIconFileName));
|
||||||
return false;
|
if (!projectIconFile.existsSync()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final Uint8List projectIconBytes = projectIconFile.readAsBytesSync();
|
||||||
|
|
||||||
|
// validate conflict with template icon file.
|
||||||
|
if (templateIconFileName != null) {
|
||||||
|
final File templateIconFile = globals.fs.file(globals.fs.path.join(
|
||||||
|
templateIconImageDirName, templateIconFileName));
|
||||||
|
if (templateIconFile.existsSync() && md5.convert(projectIconBytes) ==
|
||||||
|
md5.convert(templateIconFile.readAsBytesSync())) {
|
||||||
|
filesWithTemplateIcon.add(entry.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final File projectIconFile = globals.fs.file(globals.fs.path.join(app.projectAppIconDirName, projectIconFileName));
|
// validate image size is correct.
|
||||||
final File templateIconFile = globals.fs.file(globals.fs.path.join(templateIconImageDirName, templateIconFileName));
|
// PNG file's width is at byte [16, 20), and height is at byte [20, 24), in big endian format.
|
||||||
return projectIconFile.existsSync()
|
// Based on https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_format
|
||||||
&& templateIconFile.existsSync()
|
final ByteData projectIconData = projectIconBytes.buffer.asByteData();
|
||||||
&& md5.convert(projectIconFile.readAsBytesSync()) == md5.convert(templateIconFile.readAsBytesSync());
|
if (projectIconData.lengthInBytes < 24) {
|
||||||
})
|
continue;
|
||||||
.isNotEmpty;
|
}
|
||||||
|
final int width = projectIconData.getInt32(16);
|
||||||
|
final int height = projectIconData.getInt32(20);
|
||||||
|
if (width != entry.key.pixelSize || height != entry.key.pixelSize) {
|
||||||
|
filesWithWrongSize.add(entry.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hasConflict) {
|
if (filesWithTemplateIcon.isNotEmpty) {
|
||||||
messageBuffer.writeln('\nWarning: App icon is set to the default placeholder icon. Replace with unique icons.');
|
messageBuffer.writeln('\nWarning: App icon is set to the default placeholder icon. Replace with unique icons.');
|
||||||
}
|
}
|
||||||
|
if (filesWithWrongSize.isNotEmpty) {
|
||||||
|
messageBuffer.writeln('\nWarning: App icon is using the wrong size (e.g. ${filesWithWrongSize.first}).');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _validateXcodeBuildSettingsAfterArchive(StringBuffer messageBuffer) async {
|
Future<void> _validateXcodeBuildSettingsAfterArchive(StringBuffer messageBuffer) async {
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:args/command_runner.dart';
|
import 'package:args/command_runner.dart';
|
||||||
import 'package:file/memory.dart';
|
import 'package:file/memory.dart';
|
||||||
import 'package:flutter_tools/src/base/file_system.dart';
|
import 'package:flutter_tools/src/base/file_system.dart';
|
||||||
@ -1010,7 +1012,11 @@ void main() {
|
|||||||
"filename": "Icon-App-20x20@2x.png",
|
"filename": "Icon-App-20x20@2x.png",
|
||||||
"scale": "2x"
|
"scale": "2x"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"info": {
|
||||||
|
"version": 1,
|
||||||
|
"author": "xcode"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
''');
|
''');
|
||||||
fileSystem.file(templateIconImagePath)
|
fileSystem.file(templateIconImagePath)
|
||||||
@ -1028,7 +1034,11 @@ void main() {
|
|||||||
"filename": "Icon-App-20x20@2x.png",
|
"filename": "Icon-App-20x20@2x.png",
|
||||||
"scale": "2x"
|
"scale": "2x"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"info": {
|
||||||
|
"version": 1,
|
||||||
|
"author": "xcode"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
''');
|
''');
|
||||||
fileSystem.file(projectIconImagePath)
|
fileSystem.file(projectIconImagePath)
|
||||||
@ -1081,7 +1091,11 @@ void main() {
|
|||||||
"filename": "Icon-App-20x20@2x.png",
|
"filename": "Icon-App-20x20@2x.png",
|
||||||
"scale": "2x"
|
"scale": "2x"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"info": {
|
||||||
|
"version": 1,
|
||||||
|
"author": "xcode"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
''');
|
''');
|
||||||
fileSystem.file(templateIconImagePath)
|
fileSystem.file(templateIconImagePath)
|
||||||
@ -1099,7 +1113,11 @@ void main() {
|
|||||||
"filename": "Icon-App-20x20@2x.png",
|
"filename": "Icon-App-20x20@2x.png",
|
||||||
"scale": "2x"
|
"scale": "2x"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"info": {
|
||||||
|
"version": 1,
|
||||||
|
"author": "xcode"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
''');
|
''');
|
||||||
fileSystem.file(projectIconImagePath)
|
fileSystem.file(projectIconImagePath)
|
||||||
@ -1128,6 +1146,336 @@ void main() {
|
|||||||
Platform: () => macosPlatform,
|
Platform: () => macosPlatform,
|
||||||
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
|
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testUsingContext('Validate app icon using the wrong width', () async {
|
||||||
|
const String projectIconContentsJsonPath = 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json';
|
||||||
|
const String projectIconImagePath = 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png';
|
||||||
|
|
||||||
|
fakeProcessManager.addCommands(<FakeCommand>[
|
||||||
|
xattrCommand,
|
||||||
|
setUpFakeXcodeBuildHandler(onRun: () {
|
||||||
|
fileSystem.file(projectIconContentsJsonPath)
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsStringSync('''
|
||||||
|
{
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"size": "20x20",
|
||||||
|
"idiom": "iphone",
|
||||||
|
"filename": "Icon-App-20x20@2x.png",
|
||||||
|
"scale": "2x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info": {
|
||||||
|
"version": 1,
|
||||||
|
"author": "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
fileSystem.file(projectIconImagePath)
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsBytes(Uint8List(16))
|
||||||
|
// set width to 1 pixel
|
||||||
|
..writeAsBytes(Uint8List(4)..buffer.asByteData().setInt32(0, 1), mode: FileMode.append)
|
||||||
|
// set height to 40 pixels
|
||||||
|
..writeAsBytes(Uint8List(4)..buffer.asByteData().setInt32(0, 40), mode: FileMode.append);
|
||||||
|
}),
|
||||||
|
exportArchiveCommand(exportOptionsPlist: _exportOptionsPlist),
|
||||||
|
]);
|
||||||
|
|
||||||
|
createMinimalMockProjectFiles();
|
||||||
|
|
||||||
|
final BuildCommand command = BuildCommand(
|
||||||
|
androidSdk: FakeAndroidSdk(),
|
||||||
|
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
|
||||||
|
fileSystem: MemoryFileSystem.test(),
|
||||||
|
logger: BufferLogger.test(),
|
||||||
|
osUtils: FakeOperatingSystemUtils(),
|
||||||
|
);
|
||||||
|
await createTestCommandRunner(command).run(
|
||||||
|
<String>['build', 'ipa', '--no-pub']);
|
||||||
|
|
||||||
|
expect(testLogger.statusText, contains('Warning: App icon is using the wrong size (e.g. Icon-App-20x20@2x.png).'));
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
FileSystem: () => fileSystem,
|
||||||
|
ProcessManager: () => fakeProcessManager,
|
||||||
|
Platform: () => macosPlatform,
|
||||||
|
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('Validate app icon using the wrong height', () async {
|
||||||
|
const String projectIconContentsJsonPath = 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json';
|
||||||
|
const String projectIconImagePath = 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png';
|
||||||
|
|
||||||
|
fakeProcessManager.addCommands(<FakeCommand>[
|
||||||
|
xattrCommand,
|
||||||
|
setUpFakeXcodeBuildHandler(onRun: () {
|
||||||
|
fileSystem.file(projectIconContentsJsonPath)
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsStringSync('''
|
||||||
|
{
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"size": "20x20",
|
||||||
|
"idiom": "iphone",
|
||||||
|
"filename": "Icon-App-20x20@2x.png",
|
||||||
|
"scale": "2x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info": {
|
||||||
|
"version": 1,
|
||||||
|
"author": "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
fileSystem.file(projectIconImagePath)
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsBytes(Uint8List(16))
|
||||||
|
// set width to 40 pixels
|
||||||
|
..writeAsBytes(Uint8List(4)..buffer.asByteData().setInt32(0, 40), mode: FileMode.append)
|
||||||
|
// set height to 1 pixel
|
||||||
|
..writeAsBytes(Uint8List(4)..buffer.asByteData().setInt32(0, 1), mode: FileMode.append);
|
||||||
|
}),
|
||||||
|
exportArchiveCommand(exportOptionsPlist: _exportOptionsPlist),
|
||||||
|
]);
|
||||||
|
|
||||||
|
createMinimalMockProjectFiles();
|
||||||
|
|
||||||
|
final BuildCommand command = BuildCommand(
|
||||||
|
androidSdk: FakeAndroidSdk(),
|
||||||
|
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
|
||||||
|
fileSystem: MemoryFileSystem.test(),
|
||||||
|
logger: BufferLogger.test(),
|
||||||
|
osUtils: FakeOperatingSystemUtils(),
|
||||||
|
);
|
||||||
|
await createTestCommandRunner(command).run(
|
||||||
|
<String>['build', 'ipa', '--no-pub']);
|
||||||
|
|
||||||
|
expect(testLogger.statusText, contains('Warning: App icon is using the wrong size (e.g. Icon-App-20x20@2x.png).'));
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
FileSystem: () => fileSystem,
|
||||||
|
ProcessManager: () => fakeProcessManager,
|
||||||
|
Platform: () => macosPlatform,
|
||||||
|
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('Validate app icon using the correct width and height', () async {
|
||||||
|
const String projectIconContentsJsonPath = 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json';
|
||||||
|
const String projectIconImagePath = 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png';
|
||||||
|
|
||||||
|
fakeProcessManager.addCommands(<FakeCommand>[
|
||||||
|
xattrCommand,
|
||||||
|
setUpFakeXcodeBuildHandler(onRun: () {
|
||||||
|
fileSystem.file(projectIconContentsJsonPath)
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsStringSync('''
|
||||||
|
{
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"size": "20x20",
|
||||||
|
"idiom": "iphone",
|
||||||
|
"filename": "Icon-App-20x20@2x.png",
|
||||||
|
"scale": "2x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info": {
|
||||||
|
"version": 1,
|
||||||
|
"author": "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
fileSystem.file(projectIconImagePath)
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsBytes(Uint8List(16))
|
||||||
|
// set width to 40 pixels
|
||||||
|
..writeAsBytes(Uint8List(4)..buffer.asByteData().setInt32(0, 40), mode: FileMode.append)
|
||||||
|
// set height to 40 pixel
|
||||||
|
..writeAsBytes(Uint8List(4)..buffer.asByteData().setInt32(0, 40), mode: FileMode.append);
|
||||||
|
}),
|
||||||
|
exportArchiveCommand(exportOptionsPlist: _exportOptionsPlist),
|
||||||
|
]);
|
||||||
|
|
||||||
|
createMinimalMockProjectFiles();
|
||||||
|
|
||||||
|
final BuildCommand command = BuildCommand(
|
||||||
|
androidSdk: FakeAndroidSdk(),
|
||||||
|
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
|
||||||
|
fileSystem: MemoryFileSystem.test(),
|
||||||
|
logger: BufferLogger.test(),
|
||||||
|
osUtils: FakeOperatingSystemUtils(),
|
||||||
|
);
|
||||||
|
await createTestCommandRunner(command).run(
|
||||||
|
<String>['build', 'ipa', '--no-pub']);
|
||||||
|
|
||||||
|
expect(testLogger.statusText, isNot(contains('Warning: App icon is using the wrong size (e.g. Icon-App-20x20@2x.png).')));
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
FileSystem: () => fileSystem,
|
||||||
|
ProcessManager: () => fakeProcessManager,
|
||||||
|
Platform: () => macosPlatform,
|
||||||
|
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('Validate app icon should skip validation for unknown format version', () async {
|
||||||
|
const String projectIconContentsJsonPath = 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json';
|
||||||
|
const String projectIconImagePath = 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png';
|
||||||
|
|
||||||
|
fakeProcessManager.addCommands(<FakeCommand>[
|
||||||
|
xattrCommand,
|
||||||
|
setUpFakeXcodeBuildHandler(onRun: () {
|
||||||
|
// Uses unknown format version 123.
|
||||||
|
fileSystem.file(projectIconContentsJsonPath)
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsStringSync('''
|
||||||
|
{
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"size": "20x20",
|
||||||
|
"idiom": "iphone",
|
||||||
|
"filename": "Icon-App-20x20@2x.png",
|
||||||
|
"scale": "2x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info": {
|
||||||
|
"version": 123,
|
||||||
|
"author": "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
fileSystem.file(projectIconImagePath)
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsBytes(Uint8List(16))
|
||||||
|
// set width to 1 pixel
|
||||||
|
..writeAsBytes(Uint8List(4)..buffer.asByteData().setInt32(0, 1), mode: FileMode.append)
|
||||||
|
// set height to 1 pixel
|
||||||
|
..writeAsBytes(Uint8List(4)..buffer.asByteData().setInt32(0, 1), mode: FileMode.append);
|
||||||
|
}),
|
||||||
|
exportArchiveCommand(exportOptionsPlist: _exportOptionsPlist),
|
||||||
|
]);
|
||||||
|
|
||||||
|
createMinimalMockProjectFiles();
|
||||||
|
|
||||||
|
final BuildCommand command = BuildCommand(
|
||||||
|
androidSdk: FakeAndroidSdk(),
|
||||||
|
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
|
||||||
|
fileSystem: MemoryFileSystem.test(),
|
||||||
|
logger: BufferLogger.test(),
|
||||||
|
osUtils: FakeOperatingSystemUtils(),
|
||||||
|
);
|
||||||
|
await createTestCommandRunner(command).run(
|
||||||
|
<String>['build', 'ipa', '--no-pub']);
|
||||||
|
|
||||||
|
// The validation should be skipped, even when the icon size is incorrect.
|
||||||
|
expect(testLogger.statusText, isNot(contains('Warning: App icon is using the wrong size (e.g. Icon-App-20x20@2x.png).')));
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
FileSystem: () => fileSystem,
|
||||||
|
ProcessManager: () => fakeProcessManager,
|
||||||
|
Platform: () => macosPlatform,
|
||||||
|
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('Validate app icon should skip validation of an icon image if invalid format', () async {
|
||||||
|
const String projectIconContentsJsonPath = 'ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json';
|
||||||
|
final List<String> imageFileNames = <String>[
|
||||||
|
'Icon-App-20x20@1x.png',
|
||||||
|
'Icon-App-20x20@2x.png',
|
||||||
|
'Icon-App-20x20@3x.png',
|
||||||
|
'Icon-App-29x29@1x.png',
|
||||||
|
'Icon-App-29x29@2x.png',
|
||||||
|
'Icon-App-29x29@3x.png',
|
||||||
|
];
|
||||||
|
|
||||||
|
fakeProcessManager.addCommands(<FakeCommand>[
|
||||||
|
xattrCommand,
|
||||||
|
setUpFakeXcodeBuildHandler(onRun: () {
|
||||||
|
// The following json contains examples of:
|
||||||
|
// - invalid size
|
||||||
|
// - invalid scale
|
||||||
|
// - missing size
|
||||||
|
// - missing idiom
|
||||||
|
// - missing filename
|
||||||
|
// - missing scale
|
||||||
|
fileSystem.file(projectIconContentsJsonPath)
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsStringSync('''
|
||||||
|
{
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"size": "20*20",
|
||||||
|
"idiom": "iphone",
|
||||||
|
"filename": "Icon-App-20x20@2x.png",
|
||||||
|
"scale": "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "20x20",
|
||||||
|
"idiom": "iphone",
|
||||||
|
"filename": "Icon-App-20x20@2x.png",
|
||||||
|
"scale": "2@"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "iphone",
|
||||||
|
"filename": "Icon-App-20x20@3x.png",
|
||||||
|
"scale": "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "29x29",
|
||||||
|
"filename": "Icon-App-29x29@1x.png",
|
||||||
|
"scale": "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "29x29",
|
||||||
|
"idiom": "iphone",
|
||||||
|
"scale": "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size": "29x29",
|
||||||
|
"idiom": "iphone",
|
||||||
|
"filename": "Icon-App-20x20@3x.png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info": {
|
||||||
|
"version": 1,
|
||||||
|
"author": "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
|
||||||
|
// Resize all related images to 1x1.
|
||||||
|
for (final String imageFileName in imageFileNames) {
|
||||||
|
fileSystem.file('ios/Runner/Assets.xcassets/AppIcon.appiconset/$imageFileName')
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsBytes(Uint8List(16))
|
||||||
|
// set width to 1 pixel
|
||||||
|
..writeAsBytes(Uint8List(4)..buffer.asByteData().setInt32(0, 1), mode: FileMode.append)
|
||||||
|
// set height to 1 pixel
|
||||||
|
..writeAsBytes(Uint8List(4)..buffer.asByteData().setInt32(0, 1), mode: FileMode.append);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
exportArchiveCommand(exportOptionsPlist: _exportOptionsPlist),
|
||||||
|
]);
|
||||||
|
|
||||||
|
createMinimalMockProjectFiles();
|
||||||
|
|
||||||
|
final BuildCommand command = BuildCommand(
|
||||||
|
androidSdk: FakeAndroidSdk(),
|
||||||
|
buildSystem: TestBuildSystem.all(BuildResult(success: true)),
|
||||||
|
fileSystem: MemoryFileSystem.test(),
|
||||||
|
logger: BufferLogger.test(),
|
||||||
|
osUtils: FakeOperatingSystemUtils(),
|
||||||
|
);
|
||||||
|
await createTestCommandRunner(command).run(
|
||||||
|
<String>['build', 'ipa', '--no-pub']);
|
||||||
|
|
||||||
|
// The validation should be skipped, even when the image size is incorrect.
|
||||||
|
for (final String imageFileName in imageFileNames) {
|
||||||
|
expect(testLogger.statusText, isNot(contains(
|
||||||
|
'Warning: App icon is using the wrong size (e.g. $imageFileName).')));
|
||||||
|
}
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
FileSystem: () => fileSystem,
|
||||||
|
ProcessManager: () => fakeProcessManager,
|
||||||
|
Platform: () => macosPlatform,
|
||||||
|
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user