Skip to content

Commit dcae424

Browse files
authored
[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
1 parent b5345ff commit dcae424

File tree

6 files changed

+452
-46
lines changed

6 files changed

+452
-46
lines changed

packages/flutter_tools/lib/src/commands/build_ios.dart

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:crypto/crypto.dart';
56
import 'package:file/file.dart';
67
import 'package:meta/meta.dart';
78

@@ -130,7 +131,63 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
130131
return super.validateCommand();
131132
}
132133

133-
Future<void> _validateXcodeBuildSettingsAfterArchive() async {
134+
// 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.
135+
Map<String, String> _parseIconContentsJson(String contentsJsonDirName) {
136+
final Directory contentsJsonDirectory = globals.fs.directory(contentsJsonDirName);
137+
if (!contentsJsonDirectory.existsSync()) {
138+
return <String, String>{};
139+
}
140+
final File contentsJsonFile = contentsJsonDirectory.childFile('Contents.json');
141+
final Map<String, dynamic> content = json.decode(contentsJsonFile.readAsStringSync()) as Map<String, dynamic>;
142+
final List<dynamic> images = content['images'] as List<dynamic>? ?? <dynamic>[];
143+
144+
final Map<String, String> iconInfo = <String, String>{};
145+
146+
for (final dynamic image in images) {
147+
final Map<String, dynamic> imageMap = image as Map<String, dynamic>;
148+
final String? idiom = imageMap['idiom'] as String?;
149+
final String? size = imageMap['size'] as String?;
150+
final String? scale = imageMap['scale'] as String?;
151+
final String? fileName = imageMap['filename'] as String?;
152+
153+
if (size != null && idiom != null && scale != null && fileName != null) {
154+
iconInfo['$idiom $size $scale'] = fileName;
155+
}
156+
}
157+
158+
return iconInfo;
159+
}
160+
161+
Future<void> _validateIconsAfterArchive(StringBuffer messageBuffer) async {
162+
final BuildableIOSApp app = await buildableIOSApp;
163+
final String templateIconImageDirName = await app.templateAppIconDirNameForImages;
164+
165+
final Map<String, String> templateIconMap = _parseIconContentsJson(app.templateAppIconDirNameForContentsJson);
166+
final Map<String, String> projectIconMap = _parseIconContentsJson(app.projectAppIconDirName);
167+
168+
// find if any of the project icons conflict with template icons
169+
final bool hasConflict = projectIconMap.entries
170+
.where((MapEntry<String, String> entry) {
171+
final String projectIconFileName = entry.value;
172+
final String? templateIconFileName = templateIconMap[entry.key];
173+
if (templateIconFileName == null) {
174+
return false;
175+
}
176+
177+
final File projectIconFile = globals.fs.file(globals.fs.path.join(app.projectAppIconDirName, projectIconFileName));
178+
final File templateIconFile = globals.fs.file(globals.fs.path.join(templateIconImageDirName, templateIconFileName));
179+
return projectIconFile.existsSync()
180+
&& templateIconFile.existsSync()
181+
&& md5.convert(projectIconFile.readAsBytesSync()) == md5.convert(templateIconFile.readAsBytesSync());
182+
})
183+
.isNotEmpty;
184+
185+
if (hasConflict) {
186+
messageBuffer.writeln('\nWarning: App icon is set to the default placeholder icon. Replace with unique icons.');
187+
}
188+
}
189+
190+
Future<void> _validateXcodeBuildSettingsAfterArchive(StringBuffer messageBuffer) async {
134191
final BuildableIOSApp app = await buildableIOSApp;
135192

136193
final String plistPath = app.builtInfoPlistPathAfterArchive;
@@ -148,21 +205,13 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
148205
xcodeProjectSettingsMap['Deployment Target'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kMinimumOSVersionKey);
149206
xcodeProjectSettingsMap['Bundle Identifier'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey);
150207

151-
final StringBuffer buffer = StringBuffer();
152208
xcodeProjectSettingsMap.forEach((String title, String? info) {
153-
buffer.writeln('$title: ${info ?? "Missing"}');
209+
messageBuffer.writeln('$title: ${info ?? "Missing"}');
154210
});
155211

156-
final String message;
157212
if (xcodeProjectSettingsMap.values.any((String? element) => element == null)) {
158-
buffer.writeln('\nYou must set up the missing settings');
159-
buffer.write('Instructions: https://docs.flutter.dev/deployment/ios');
160-
message = buffer.toString();
161-
} else {
162-
// remove the new line
163-
message = buffer.toString().trim();
213+
messageBuffer.writeln('\nYou must set up the missing settings.');
164214
}
165-
globals.printBox(message, title: 'App Settings');
166215
}
167216

168217
@override
@@ -171,7 +220,11 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
171220
displayNullSafetyMode(buildInfo);
172221
final FlutterCommandResult xcarchiveResult = await super.runCommand();
173222

174-
await _validateXcodeBuildSettingsAfterArchive();
223+
final StringBuffer validationMessageBuffer = StringBuffer();
224+
await _validateXcodeBuildSettingsAfterArchive(validationMessageBuffer);
225+
await _validateIconsAfterArchive(validationMessageBuffer);
226+
validationMessageBuffer.write('\nTo update the settings, please refer to https://docs.flutter.dev/deployment/ios');
227+
globals.printBox(validationMessageBuffer.toString(), title: 'App Settings');
175228

176229
// xcarchive failed or not at expected location.
177230
if (xcarchiveResult.exitStatus != ExitStatus.success) {

packages/flutter_tools/lib/src/ios/application_package.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import '../application_package.dart';
66
import '../base/file_system.dart';
77
import '../build_info.dart';
8+
import '../cache.dart';
89
import '../globals.dart' as globals;
10+
import '../template.dart';
911
import '../xcode_project.dart';
1012
import 'plist_parser.dart';
1113

@@ -151,12 +153,42 @@ class BuildableIOSApp extends IOSApp {
151153
_hostAppBundleName == null ? 'Runner.app' : _hostAppBundleName!,
152154
'Info.plist');
153155

156+
// Both project icon's image assets and Contents.json are in the same directory.
157+
String get projectAppIconDirName => globals.fs.path.join('ios', _appIconDirNameSuffix);
158+
159+
// template icon's Contents.json is in flutter_tools.
160+
String get templateAppIconDirNameForContentsJson => globals.fs.path.join(
161+
Cache.flutterRoot!,
162+
'packages',
163+
'flutter_tools',
164+
'templates',
165+
'app_shared',
166+
'ios.tmpl',
167+
_appIconDirNameSuffix,
168+
);
169+
170+
// template icon's image assets are in flutter_template_images package.
171+
Future<String> get templateAppIconDirNameForImages async {
172+
final Directory imageTemplate = await templateImageDirectory(null, globals.fs, globals.logger);
173+
return globals.fs.path.join(
174+
imageTemplate.path,
175+
'app_shared',
176+
'ios.tmpl',
177+
_appIconDirNameSuffix,
178+
);
179+
}
180+
154181
String get ipaOutputPath =>
155182
globals.fs.path.join(getIosBuildDirectory(), 'ipa');
156183

157184
String _buildAppPath(String type) {
158185
return globals.fs.path.join(getIosBuildDirectory(), type, _hostAppBundleName);
159186
}
187+
188+
String get _appIconDirNameSuffix => globals.fs.path.join(
189+
'Runner',
190+
'Assets.xcassets',
191+
'AppIcon.appiconset');
160192
}
161193

162194
class PrebuiltIOSApp extends IOSApp implements PrebuiltApplicationPackage {

packages/flutter_tools/lib/src/template.dart

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ class Template {
101101
}) async {
102102
// All named templates are placed in the 'templates' directory
103103
final Directory templateDir = _templateDirectoryInPackage(name, fileSystem);
104-
final Directory imageDir = await _templateImageDirectory(name, fileSystem, logger);
104+
final Directory imageDir = await templateImageDirectory(name, fileSystem, logger);
105105
return Template._(
106106
<Directory>[templateDir],
107107
<Directory>[imageDir],
@@ -126,8 +126,8 @@ class Template {
126126
],
127127
<Directory>[
128128
for (final String name in names)
129-
if ((await _templateImageDirectory(name, fileSystem, logger)).existsSync())
130-
await _templateImageDirectory(name, fileSystem, logger),
129+
if ((await templateImageDirectory(name, fileSystem, logger)).existsSync())
130+
await templateImageDirectory(name, fileSystem, logger),
131131
],
132132
fileSystem: fileSystem,
133133
logger: logger,
@@ -350,9 +350,10 @@ Directory _templateDirectoryInPackage(String name, FileSystem fileSystem) {
350350
return fileSystem.directory(fileSystem.path.join(templatesDir, name));
351351
}
352352

353-
// Returns the directory containing the 'name' template directory in
354-
// flutter_template_images, to resolve image placeholder against.
355-
Future<Directory> _templateImageDirectory(String name, FileSystem fileSystem, Logger logger) async {
353+
/// Returns the directory containing the 'name' template directory in
354+
/// flutter_template_images, to resolve image placeholder against.
355+
/// if 'name' is null, return the parent template directory.
356+
Future<Directory> templateImageDirectory(String? name, FileSystem fileSystem, Logger logger) async {
356357
final String toolPackagePath = fileSystem.path.join(
357358
Cache.flutterRoot!, 'packages', 'flutter_tools');
358359
final String packageFilePath = fileSystem.path.join(toolPackagePath, '.dart_tool', 'package_config.json');
@@ -361,10 +362,10 @@ Future<Directory> _templateImageDirectory(String name, FileSystem fileSystem, Lo
361362
logger: logger,
362363
);
363364
final Uri? imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot;
364-
return fileSystem.directory(imagePackageLibDir)
365+
final Directory templateDirectory = fileSystem.directory(imagePackageLibDir)
365366
.parent
366-
.childDirectory('templates')
367-
.childDirectory(name);
367+
.childDirectory('templates');
368+
return name == null ? templateDirectory : templateDirectory.childDirectory(name);
368369
}
369370

370371
String _escapeKotlinKeywords(String androidIdentifier) {

0 commit comments

Comments
 (0)