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' ;
56import 'package:file/file.dart' ;
67import '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 ('\n Warning: 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 ('\n You 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 ('\n You 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 ('\n To 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) {
0 commit comments