Unverified Commit dcae4247 authored by hellohuanlin's avatar hellohuanlin Committed by GitHub

[tools]build ipa validate template icon files (#114841)

* [tools]build ipa validate template icon files

* use the same box for both validations, and added some unit test, and some nits

* add unit test for templateImageDirectory

* use fs.path.join intead of raw path

* use the correct filesystem

* lint

* use absolute path for flutter_template_images

* fix rebase

* update indentation
parent b5345ff5
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// 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 'package:crypto/crypto.dart';
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
...@@ -130,7 +131,63 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { ...@@ -130,7 +131,63 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
return super.validateCommand(); return super.validateCommand();
} }
Future<void> _validateXcodeBuildSettingsAfterArchive() async { // 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.
Map<String, String> _parseIconContentsJson(String contentsJsonDirName) {
final Directory contentsJsonDirectory = globals.fs.directory(contentsJsonDirName);
if (!contentsJsonDirectory.existsSync()) {
return <String, String>{};
}
final File contentsJsonFile = contentsJsonDirectory.childFile('Contents.json');
final Map<String, dynamic> content = json.decode(contentsJsonFile.readAsStringSync()) as Map<String, dynamic>;
final List<dynamic> images = content['images'] as List<dynamic>? ?? <dynamic>[];
final Map<String, String> iconInfo = <String, String>{};
for (final dynamic image in images) {
final Map<String, dynamic> imageMap = image as Map<String, dynamic>;
final String? idiom = imageMap['idiom'] as String?;
final String? size = imageMap['size'] as String?;
final String? scale = imageMap['scale'] as String?;
final String? fileName = imageMap['filename'] as String?;
if (size != null && idiom != null && scale != null && fileName != null) {
iconInfo['$idiom $size $scale'] = fileName;
}
}
return iconInfo;
}
Future<void> _validateIconsAfterArchive(StringBuffer messageBuffer) async {
final BuildableIOSApp app = await buildableIOSApp;
final String templateIconImageDirName = await app.templateAppIconDirNameForImages;
final Map<String, String> templateIconMap = _parseIconContentsJson(app.templateAppIconDirNameForContentsJson);
final Map<String, String> projectIconMap = _parseIconContentsJson(app.projectAppIconDirName);
// find if any of the project icons conflict with template icons
final bool hasConflict = projectIconMap.entries
.where((MapEntry<String, String> entry) {
final String projectIconFileName = entry.value;
final String? templateIconFileName = templateIconMap[entry.key];
if (templateIconFileName == null) {
return false;
}
final File projectIconFile = globals.fs.file(globals.fs.path.join(app.projectAppIconDirName, projectIconFileName));
final File templateIconFile = globals.fs.file(globals.fs.path.join(templateIconImageDirName, templateIconFileName));
return projectIconFile.existsSync()
&& templateIconFile.existsSync()
&& md5.convert(projectIconFile.readAsBytesSync()) == md5.convert(templateIconFile.readAsBytesSync());
})
.isNotEmpty;
if (hasConflict) {
messageBuffer.writeln('\nWarning: App icon is set to the default placeholder icon. Replace with unique icons.');
}
}
Future<void> _validateXcodeBuildSettingsAfterArchive(StringBuffer messageBuffer) async {
final BuildableIOSApp app = await buildableIOSApp; final BuildableIOSApp app = await buildableIOSApp;
final String plistPath = app.builtInfoPlistPathAfterArchive; final String plistPath = app.builtInfoPlistPathAfterArchive;
...@@ -148,21 +205,13 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { ...@@ -148,21 +205,13 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
xcodeProjectSettingsMap['Deployment Target'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kMinimumOSVersionKey); xcodeProjectSettingsMap['Deployment Target'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kMinimumOSVersionKey);
xcodeProjectSettingsMap['Bundle Identifier'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey); xcodeProjectSettingsMap['Bundle Identifier'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey);
final StringBuffer buffer = StringBuffer();
xcodeProjectSettingsMap.forEach((String title, String? info) { xcodeProjectSettingsMap.forEach((String title, String? info) {
buffer.writeln('$title: ${info ?? "Missing"}'); messageBuffer.writeln('$title: ${info ?? "Missing"}');
}); });
final String message;
if (xcodeProjectSettingsMap.values.any((String? element) => element == null)) { if (xcodeProjectSettingsMap.values.any((String? element) => element == null)) {
buffer.writeln('\nYou must set up the missing settings'); messageBuffer.writeln('\nYou must set up the missing settings.');
buffer.write('Instructions: https://docs.flutter.dev/deployment/ios');
message = buffer.toString();
} else {
// remove the new line
message = buffer.toString().trim();
} }
globals.printBox(message, title: 'App Settings');
} }
@override @override
...@@ -171,7 +220,11 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { ...@@ -171,7 +220,11 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
displayNullSafetyMode(buildInfo); displayNullSafetyMode(buildInfo);
final FlutterCommandResult xcarchiveResult = await super.runCommand(); final FlutterCommandResult xcarchiveResult = await super.runCommand();
await _validateXcodeBuildSettingsAfterArchive(); final StringBuffer validationMessageBuffer = StringBuffer();
await _validateXcodeBuildSettingsAfterArchive(validationMessageBuffer);
await _validateIconsAfterArchive(validationMessageBuffer);
validationMessageBuffer.write('\nTo update the settings, please refer to https://docs.flutter.dev/deployment/ios');
globals.printBox(validationMessageBuffer.toString(), title: 'App Settings');
// xcarchive failed or not at expected location. // xcarchive failed or not at expected location.
if (xcarchiveResult.exitStatus != ExitStatus.success) { if (xcarchiveResult.exitStatus != ExitStatus.success) {
......
...@@ -5,7 +5,9 @@ ...@@ -5,7 +5,9 @@
import '../application_package.dart'; import '../application_package.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../cache.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import '../template.dart';
import '../xcode_project.dart'; import '../xcode_project.dart';
import 'plist_parser.dart'; import 'plist_parser.dart';
...@@ -151,12 +153,42 @@ class BuildableIOSApp extends IOSApp { ...@@ -151,12 +153,42 @@ class BuildableIOSApp extends IOSApp {
_hostAppBundleName == null ? 'Runner.app' : _hostAppBundleName!, _hostAppBundleName == null ? 'Runner.app' : _hostAppBundleName!,
'Info.plist'); 'Info.plist');
// Both project icon's image assets and Contents.json are in the same directory.
String get projectAppIconDirName => globals.fs.path.join('ios', _appIconDirNameSuffix);
// template icon's Contents.json is in flutter_tools.
String get templateAppIconDirNameForContentsJson => globals.fs.path.join(
Cache.flutterRoot!,
'packages',
'flutter_tools',
'templates',
'app_shared',
'ios.tmpl',
_appIconDirNameSuffix,
);
// template icon's image assets are in flutter_template_images package.
Future<String> get templateAppIconDirNameForImages async {
final Directory imageTemplate = await templateImageDirectory(null, globals.fs, globals.logger);
return globals.fs.path.join(
imageTemplate.path,
'app_shared',
'ios.tmpl',
_appIconDirNameSuffix,
);
}
String get ipaOutputPath => String get ipaOutputPath =>
globals.fs.path.join(getIosBuildDirectory(), 'ipa'); globals.fs.path.join(getIosBuildDirectory(), 'ipa');
String _buildAppPath(String type) { String _buildAppPath(String type) {
return globals.fs.path.join(getIosBuildDirectory(), type, _hostAppBundleName); return globals.fs.path.join(getIosBuildDirectory(), type, _hostAppBundleName);
} }
String get _appIconDirNameSuffix => globals.fs.path.join(
'Runner',
'Assets.xcassets',
'AppIcon.appiconset');
} }
class PrebuiltIOSApp extends IOSApp implements PrebuiltApplicationPackage { class PrebuiltIOSApp extends IOSApp implements PrebuiltApplicationPackage {
......
...@@ -101,7 +101,7 @@ class Template { ...@@ -101,7 +101,7 @@ class Template {
}) async { }) async {
// All named templates are placed in the 'templates' directory // All named templates are placed in the 'templates' directory
final Directory templateDir = _templateDirectoryInPackage(name, fileSystem); final Directory templateDir = _templateDirectoryInPackage(name, fileSystem);
final Directory imageDir = await _templateImageDirectory(name, fileSystem, logger); final Directory imageDir = await templateImageDirectory(name, fileSystem, logger);
return Template._( return Template._(
<Directory>[templateDir], <Directory>[templateDir],
<Directory>[imageDir], <Directory>[imageDir],
...@@ -126,8 +126,8 @@ class Template { ...@@ -126,8 +126,8 @@ class Template {
], ],
<Directory>[ <Directory>[
for (final String name in names) for (final String name in names)
if ((await _templateImageDirectory(name, fileSystem, logger)).existsSync()) if ((await templateImageDirectory(name, fileSystem, logger)).existsSync())
await _templateImageDirectory(name, fileSystem, logger), await templateImageDirectory(name, fileSystem, logger),
], ],
fileSystem: fileSystem, fileSystem: fileSystem,
logger: logger, logger: logger,
...@@ -350,9 +350,10 @@ Directory _templateDirectoryInPackage(String name, FileSystem fileSystem) { ...@@ -350,9 +350,10 @@ Directory _templateDirectoryInPackage(String name, FileSystem fileSystem) {
return fileSystem.directory(fileSystem.path.join(templatesDir, name)); return fileSystem.directory(fileSystem.path.join(templatesDir, name));
} }
// Returns the directory containing the 'name' template directory in /// Returns the directory containing the 'name' template directory in
// flutter_template_images, to resolve image placeholder against. /// flutter_template_images, to resolve image placeholder against.
Future<Directory> _templateImageDirectory(String name, FileSystem fileSystem, Logger logger) async { /// if 'name' is null, return the parent template directory.
Future<Directory> templateImageDirectory(String? name, FileSystem fileSystem, Logger logger) async {
final String toolPackagePath = fileSystem.path.join( final String toolPackagePath = fileSystem.path.join(
Cache.flutterRoot!, 'packages', 'flutter_tools'); Cache.flutterRoot!, 'packages', 'flutter_tools');
final String packageFilePath = fileSystem.path.join(toolPackagePath, '.dart_tool', 'package_config.json'); final String packageFilePath = fileSystem.path.join(toolPackagePath, '.dart_tool', 'package_config.json');
...@@ -361,10 +362,10 @@ Future<Directory> _templateImageDirectory(String name, FileSystem fileSystem, Lo ...@@ -361,10 +362,10 @@ Future<Directory> _templateImageDirectory(String name, FileSystem fileSystem, Lo
logger: logger, logger: logger,
); );
final Uri? imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot; final Uri? imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot;
return fileSystem.directory(imagePackageLibDir) final Directory templateDirectory = fileSystem.directory(imagePackageLibDir)
.parent .parent
.childDirectory('templates') .childDirectory('templates');
.childDirectory(name); return name == null ? templateDirectory : templateDirectory.childDirectory(name);
} }
String _escapeKotlinKeywords(String androidIdentifier) { String _escapeKotlinKeywords(String androidIdentifier) {
......
...@@ -386,10 +386,96 @@ void main() { ...@@ -386,10 +386,96 @@ void main() {
final Directory project = globals.fs.directory('ios/Runner.xcodeproj')..createSync(recursive: true); final Directory project = globals.fs.directory('ios/Runner.xcodeproj')..createSync(recursive: true);
project.childFile('project.pbxproj').createSync(); project.childFile('project.pbxproj').createSync();
final BuildableIOSApp? iosApp = await IOSApp.fromIosProject( final BuildableIOSApp? iosApp = await IOSApp.fromIosProject(
FlutterProject.fromDirectory(globals.fs.currentDirectory).ios, null) as BuildableIOSApp?; FlutterProject.fromDirectory(globals.fs.currentDirectory).ios, null) as BuildableIOSApp?;
expect(iosApp, null); expect(iosApp, null);
}, overrides: overrides); }, overrides: overrides);
testUsingContext('returns project app icon dirname', () async {
final BuildableIOSApp iosApp = BuildableIOSApp(
IosProject.fromFlutter(FlutterProject.fromDirectory(globals.fs.currentDirectory)),
'com.foo.bar',
'Runner',
);
final String iconDirSuffix = globals.fs.path.join(
'Runner',
'Assets.xcassets',
'AppIcon.appiconset',
);
expect(iosApp.projectAppIconDirName, globals.fs.path.join('ios', iconDirSuffix));
}, overrides: overrides);
testUsingContext('returns template app icon dirname for Contents.json', () async {
final BuildableIOSApp iosApp = BuildableIOSApp(
IosProject.fromFlutter(FlutterProject.fromDirectory(globals.fs.currentDirectory)),
'com.foo.bar',
'Runner',
);
final String iconDirSuffix = globals.fs.path.join(
'Runner',
'Assets.xcassets',
'AppIcon.appiconset',
);
expect(
iosApp.templateAppIconDirNameForContentsJson,
globals.fs.path.join(
Cache.flutterRoot!,
'packages',
'flutter_tools',
'templates',
'app_shared',
'ios.tmpl',
iconDirSuffix,
),
);
}, overrides: overrides);
testUsingContext('returns template app icon dirname for images', () async {
final String toolsDir = globals.fs.path.join(
Cache.flutterRoot!,
'packages',
'flutter_tools',
);
final String packageConfigPath = globals.fs.path.join(
toolsDir,
'.dart_tool',
'package_config.json'
);
globals.fs.file(packageConfigPath)
..createSync(recursive: true)
..writeAsStringSync('''
{
"configVersion": 2,
"packages": [
{
"name": "flutter_template_images",
"rootUri": "/flutter_template_images",
"packageUri": "lib/",
"languageVersion": "2.12"
}
]
}
''');
final BuildableIOSApp iosApp = BuildableIOSApp(
IosProject.fromFlutter(FlutterProject.fromDirectory(globals.fs.currentDirectory)),
'com.foo.bar',
'Runner');
final String iconDirSuffix = globals.fs.path.join(
'Runner',
'Assets.xcassets',
'AppIcon.appiconset',
);
expect(
await iosApp.templateAppIconDirNameForImages,
globals.fs.path.absolute(
'flutter_template_images',
'templates',
'app_shared',
'ios.tmpl',
iconDirSuffix,
),
);
}, overrides: overrides);
}); });
group('FuchsiaApp', () { group('FuchsiaApp', () {
......
...@@ -9,8 +9,11 @@ import 'package:file_testing/file_testing.dart'; ...@@ -9,8 +9,11 @@ import 'package:file_testing/file_testing.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/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/template.dart'; import 'package:flutter_tools/src/base/template.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/template.dart'; import 'package:flutter_tools/src/template.dart';
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart';
void main() { void main() {
testWithoutContext('Template constructor throws ToolExit when source directory is missing', () { testWithoutContext('Template constructor throws ToolExit when source directory is missing', () {
...@@ -43,6 +46,79 @@ void main() { ...@@ -43,6 +46,79 @@ void main() {
throwsToolExit()); throwsToolExit());
}); });
group('template image directory', () {
final Map<Type, Generator> overrides = <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
};
testUsingContext('templateImageDirectory returns parent template directory if passed null name', () async {
final String packageConfigPath = globals.fs.path.join(
Cache.flutterRoot!,
'packages',
'flutter_tools',
'.dart_tool',
'package_config.json',
);
globals.fs.file(packageConfigPath)
..createSync(recursive: true)
..writeAsStringSync('''
{
"configVersion": 2,
"packages": [
{
"name": "flutter_template_images",
"rootUri": "/flutter_template_images",
"packageUri": "lib/",
"languageVersion": "2.12"
}
]
}
''');
expect(
(await templateImageDirectory(null, globals.fs, globals.logger)).path,
globals.fs.path.absolute(
'flutter_template_images',
'templates',
),
);
}, overrides: overrides);
testUsingContext('templateImageDirectory returns the directory containing the `name` template directory', () async {
final String packageConfigPath = globals.fs.path.join(
Cache.flutterRoot!,
'packages',
'flutter_tools',
'.dart_tool',
'package_config.json',
);
globals.fs.file(packageConfigPath)
..createSync(recursive: true)
..writeAsStringSync('''
{
"configVersion": 2,
"packages": [
{
"name": "flutter_template_images",
"rootUri": "/flutter_template_images",
"packageUri": "lib/",
"languageVersion": "2.12"
}
]
}
''');
expect(
(await templateImageDirectory('app_shared', globals.fs, globals.logger)).path,
globals.fs.path.absolute(
'flutter_template_images',
'templates',
'app_shared',
),
);
}, overrides: overrides);
});
group('renders template', () { group('renders template', () {
late Directory destination; late Directory destination;
const String imageName = 'some_image.png'; const String imageName = 'some_image.png';
......
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