2
2
// Use of this source code is governed by a BSD-style license that can be
3
3
// found in the LICENSE file.
4
4
5
+ import 'package:crypto/crypto.dart' ;
5
6
import 'package:file/file.dart' ;
6
7
import 'package:meta/meta.dart' ;
7
8
@@ -130,7 +131,63 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
130
131
return super .validateCommand ();
131
132
}
132
133
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 {
134
191
final BuildableIOSApp app = await buildableIOSApp;
135
192
136
193
final String plistPath = app.builtInfoPlistPathAfterArchive;
@@ -148,21 +205,13 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
148
205
xcodeProjectSettingsMap['Deployment Target' ] = globals.plistParser.getStringValueFromFile (plistPath, PlistParser .kMinimumOSVersionKey);
149
206
xcodeProjectSettingsMap['Bundle Identifier' ] = globals.plistParser.getStringValueFromFile (plistPath, PlistParser .kCFBundleIdentifierKey);
150
207
151
- final StringBuffer buffer = StringBuffer ();
152
208
xcodeProjectSettingsMap.forEach ((String title, String ? info) {
153
- buffer .writeln ('$title : ${info ?? "Missing" }' );
209
+ messageBuffer .writeln ('$title : ${info ?? "Missing" }' );
154
210
});
155
211
156
- final String message;
157
212
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.' );
164
214
}
165
- globals.printBox (message, title: 'App Settings' );
166
215
}
167
216
168
217
@override
@@ -171,7 +220,11 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
171
220
displayNullSafetyMode (buildInfo);
172
221
final FlutterCommandResult xcarchiveResult = await super .runCommand ();
173
222
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' );
175
228
176
229
// xcarchive failed or not at expected location.
177
230
if (xcarchiveResult.exitStatus != ExitStatus .success) {
0 commit comments