| OLD | NEW |
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2008 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2008 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 """ | 5 """ |
| 6 Commandline modification of Xcode project files | 6 Commandline modification of Xcode project files |
| 7 """ | 7 """ |
| 8 | 8 |
| 9 import sys | 9 import sys |
| 10 import os | 10 import os |
| (...skipping 155 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 166 # Read in the section, using custom classes where we need them | 166 # Read in the section, using custom classes where we need them |
| 167 section_end_match = self.__class__.SECTION_END_RE.match( | 167 section_end_match = self.__class__.SECTION_END_RE.match( |
| 168 self._raw_content[parse_line_no]) | 168 self._raw_content[parse_line_no]) |
| 169 while not section_end_match: | 169 while not section_end_match: |
| 170 # Unhandled lines | 170 # Unhandled lines |
| 171 content = self._raw_content[parse_line_no] | 171 content = self._raw_content[parse_line_no] |
| 172 # Sections we can parse line-by-line | 172 # Sections we can parse line-by-line |
| 173 if section in ('PBXBuildFile', 'PBXFileReference'): | 173 if section in ('PBXBuildFile', 'PBXFileReference'): |
| 174 content = eval('%s.FromContent(content)' % section) | 174 content = eval('%s.FromContent(content)' % section) |
| 175 # Multiline sections | 175 # Multiline sections |
| 176 elif section in ('PBXGroup', 'PBXVariantGroup', 'PBXProject', | 176 elif section in ('PBXGroup', 'PBXVariantGroup', 'PBXProject', |
| 177 'PBXNativeTarget', 'PBXSourcesBuildPhase'): | 177 'PBXNativeTarget', 'PBXSourcesBuildPhase'): |
| 178 # Accumulate lines | 178 # Accumulate lines |
| 179 content_lines = [] | 179 content_lines = [] |
| 180 while 1: | 180 while 1: |
| 181 content_lines.append(content) | 181 content_lines.append(content) |
| 182 if content == '\t\t};\n': break | 182 if content == '\t\t};\n': break |
| 183 parse_line_no += 1 | 183 parse_line_no += 1 |
| 184 content = self._raw_content[parse_line_no] | 184 content = self._raw_content[parse_line_no] |
| 185 content = eval('%s.FromContent(content_lines)' % section) | 185 content = eval('%s.FromContent(content_lines)' % section) |
| 186 | 186 |
| (...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 267 if not found_uuid: | 267 if not found_uuid: |
| 268 raise RuntimeError('XcodeProject group descent failed to find %s' % | 268 raise RuntimeError('XcodeProject group descent failed to find %s' % |
| 269 child_uuid) | 269 child_uuid) |
| 270 self._root_group = None | 270 self._root_group = None |
| 271 for group in self._sections['PBXGroup']: | 271 for group in self._sections['PBXGroup']: |
| 272 if group.uuid == self._root_group_uuid: | 272 if group.uuid == self._root_group_uuid: |
| 273 self._root_group = group | 273 self._root_group = group |
| 274 GroupPathRecurse(group, self.source_root_path) | 274 GroupPathRecurse(group, self.source_root_path) |
| 275 if not self._root_group: | 275 if not self._root_group: |
| 276 raise RuntimeError('XcodeProject failed to find root group by UUID') | 276 raise RuntimeError('XcodeProject failed to find root group by UUID') |
| 277 | 277 |
| 278 def FileContent(self): | 278 def FileContent(self): |
| 279 """Generate and return the project file content as a list of lines""" | 279 """Generate and return the project file content as a list of lines""" |
| 280 content = [] | 280 content = [] |
| 281 content.extend(self._header[:-1]) | 281 content.extend(self._header[:-1]) |
| 282 for section in self._section_order: | 282 for section in self._section_order: |
| 283 content.append('\n/* Begin %s section */\n' % section) | 283 content.append('\n/* Begin %s section */\n' % section) |
| 284 for section_content in self._sections[section]: | 284 for section_content in self._sections[section]: |
| 285 content.append(str(section_content)) | 285 content.append(str(section_content)) |
| 286 content.append('/* End %s section */\n' % section) | 286 content.append('/* End %s section */\n' % section) |
| 287 content.extend(self._tail) | 287 content.extend(self._tail) |
| (...skipping 98 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 386 Returns: | 386 Returns: |
| 387 PBXFileReference instance | 387 PBXFileReference instance |
| 388 """ | 388 """ |
| 389 for file_ref in self._sections['PBXFileReference']: | 389 for file_ref in self._sections['PBXFileReference']: |
| 390 if file_ref.uuid == uuid: | 390 if file_ref.uuid == uuid: |
| 391 return file_ref | 391 return file_ref |
| 392 raise RuntimeError('Missing PBXFileReference for UUID "%s"' % uuid) | 392 raise RuntimeError('Missing PBXFileReference for UUID "%s"' % uuid) |
| 393 | 393 |
| 394 def RemoveSourceFileReference(self, file_ref): | 394 def RemoveSourceFileReference(self, file_ref): |
| 395 """Remove a source file's PBXFileReference from the project, cleaning up all | 395 """Remove a source file's PBXFileReference from the project, cleaning up all |
| 396 PBXGroup and PBXBuildFile references to that PBXFileReference and | 396 PBXGroup and PBXBuildFile references to that PBXFileReference and |
| 397 furthermore, removing any PBXBuildFiles from all PBXNativeTarget source | 397 furthermore, removing any PBXBuildFiles from all PBXNativeTarget source |
| 398 lists. | 398 lists. |
| 399 | 399 |
| 400 Args: | 400 Args: |
| 401 file_ref: PBXFileReference instance | 401 file_ref: PBXFileReference instance |
| 402 | 402 |
| 403 Raises: | 403 Raises: |
| 404 RuntimeError if |file_ref| is not a source file reference in PBXBuildFile | 404 RuntimeError if |file_ref| is not a source file reference in PBXBuildFile |
| 405 """ | 405 """ |
| 406 self._sections['PBXFileReference'].remove(file_ref) | 406 self._sections['PBXFileReference'].remove(file_ref) |
| (...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 470 """Convert a path to a group-relative path if possible | 470 """Convert a path to a group-relative path if possible |
| 471 | 471 |
| 472 Args: | 472 Args: |
| 473 abs_path: Absolute path to convert | 473 abs_path: Absolute path to convert |
| 474 | 474 |
| 475 Returns: | 475 Returns: |
| 476 Parent PBXGroup instance if possible or None | 476 Parent PBXGroup instance if possible or None |
| 477 """ | 477 """ |
| 478 needed_path = os.path.dirname(abs_path) | 478 needed_path = os.path.dirname(abs_path) |
| 479 possible_groups = [ g for g in self._sections['PBXGroup'] | 479 possible_groups = [ g for g in self._sections['PBXGroup'] |
| 480 if g.abs_path == needed_path and | 480 if g.abs_path == needed_path and |
| 481 not g.name in NON_SOURCE_GROUP_NAMES ] | 481 not g.name in NON_SOURCE_GROUP_NAMES ] |
| 482 if len(possible_groups) < 1: | 482 if len(possible_groups) < 1: |
| 483 return None | 483 return None |
| 484 elif len(possible_groups) == 1: | 484 elif len(possible_groups) == 1: |
| 485 return possible_groups[0] | 485 return possible_groups[0] |
| 486 # Multiple groups match, try to find the best using some simple | 486 # Multiple groups match, try to find the best using some simple |
| 487 # heuristics. Does only one group contain source? | 487 # heuristics. Does only one group contain source? |
| 488 groups_with_source = [] | 488 groups_with_source = [] |
| 489 for group in possible_groups: | 489 for group in possible_groups: |
| 490 for child_uuid in group.child_uuids: | 490 for child_uuid in group.child_uuids: |
| (...skipping 11 matching lines...) Expand all Loading... |
| 502 if g is not self._root_group ] | 502 if g is not self._root_group ] |
| 503 if len(non_root_groups) == 1: | 503 if len(non_root_groups) == 1: |
| 504 return non_root_groups[0] | 504 return non_root_groups[0] |
| 505 # Best guess | 505 # Best guess |
| 506 if len(non_root_groups): | 506 if len(non_root_groups): |
| 507 return non_root_groups[0] | 507 return non_root_groups[0] |
| 508 elif len(groups_with_source): | 508 elif len(groups_with_source): |
| 509 return groups_with_source[0] | 509 return groups_with_source[0] |
| 510 else: | 510 else: |
| 511 return possible_groups[0] | 511 return possible_groups[0] |
| 512 | 512 |
| 513 def AddSourceFile(self, path): | 513 def AddSourceFile(self, path): |
| 514 """Add a source file to the project, attempting to position it | 514 """Add a source file to the project, attempting to position it |
| 515 in the GUI group heirarchy reasonably. | 515 in the GUI group heirarchy reasonably. |
| 516 | 516 |
| 517 NOTE: Adding a source file does not add it to any targets | 517 NOTE: Adding a source file does not add it to any targets |
| 518 | 518 |
| 519 Args: | 519 Args: |
| 520 path: Absolute path to the file to add | 520 path: Absolute path to the file to add |
| 521 | 521 |
| 522 Returns: | 522 Returns: |
| (...skipping 153 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 676 | 676 |
| 677 @classmethod | 677 @classmethod |
| 678 def FromContent(klass, content_line): | 678 def FromContent(klass, content_line): |
| 679 parsed = klass.PBXBUILDFILE_LINE_RE.match(content_line) | 679 parsed = klass.PBXBUILDFILE_LINE_RE.match(content_line) |
| 680 if not parsed: | 680 if not parsed: |
| 681 raise RuntimeError('PBXBuildFile unable to parse content:\n%s' | 681 raise RuntimeError('PBXBuildFile unable to parse content:\n%s' |
| 682 % content_line) | 682 % content_line) |
| 683 if parsed.group(2) != parsed.group(5): | 683 if parsed.group(2) != parsed.group(5): |
| 684 raise RuntimeError('PBXBuildFile name mismatch "%s" vs "%s"' % | 684 raise RuntimeError('PBXBuildFile name mismatch "%s" vs "%s"' % |
| 685 (parsed.group(2), parsed.group(5))) | 685 (parsed.group(2), parsed.group(5))) |
| 686 if not parsed.group(3) in ('Sources', 'Frameworks', | 686 if not parsed.group(3) in ('Sources', 'Frameworks', |
| 687 'Resources', 'CopyFiles', | 687 'Resources', 'CopyFiles', |
| 688 'Headers', 'Copy Into Framework', | 688 'Headers', 'Copy Into Framework', |
| 689 'Rez', 'Copy Generated Headers'): | 689 'Rez', 'Copy Generated Headers'): |
| 690 raise RuntimeError('PBXBuildFile unknown type "%s"' % parsed.group(3)) | 690 raise RuntimeError('PBXBuildFile unknown type "%s"' % parsed.group(3)) |
| 691 return klass(parsed.group(1), parsed.group(2), parsed.group(3), | 691 return klass(parsed.group(1), parsed.group(2), parsed.group(3), |
| 692 parsed.group(4), parsed.group(6)) | 692 parsed.group(4), parsed.group(6)) |
| 693 | 693 |
| 694 def __init__(self, uuid, name, type, file_ref_uuid, raw_extras): | 694 def __init__(self, uuid, name, type, file_ref_uuid, raw_extras): |
| 695 self.uuid = uuid | 695 self.uuid = uuid |
| 696 self.name = name | 696 self.name = name |
| (...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 745 % content_line) | 745 % content_line) |
| 746 tree_parsed = klass.PBXFILEREFERENCE_SOURCETREE_RE.search(content_line) | 746 tree_parsed = klass.PBXFILEREFERENCE_SOURCETREE_RE.search(content_line) |
| 747 if not tree_parsed: | 747 if not tree_parsed: |
| 748 raise RuntimeError( | 748 raise RuntimeError( |
| 749 'PBXFileReference unable to parse source tree content:\n%s' | 749 'PBXFileReference unable to parse source tree content:\n%s' |
| 750 % content_line) | 750 % content_line) |
| 751 return klass(header_parsed.group(1), header_parsed.group(2), | 751 return klass(header_parsed.group(1), header_parsed.group(2), |
| 752 last_known_type, explicit_type, path_parsed.group(1), | 752 last_known_type, explicit_type, path_parsed.group(1), |
| 753 tree_parsed.group(1), content_line) | 753 tree_parsed.group(1), content_line) |
| 754 | 754 |
| 755 def __init__(self, uuid, name, last_known_file_type, explicit_file_type, | 755 def __init__(self, uuid, name, last_known_file_type, explicit_file_type, |
| 756 path, source_tree, raw_line): | 756 path, source_tree, raw_line): |
| 757 self.uuid = uuid | 757 self.uuid = uuid |
| 758 self.name = name | 758 self.name = name |
| 759 self._last_known_file_type = last_known_file_type | 759 self._last_known_file_type = last_known_file_type |
| 760 self._explicit_file_type = explicit_file_type | 760 self._explicit_file_type = explicit_file_type |
| 761 if explicit_file_type: | 761 if explicit_file_type: |
| 762 self.file_type = explicit_file_type | 762 self.file_type = explicit_file_type |
| 763 else: | 763 else: |
| 764 self.file_type = last_known_file_type | 764 self.file_type = last_known_file_type |
| 765 self.path = path | 765 self.path = path |
| (...skipping 113 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 879 | 879 |
| 880 def __init__(self, uuid, name, path, source_tree, child_uuids, child_names, | 880 def __init__(self, uuid, name, path, source_tree, child_uuids, child_names, |
| 881 tab_width, uses_tabs, indent_width): | 881 tab_width, uses_tabs, indent_width): |
| 882 self.uuid = uuid | 882 self.uuid = uuid |
| 883 self.name = name | 883 self.name = name |
| 884 self.path = path | 884 self.path = path |
| 885 self.source_tree = source_tree | 885 self.source_tree = source_tree |
| 886 self.child_uuids = child_uuids | 886 self.child_uuids = child_uuids |
| 887 self.child_names = child_names | 887 self.child_names = child_names |
| 888 self.abs_path = None | 888 self.abs_path = None |
| 889 # Semantically I'm not sure these aren't an error, but they | 889 # Semantically I'm not sure these aren't an error, but they |
| 890 # appear in some projects | 890 # appear in some projects |
| 891 self._tab_width = tab_width | 891 self._tab_width = tab_width |
| 892 self._uses_tabs = uses_tabs | 892 self._uses_tabs = uses_tabs |
| 893 self._indent_width = indent_width | 893 self._indent_width = indent_width |
| 894 | 894 |
| 895 def __str__(self): | 895 def __str__(self): |
| 896 if self.name: | 896 if self.name: |
| 897 header_comment = '/* %s */ ' % self.name | 897 header_comment = '/* %s */ ' % self.name |
| 898 elif self.path: | 898 elif self.path: |
| 899 header_comment = '/* %s */ ' % self.path | 899 header_comment = '/* %s */ ' % self.path |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 932 '\t\t\tchildren = (\n' \ | 932 '\t\t\tchildren = (\n' \ |
| 933 '%s' \ | 933 '%s' \ |
| 934 '\t\t\t);\n' \ | 934 '\t\t\t);\n' \ |
| 935 '%s' \ | 935 '%s' \ |
| 936 '%s' \ | 936 '%s' \ |
| 937 '%s' \ | 937 '%s' \ |
| 938 '\t\t\tsourceTree = %s;\n' \ | 938 '\t\t\tsourceTree = %s;\n' \ |
| 939 '%s' \ | 939 '%s' \ |
| 940 '%s' \ | 940 '%s' \ |
| 941 '\t\t};\n' % ( | 941 '\t\t};\n' % ( |
| 942 self.uuid, header_comment, | 942 self.uuid, header_comment, |
| 943 self.__class__.__name__, | 943 self.__class__.__name__, |
| 944 children, | 944 children, |
| 945 indent_width_attribute, | 945 indent_width_attribute, |
| 946 name_attribute, | 946 name_attribute, |
| 947 path_attribute, self.source_tree, | 947 path_attribute, self.source_tree, |
| 948 tab_width_attribute, uses_tabs_attribute) | 948 tab_width_attribute, uses_tabs_attribute) |
| 949 | 949 |
| 950 | 950 |
| 951 class PBXVariantGroup(PBXGroup): | 951 class PBXVariantGroup(PBXGroup): |
| 952 pass | 952 pass |
| 953 | 953 |
| 954 | 954 |
| 955 class PBXNativeTarget(object): | 955 class PBXNativeTarget(object): |
| 956 """Class for PBXNativeTarget data from an Xcode project file. | 956 """Class for PBXNativeTarget data from an Xcode project file. |
| 957 | 957 |
| 958 Attributes: | 958 Attributes: |
| 959 name: Target name | 959 name: Target name |
| 960 build_phase_uuids: Ordered list of build phase UUIDs | 960 build_phase_uuids: Ordered list of build phase UUIDs |
| (...skipping 191 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1152 project_path = os.path.join(project_path, 'project.pbxproj') | 1152 project_path = os.path.join(project_path, 'project.pbxproj') |
| 1153 if not project_path.endswith(os.sep + 'project.pbxproj'): | 1153 if not project_path.endswith(os.sep + 'project.pbxproj'): |
| 1154 option_parser.error('Invalid Xcode project file path \"%s\"' % project_path) | 1154 option_parser.error('Invalid Xcode project file path \"%s\"' % project_path) |
| 1155 if not os.path.exists(project_path): | 1155 if not os.path.exists(project_path): |
| 1156 option_parser.error('Missing Xcode project file \"%s\"' % project_path) | 1156 option_parser.error('Missing Xcode project file \"%s\"' % project_path) |
| 1157 | 1157 |
| 1158 # Construct project object | 1158 # Construct project object |
| 1159 project = XcodeProject(project_path) | 1159 project = XcodeProject(project_path) |
| 1160 | 1160 |
| 1161 # Switch on command | 1161 # Switch on command |
| 1162 if len(args) < 1: | |
| 1163 Usage(option_parser) | |
| 1164 | 1162 |
| 1165 # List native target names | 1163 # List native target names (default command) |
| 1166 elif args[0] == 'list_native_targets': | 1164 if len(args) < 1 or args[0] == 'list_native_targets': |
| 1167 # List targets | |
| 1168 if len(args) != 1: | |
| 1169 option_parser.error('list_native_targets takes no arguments') | |
| 1170 # Ape xcodebuild output | 1165 # Ape xcodebuild output |
| 1171 target_names = [] | 1166 target_names = [] |
| 1172 for target in project.NativeTargets(): | 1167 for target in project.NativeTargets(): |
| 1173 target_names.append(target.name) | 1168 target_names.append(target.name) |
| 1174 print 'Information about project "%s"\n Native Targets:\n %s' % ( | 1169 print 'Information about project "%s"\n Native Targets:\n %s' % ( |
| 1175 project.name, | 1170 project.name, |
| 1176 '\n '.join(target_names)) | 1171 '\n '.join(target_names)) |
| 1177 | 1172 |
| 1173 if len(args) < 1: |
| 1174 # Be friendly and print some hints for further actions. |
| 1175 print |
| 1176 print 'To add or remove files from given target, run:' |
| 1177 print '\txcodebodge.py -p <project> -t <target> add_source <file_name>' |
| 1178 print '\txcodebodge.py -p <project> -t <target> remove_source <file_name>' |
| 1179 |
| 1178 # List files in a native target | 1180 # List files in a native target |
| 1179 elif args[0] == 'list_target_sources': | 1181 elif args[0] == 'list_target_sources': |
| 1180 if len(args) != 1: | 1182 if len(args) != 1: |
| 1181 option_parser.error('list_target_sources takes no arguments') | 1183 option_parser.error('list_target_sources takes no arguments') |
| 1182 if not options.target: | 1184 if not options.target: |
| 1183 option_parser.error('list_target_sources requires a target') | 1185 option_parser.error('list_target_sources requires a target') |
| 1184 # Validate target and get list of files | 1186 # Validate target and get list of files |
| 1185 target = project.NativeTargetForName(options.target) | 1187 target = project.NativeTargetForName(options.target) |
| 1186 if not target: | 1188 if not target: |
| 1187 option_parser.error('No native target named "%s"' % options.target) | 1189 option_parser.error('No native target named "%s"' % options.target) |
| (...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1250 project.RelativeSourceRootPath(file_ref.abs_path) == source_path))
): | 1252 project.RelativeSourceRootPath(file_ref.abs_path) == source_path))
): |
| 1251 source_ref = file_ref | 1253 source_ref = file_ref |
| 1252 break | 1254 break |
| 1253 if not source_ref: | 1255 if not source_ref: |
| 1254 # Create a new source file ref | 1256 # Create a new source file ref |
| 1255 source_ref = project.AddSourceFile(os.path.abspath(source_path)) | 1257 source_ref = project.AddSourceFile(os.path.abspath(source_path)) |
| 1256 # Add the new source file reference to the target if its a safe type | 1258 # Add the new source file reference to the target if its a safe type |
| 1257 if source_ref.file_type in SOURCES_XCODE_FILETYPES: | 1259 if source_ref.file_type in SOURCES_XCODE_FILETYPES: |
| 1258 project.AddSourceFileToSourcesBuildPhase(source_ref, sources_phase) | 1260 project.AddSourceFileToSourcesBuildPhase(source_ref, sources_phase) |
| 1259 project.Update() | 1261 project.Update() |
| 1260 | 1262 |
| 1261 # Private sanity check. On an unmodified project make sure our output is | 1263 # Private sanity check. On an unmodified project make sure our output is |
| 1262 # the same as the input | 1264 # the same as the input |
| 1263 elif args[0] == 'parse_sanity': | 1265 elif args[0] == 'parse_sanity': |
| 1264 if ''.join(project.FileContent()) != ''.join(project._raw_content): | 1266 if ''.join(project.FileContent()) != ''.join(project._raw_content): |
| 1265 option_parser.error('Project rewrite sanity fail "%s"' % project.path) | 1267 option_parser.error('Project rewrite sanity fail "%s"' % project.path) |
| 1266 | 1268 |
| 1267 else: | 1269 else: |
| 1268 Usage(option_parser) | 1270 Usage(option_parser) |
| 1269 | 1271 |
| 1270 | 1272 |
| 1271 if __name__ == '__main__': | 1273 if __name__ == '__main__': |
| 1272 Main() | 1274 Main() |
| OLD | NEW |