From fd6eca645bb5a22a1526ae8d92daf619bf82d63f Mon Sep 17 00:00:00 2001 From: Omar Al-Ithawi Date: Sun, 30 Jun 2024 17:35:11 +0200 Subject: [PATCH 1/4] fix: support adding and removing Localizable.strings file XCode project supports adding Localizable.strings file in multiple variants in which each language gets its own variant with the language code as the file "name" property. Before ====== - The file would be added under incorrect Resources section - The file ID would still be referenced in the Localizable.strings PBXVariantGroup section event though it has been removed - File cannot have a custom name After ===== - The file can be added to the correct group via the `parent` parameter in ProjectFiles.add_file() method - When a file is removed, it's references are removed from both PBXGroup and PBXVariant group sections - Files can have custom names, differing from its path - More fixes for the `productRef` issue Fun fact ======== This code is written on Linux, with help from my colleagues to debug the version once it's ready on their iOS devices. Thanks! ======= This project has been very helpful for us in openedx/openedx-app-ios. Thanks for creating it and double thanks for your fast triage for the other pull request. --- pbxproj/pbxextensions/ProjectFiles.py | 18 ++++++++++-------- pbxproj/pbxextensions/ProjectGroups.py | 8 ++++---- pbxproj/pbxsections/PBXFileReference.py | 6 +++--- pbxproj/pbxsections/PBXVariantGroup.py | 13 +++++++++++++ pbxproj/pbxsections/__init__.py | 1 + 5 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 pbxproj/pbxsections/PBXVariantGroup.py diff --git a/pbxproj/pbxextensions/ProjectFiles.py b/pbxproj/pbxextensions/ProjectFiles.py index 9057c8df..7032e2dc 100644 --- a/pbxproj/pbxextensions/ProjectFiles.py +++ b/pbxproj/pbxextensions/ProjectFiles.py @@ -145,7 +145,7 @@ class ProjectFiles: def __init__(self): raise EnvironmentError('This class cannot be instantiated directly, use XcodeProject instead') - def add_file(self, path, parent=None, tree=TreeType.SOURCE_ROOT, target_name=None, force=True, + def add_file(self, path, name=None, parent=None, tree=TreeType.SOURCE_ROOT, target_name=None, force=True, file_options=FileOptions()): """ Adds a file to the project, taking care of the type of the file and creating additional structures depending on @@ -153,6 +153,7 @@ def add_file(self, path, parent=None, tree=TreeType.SOURCE_ROOT, target_name=Non Header file will be added to the headers sections, but not compiled, whereas the source files will be added to the compilation phase. :param path: Path to the file to be added + :param name: Optional custom name of the file, if None is passed then "path" will be used. :param parent: Parent group to be added under :param tree: Tree where the path is relative to :param target_name: Target name or list of target names where the file should be added (none for every target) @@ -167,7 +168,7 @@ def add_file(self, path, parent=None, tree=TreeType.SOURCE_ROOT, target_name=Non if target_name.__len__() == 0: return [] - file_ref, abs_path, path, tree, expected_build_phase = self._add_file_reference(path, parent, tree, force, + file_ref, abs_path, path, tree, expected_build_phase = self._add_file_reference(path, name, parent, tree, force, file_options) if path is None or tree is None: return None @@ -199,7 +200,7 @@ def _filter_targets_without_path(self, path, target_name): build_phase = self.get_object(build_phase_id) for build_file_id in build_phase.files: build_file = self.get_object(build_file_id) - if build_file is None: + if build_file is None or not hasattr(build_file, 'fileRef'): continue file_ref = self.get_object(build_file.fileRef) @@ -236,7 +237,7 @@ def add_project(self, path, parent=None, tree=TreeType.GROUP, target_name=None, if not force and self._file_exists(path): return [] - file_ref, _, path, tree, expected_build_phase = self._add_file_reference(path, parent, tree, force, + file_ref, _, path, tree, expected_build_phase = self._add_file_reference(path, None, parent, tree, force, file_options) if path is None or tree is None: return None @@ -360,7 +361,8 @@ def remove_file_by_id(self, file_id, target_name=None): return True # remove the file from any groups if there is no reference from any target - for group in filter(lambda x: file_ref.get_id() in x.children, self.objects.get_objects_in_section('PBXGroup')): + for group in filter(lambda x: file_ref.get_id() in x.children, + self.objects.get_objects_in_section('PBXGroup', 'PBXVariantGroup')): group.remove_child(file_ref) # the file is not referenced in any build file, remove it @@ -572,14 +574,14 @@ def add_package_dependency(self, target_name, create_parameters=()): # miscellaneous functions, candidates to be extracted and decouple implementation - def _add_file_reference(self, path, parent, tree, force, file_options): + def _add_file_reference(self, path, name, parent, tree, force, file_options): # decide the proper tree and path to add abs_path, path, tree = ProjectFiles._get_path_and_tree(self._source_root, path, tree) if path is None or tree is None: return None, abs_path, path, tree, None # create a PBXFileReference for the new file - file_ref = PBXFileReference.create(path, tree) + file_ref = PBXFileReference.create(path, name, tree) # determine the type of the new file: file_type, expected_build_phase = ProjectFiles._determine_file_type(file_ref, file_options.ignore_unknown_type) @@ -637,7 +639,7 @@ def _create_build_products(self, product_ref, target_name): @classmethod def _determine_file_type(cls, file_ref, unknown_type_allowed): - ext = os.path.splitext(file_ref.get_name())[1] + ext = os.path.splitext(file_ref.path)[1] if os.path.isdir(os.path.abspath(file_ref.path)) and ext not in ProjectFiles._SPECIAL_FOLDERS: file_type = 'folder' build_phase = 'PBXResourcesBuildPhase' diff --git a/pbxproj/pbxextensions/ProjectGroups.py b/pbxproj/pbxextensions/ProjectGroups.py index aa72c6e2..5202411e 100644 --- a/pbxproj/pbxextensions/ProjectGroups.py +++ b/pbxproj/pbxextensions/ProjectGroups.py @@ -62,7 +62,7 @@ def remove_group_by_id(self, group_id, recursive=True): del self.objects[group.get_id()] # remove the reference from any other group object that could be containing it. - for other_group in self.objects.get_objects_in_section('PBXGroup'): + for other_group in self.objects.get_objects_in_section('PBXGroup', 'PBXVariantGroup'): other_group.remove_child(group) return True @@ -87,14 +87,14 @@ def remove_group_by_name(self, group_name, recursive=True): return True - def get_groups_by_name(self, name, parent=None): + def get_groups_by_name(self, name, parent=None, section='PBXGroup'): """ Retrieve all groups matching the given name and optionally filtered by the given parent node. :param name: The name of the group that has to be returned :param parent: A PBXGroup object where the object has to be retrieved from. If None all matching groups are returned :return: An list of all matching groups """ - groups = self.objects.get_objects_in_section('PBXGroup') + groups = self.objects.get_objects_in_section(section) groups = [group for group in groups if group.get_name() == name] if parent: @@ -110,7 +110,7 @@ def get_groups_by_path(self, path, parent=None): :param parent: A PBXGroup object where the object has to be retrieved from. If None all matching groups are returned :return: An list of all matching groups """ - groups = self.objects.get_objects_in_section('PBXGroup') + groups = self.objects.get_objects_in_section('PBXGroup', 'PBXVariantGroup') groups = [group for group in groups if group.get_path() == path] if parent: diff --git a/pbxproj/pbxsections/PBXFileReference.py b/pbxproj/pbxsections/PBXFileReference.py index a44e2459..a30c05e0 100644 --- a/pbxproj/pbxsections/PBXFileReference.py +++ b/pbxproj/pbxsections/PBXFileReference.py @@ -5,12 +5,12 @@ class PBXFileReference(PBXGenericObject): @classmethod - def create(cls, path, tree='SOURCE_ROOT'): + def create(cls, path, name=None, tree='SOURCE_ROOT'): return cls().parse({ '_id': cls._generate_id(), 'isa': cls.__name__, 'path': path, - 'name': os.path.split(path)[1], + 'name': name or os.path.split(path)[1], 'sourceTree': tree }) @@ -48,7 +48,7 @@ def remove(self, recursive=True): build_file.remove(recursive) # search for each group that has a reference to the build file and remove it from it. - for group in parent.get_objects_in_section('PBXGroup'): + for group in parent.get_objects_in_section('PBXGroup', 'PBXVariantGroup'): if self.get_id() in group.children: group.remove_child(self) diff --git a/pbxproj/pbxsections/PBXVariantGroup.py b/pbxproj/pbxsections/PBXVariantGroup.py new file mode 100644 index 00000000..e26480ae --- /dev/null +++ b/pbxproj/pbxsections/PBXVariantGroup.py @@ -0,0 +1,13 @@ +from pbxproj.pbxsections import PBXFileReference, PBXGroup + + +class PBXVariantGroup(PBXGroup): + def add_child(self, child): + # Note: PBXVariantGroup is typically used for Localizable.strings + # If other children type is needed e.g. PBXFileReference, edit this + # function. + if not isinstance(child, PBXFileReference): + return False + + self.children.append(child.get_id()) + return True diff --git a/pbxproj/pbxsections/__init__.py b/pbxproj/pbxsections/__init__.py index ac166914..64e8679f 100644 --- a/pbxproj/pbxsections/__init__.py +++ b/pbxproj/pbxsections/__init__.py @@ -16,6 +16,7 @@ from pbxproj.pbxsections.PBXLegacyTarget import * from pbxproj.pbxsections.PBXReferenceProxy import * from pbxproj.pbxsections.PBXGroup import * +from pbxproj.pbxsections.PBXVariantGroup import * from pbxproj.pbxsections.XCSwiftPackageProductDependency import * from pbxproj.pbxsections.XCRemoteSwiftPackageReference import * from pbxproj.pbxsections.XCLocalSwiftPackageReference import * From a9187b42dc224827f162c3e7b9b34f4c0d2654ee Mon Sep 17 00:00:00 2001 From: Omar Al-Ithawi Date: Sun, 30 Jun 2024 17:59:30 +0200 Subject: [PATCH 2/4] fix: fixup previous commit --- pbxproj/pbxextensions/ProjectFiles.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pbxproj/pbxextensions/ProjectFiles.py b/pbxproj/pbxextensions/ProjectFiles.py index 7032e2dc..c52e5592 100644 --- a/pbxproj/pbxextensions/ProjectFiles.py +++ b/pbxproj/pbxextensions/ProjectFiles.py @@ -414,8 +414,8 @@ def add_folder(self, path, parent=None, excludes=None, recursive=True, create_gr # add the top folder as a group, make it the new parent path = os.path.abspath(path) if not create_groups and os.path.splitext(path)[1] not in ProjectFiles._SPECIAL_FOLDERS: - return self.add_file(path, parent, target_name=target_name, force=False, tree=TreeType.GROUP, - file_options=file_options) + return self.add_file(path, name=None, parent=parent, target_name=target_name, + force=False, tree=TreeType.GROUP, file_options=file_options) parent = self.get_or_create_group(os.path.split(path)[1], path, parent, make_relative=file_options.add_groups_relative) @@ -430,8 +430,8 @@ def add_folder(self, path, parent=None, excludes=None, recursive=True, create_gr if os.path.isfile(full_path) or os.path.splitext(child)[1] in ProjectFiles._SPECIAL_FOLDERS or \ not create_groups: # check if the file exists already, if not add it - children = self.add_file(full_path, parent, target_name=target_name, force=False, tree=TreeType.GROUP, - file_options=file_options) + children = self.add_file(full_path, name=None, parent=parent, target_name=target_name, + force=False, tree=TreeType.GROUP, file_options=file_options) else: # if recursive is true, go deeper, otherwise create the group here. if recursive: From 16a373c3e0bb91799a0351d5fe1fd9a67a88993e Mon Sep 17 00:00:00 2001 From: Omar Al-Ithawi Date: Wed, 3 Jul 2024 17:42:25 +0200 Subject: [PATCH 3/4] Update pbxproj/pbxextensions/ProjectFiles.py Co-authored-by: Ignacio Calderon --- pbxproj/pbxextensions/ProjectFiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pbxproj/pbxextensions/ProjectFiles.py b/pbxproj/pbxextensions/ProjectFiles.py index c52e5592..6ec66f3a 100644 --- a/pbxproj/pbxextensions/ProjectFiles.py +++ b/pbxproj/pbxextensions/ProjectFiles.py @@ -430,7 +430,7 @@ def add_folder(self, path, parent=None, excludes=None, recursive=True, create_gr if os.path.isfile(full_path) or os.path.splitext(child)[1] in ProjectFiles._SPECIAL_FOLDERS or \ not create_groups: # check if the file exists already, if not add it - children = self.add_file(full_path, name=None, parent=parent, target_name=target_name, + children = self.add_file(full_path, parent=parent, target_name=target_name, force=False, tree=TreeType.GROUP, file_options=file_options) else: # if recursive is true, go deeper, otherwise create the group here. From be404e5fd8002870a1869dcd89f1eb9c89da904b Mon Sep 17 00:00:00 2001 From: Omar Al-Ithawi Date: Wed, 3 Jul 2024 18:17:34 +0200 Subject: [PATCH 4/4] Update pbxproj/pbxextensions/ProjectFiles.py Co-authored-by: Ignacio Calderon --- pbxproj/pbxextensions/ProjectFiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pbxproj/pbxextensions/ProjectFiles.py b/pbxproj/pbxextensions/ProjectFiles.py index 6ec66f3a..d27296f4 100644 --- a/pbxproj/pbxextensions/ProjectFiles.py +++ b/pbxproj/pbxextensions/ProjectFiles.py @@ -414,7 +414,7 @@ def add_folder(self, path, parent=None, excludes=None, recursive=True, create_gr # add the top folder as a group, make it the new parent path = os.path.abspath(path) if not create_groups and os.path.splitext(path)[1] not in ProjectFiles._SPECIAL_FOLDERS: - return self.add_file(path, name=None, parent=parent, target_name=target_name, + return self.add_file(path, parent=parent, target_name=target_name, force=False, tree=TreeType.GROUP, file_options=file_options) parent = self.get_or_create_group(os.path.split(path)[1], path, parent, make_relative=file_options.add_groups_relative)