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 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:crypto/crypto.dart';
import 'package:file/file.dart';
import 'package:meta/meta.dart';
......@@ -130,7 +131,63 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
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 String plistPath = app.builtInfoPlistPathAfterArchive;
......@@ -148,21 +205,13 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
xcodeProjectSettingsMap['Deployment Target'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kMinimumOSVersionKey);
xcodeProjectSettingsMap['Bundle Identifier'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey);
final StringBuffer buffer = StringBuffer();
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)) {
buffer.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();
messageBuffer.writeln('\nYou must set up the missing settings.');
}
globals.printBox(message, title: 'App Settings');
}
@override
......@@ -171,7 +220,11 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
displayNullSafetyMode(buildInfo);
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.
if (xcarchiveResult.exitStatus != ExitStatus.success) {
......
......@@ -5,7 +5,9 @@
import '../application_package.dart';
import '../base/file_system.dart';
import '../build_info.dart';
import '../cache.dart';
import '../globals.dart' as globals;
import '../template.dart';
import '../xcode_project.dart';
import 'plist_parser.dart';
......@@ -151,12 +153,42 @@ class BuildableIOSApp extends IOSApp {
_hostAppBundleName == null ? 'Runner.app' : _hostAppBundleName!,
'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 =>
globals.fs.path.join(getIosBuildDirectory(), 'ipa');
String _buildAppPath(String type) {
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 {
......
......@@ -101,7 +101,7 @@ class Template {
}) async {
// All named templates are placed in the 'templates' directory
final Directory templateDir = _templateDirectoryInPackage(name, fileSystem);
final Directory imageDir = await _templateImageDirectory(name, fileSystem, logger);
final Directory imageDir = await templateImageDirectory(name, fileSystem, logger);
return Template._(
<Directory>[templateDir],
<Directory>[imageDir],
......@@ -126,8 +126,8 @@ class Template {
],
<Directory>[
for (final String name in names)
if ((await _templateImageDirectory(name, fileSystem, logger)).existsSync())
await _templateImageDirectory(name, fileSystem, logger),
if ((await templateImageDirectory(name, fileSystem, logger)).existsSync())
await templateImageDirectory(name, fileSystem, logger),
],
fileSystem: fileSystem,
logger: logger,
......@@ -350,9 +350,10 @@ Directory _templateDirectoryInPackage(String name, FileSystem fileSystem) {
return fileSystem.directory(fileSystem.path.join(templatesDir, name));
}
// Returns the directory containing the 'name' template directory in
// flutter_template_images, to resolve image placeholder against.
Future<Directory> _templateImageDirectory(String name, FileSystem fileSystem, Logger logger) async {
/// Returns the directory containing the 'name' template directory in
/// flutter_template_images, to resolve image placeholder against.
/// 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(
Cache.flutterRoot!, 'packages', 'flutter_tools');
final String packageFilePath = fileSystem.path.join(toolPackagePath, '.dart_tool', 'package_config.json');
......@@ -361,10 +362,10 @@ Future<Directory> _templateImageDirectory(String name, FileSystem fileSystem, Lo
logger: logger,
);
final Uri? imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot;
return fileSystem.directory(imagePackageLibDir)
final Directory templateDirectory = fileSystem.directory(imagePackageLibDir)
.parent
.childDirectory('templates')
.childDirectory(name);
.childDirectory('templates');
return name == null ? templateDirectory : templateDirectory.childDirectory(name);
}
String _escapeKotlinKeywords(String androidIdentifier) {
......
......@@ -386,10 +386,96 @@ void main() {
final Directory project = globals.fs.directory('ios/Runner.xcodeproj')..createSync(recursive: true);
project.childFile('project.pbxproj').createSync();
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);
}, 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', () {
......
......@@ -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/logger.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 '../src/common.dart';
import '../src/context.dart';
void main() {
testWithoutContext('Template constructor throws ToolExit when source directory is missing', () {
......@@ -43,6 +46,79 @@ void main() {
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', () {
late Directory destination;
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